Python 什么';插入记录后,让QTableView和数据库同步的最佳方法是什么?

Python 什么';插入记录后,让QTableView和数据库同步的最佳方法是什么?,python,pyqt,pyqt4,Python,Pyqt,Pyqt4,假设我有一个带有QSqlTableModel/Database的QTableView。我不想让用户在QTableView中编辑单元格。有CRUD按钮可以打开新的对话框表单,用户应该输入数据。用户单击对话框的“确定”按钮后,将新记录插入数据库和视图(使其同步)的最佳方式是什么,因为数据库可能在当时不可用(例如,在出现internet连接问题时插入远程数据库) 我主要关心的是我不想在视图中显示幻影记录,我希望用户知道该记录没有输入到数据库中 我放了一些python代码(但对于Qt,我的问题是相同的)

假设我有一个带有QSqlTableModel/Database的QTableView。我不想让用户在QTableView中编辑单元格。有CRUD按钮可以打开新的对话框表单,用户应该输入数据。用户单击对话框的“确定”按钮后,将新记录插入数据库和视图(使其同步)的最佳方式是什么,因为数据库可能在当时不可用(例如,在出现internet连接问题时插入远程数据库)

我主要关心的是我不想在视图中显示幻影记录,我希望用户知道该记录没有输入到数据库中

我放了一些python代码(但对于Qt,我的问题是相同的)来说明这一点,并在注释中提出了一些其他问题:

import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import *
from PyQt4.QtSql import *

class Window(QWidget):
    def __init__(self, parent=None):
        QWidget.__init__(self, parent)
        self.model = QSqlTableModel(self)
        self.model.setTable("names")
        self.model.setHeaderData(0, Qt.Horizontal, "Id")
        self.model.setHeaderData(1, Qt.Horizontal, "Name")
        self.model.setEditStrategy(QSqlTableModel.OnManualSubmit)
        self.model.select()

        self.view = QTableView()
        self.view.setModel(self.model)
        self.view.setSelectionMode(QAbstractItemView.SingleSelection)
        self.view.setSelectionBehavior(QAbstractItemView.SelectRows)
        #self.view.setColumnHidden(0, True)
        self.view.resizeColumnsToContents()
        self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.view.horizontalHeader().setStretchLastSection(True)

        addButton = QPushButton("Add")
        editButton = QPushButton("Edit")
        deleteButton = QPushButton("Delete")
        exitButton = QPushButton("Exit")

        hbox = QHBoxLayout()
        hbox.addWidget(addButton)
        hbox.addWidget(editButton)
        hbox.addWidget(deleteButton)
        hbox.addStretch()
        hbox.addWidget(exitButton)

        vbox = QVBoxLayout()
        vbox.addWidget(self.view)
        vbox.addLayout(hbox)
        self.setLayout(vbox)

        addButton.clicked.connect(self.addRecord)
        #editButton.clicked.connect(self.editRecord) # omitted for simplicity
        #deleteButton.clicked.connect(self.deleteRecord) # omitted for simplicity
        exitButton.clicked.connect(self.close)

    def addRecord(self):
        # just QInputDialog for simplicity
        value, ok = QInputDialog.getText(self, 'Input Dialog', 'Enter the name:')
        if not ok:
            return

        # Now, what is the best way to insert the record?

        # 1st approach, first in database, then model.select()
        # it seems like the most natural way to me
        query = QSqlQuery()
        query.prepare("INSERT INTO names (name) VALUES(:name)")
        query.bindValue( ":name", value )
        if query.exec_():
            self.model.select() # now we know the record is inserted to db
            # the problem with this approach is that select() can be slow
            # somehow position the view to newly added record?!
        else:
            pass
            # message to user
            # if the record can't be inserted to database, 
            # there's no way I will show that record in view

        # 2nd approach, first in view (model cache), then in database
        # actually, I don't know how to do this
        # can somebody instruct me?
        # maybe:
        # record = ...
        # self.model.insertRecord(-1, record) #
        # submitAll()
        # what if database is unavailable?
        # what if submitAll() fails?
        # in that case, how to have view and model in sync?
        # is this the right approach?

        # 3. is there some other approach?

app = QApplication(sys.argv)
db = QSqlDatabase.addDatabase("QSQLITE")
db.setDatabaseName(":memory:")
db.open()
query = QSqlQuery()
query.exec_("DROP TABLE names")
query.exec_("CREATE TABLE names(id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, name TEXT)")
query.exec_("INSERT INTO names VALUES(1, 'George')")
query.exec_("INSERT INTO names VALUES(2, 'Rita')")
query.exec_("INSERT INTO names VALUES(3, 'Jane')")
query.exec_("INSERT INTO names VALUES(4, 'Steve')")
query.exec_("INSERT INTO names VALUES(5, 'Maria')")
query.exec_("INSERT INTO names VALUES(6, 'Bill')")
window = Window()
window.resize(600, 400)
window.show()
app.exec_()

您仍然可以使用
QSqlTableModel
。您可以关闭表视图中的所有编辑触发器,然后将模型传递给数据捕获表单,并使用
QDataWidgetMapper
让小部件绑定到模型,确保提交模式设置为手动,以便您可以首先验证字段。

您仍然可以使用
QSqlTableModel
。您可以关闭表视图中的所有编辑触发器,然后将模型传递给数据捕获表单,并让小部件使用
QDataWidgetMapper
绑定到模型,确保提交模式设置为手动,以便您可以首先验证字段。

RobbieE是正确的,我可以使用表单编辑(使用QDataWidgetMapper)而不是直接编辑单元格,但我的问题不是表单或单元格编辑

我的问题是,我的例子中哪种方法更好,第一种还是第二种

我更改了代码并实现了第二种方法(我不喜欢)。这是一个好的实现吗

但问题仍然存在。(Py)Qt开发人员如何使用QtSql执行CRUD?先是数据库,然后是模型/视图,还是先是模型/视图,然后是数据库

编辑:我编辑了示例,添加了3。方法(不完整)和模拟数据库关闭的可能性。现在,更容易测试所有3种方法

import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import *
from PyQt4.QtSql import *

class Window(QWidget):
    def __init__(self, parent=None):
        QWidget.__init__(self, parent)
        self.model = QSqlTableModel(self)
        self.model.setTable("names")
        self.model.setHeaderData(0, Qt.Horizontal, "Id")
        self.model.setHeaderData(1, Qt.Horizontal, "Name")
        self.model.setEditStrategy(QSqlTableModel.OnManualSubmit)
        self.model.select()

        self.view = QTableView()
        self.view.setModel(self.model)
        self.view.setSelectionMode(QAbstractItemView.SingleSelection)
        self.view.setSelectionBehavior(QAbstractItemView.SelectRows)
        #self.view.setColumnHidden(0, True)
        self.view.resizeColumnsToContents()
        self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.view.horizontalHeader().setStretchLastSection(True)

        addButton = QPushButton("Add")
        editButton = QPushButton("Edit")
        deleteButton = QPushButton("Delete")
        exitButton = QPushButton("Exit")
        self.combo = QComboBox()
        self.combo.addItem("1) 1.Database, 2.Model (select)")
        self.combo.addItem("2) 1.Model, 2.Database")
        self.combo.addItem("3) 1.Database, 2.Model (insert)")
        self.combo.setCurrentIndex (0)
        self.checkbox = QCheckBox("Database Closed")

        hbox = QHBoxLayout()
        hbox.addWidget(addButton)
        hbox.addWidget(editButton)
        hbox.addWidget(deleteButton)
        hbox.addWidget(self.combo)
        hbox.addWidget(self.checkbox)
        hbox.addStretch()
        hbox.addWidget(exitButton)

        vbox = QVBoxLayout()
        vbox.addWidget(self.view)
        vbox.addLayout(hbox)
        self.setLayout(vbox)

        addButton.clicked.connect(self.addRecord)
        #editButton.clicked.connect(self.editRecord) # omitted for simplicity
        #deleteButton.clicked.connect(self.deleteRecord) # omitted for simplicity
        self.checkbox.clicked.connect(self.checkBoxCloseDatabase)
        exitButton.clicked.connect(self.close)

    def checkBoxCloseDatabase(self):
        if self.checkbox.isChecked():
            closeDatabase()
        else:
            pass
            #db.open() # it doesn't work

    def addRecord(self):
        # just QInputDialog for simplicity
        value, ok = QInputDialog.getText(self, 'Input Dialog', 'Enter the name:')
        if not ok:
            return

        # Now, what is the best way to insert the record?

        if self.combo.currentIndex() == 0:
            # 1st approach, first in database, then model.select()
            # it seems like the most natural way to me
            query = QSqlQuery()
            query.prepare("INSERT INTO names (name) VALUES(:name)")
            query.bindValue( ":name", value )
            if query.exec_():
                self.model.select() # now we know the record is inserted to db
                # the problem with this approach is that select() can be slow
                # somehow position the view to newly added record?!
            else:
                pass
                # message to user
                # if the record can't be inserted to database,
                # there's no way I will show that record in view
        elif self.combo.currentIndex() == 1:
            # 2nd approach, first in view (model cache), then in database
            QSqlDatabase.database().transaction()
            row = self.model.rowCount()
            self.model.insertRow(row)
            self.model.setData(self.model.index(row, 1), value)
            #self.model.submit()
            if self.model.submitAll():
                QSqlDatabase.database().commit()
                self.view.setCurrentIndex(self.model.index(row, 1))
            else:
                self.model.revertAll()
                QSqlDatabase.database().rollback()
                QMessageBox.warning(self, "Error", "Database not available. Please, try again later.")

        else:
            # 3rd approach, first in database, then model.insertRow()
            # it is not a complete solution and is not so practical
            query = QSqlQuery()
            query.prepare("INSERT INTO names (name) VALUES(:name)")
            query.bindValue( ":name", value )
            if query.exec_():
                #id = ... # somehow find id from the newly added record in db
                row = self.model.rowCount()
                self.model.insertRow(row)
                #self.model.setData(self.model.index(row, 0), id) # we don't know it
                self.model.setData(self.model.index(row, 1), value)
                # not a complete solution
            else:
                pass
                # do nothing, because model isn't changed
                # message to user

def closeDatabase():
    db.close()

def createFakeData():
    query = QSqlQuery()
    query.exec_("DROP TABLE names")
    query.exec_("CREATE TABLE names(id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, name TEXT)")
    query.exec_("INSERT INTO names VALUES(1, 'George')")
    query.exec_("INSERT INTO names VALUES(2, 'Rita')")
    query.exec_("INSERT INTO names VALUES(3, 'Jane')")
    query.exec_("INSERT INTO names VALUES(4, 'Steve')")
    query.exec_("INSERT INTO names VALUES(5, 'Maria')")
    query.exec_("INSERT INTO names VALUES(6, 'Bill')")
    #import random
    #for i in range(1000):
    #    name = chr(random.randint(65, 90))
    #    for j in range(random.randrange(3, 10)):
    #        name += chr(random.randint(97, 122))
    #
    #    query.prepare("INSERT INTO names (name) VALUES(:name)")
    #    query.bindValue( ":name", name )
    #    query.exec_()

app = QApplication(sys.argv)
db = QSqlDatabase.addDatabase("QSQLITE")
#db.setDatabaseName("test.db")
db.setDatabaseName(":memory:")
#openDatabase()
db.open()
createFakeData()
window = Window()
window.resize(800, 500)
window.show()
app.exec_()
EDIT2 2019年10月:我最终停止使用带有RDBMS的QSqlTableModel。我只使用QSqlQueryModel,不需要QSqlTableModel或QSqlRelationalTableModel。我不使用单元格编辑(如Excel),只通过表单编辑整个记录(行)。在表单上单击OK之后,我更新数据库并重新选择QSqlQueryModel。有趣的是,在重新选择之后,当前行再次聚焦。QSql(Relational)TableModel在处理数据库字段方面有太多问题,他们完全搞砸了,对于严肃的工作来说几乎是无用的。他们本可以让它比现在有用得多


我的建议:对于严肃的工作和业务应用程序,请使用QSqlQueryModel。

Robbie说得对,我可以使用表单编辑(使用QDataWidgetMapper)而不是直接编辑单元格,但我的问题不是表单或单元格编辑

我的问题是,我的例子中哪种方法更好,第一种还是第二种

我更改了代码并实现了第二种方法(我不喜欢)。这是一个好的实现吗

但问题仍然存在。(Py)Qt开发人员如何使用QtSql执行CRUD?先是数据库,然后是模型/视图,还是先是模型/视图,然后是数据库

编辑:我编辑了示例,添加了3。方法(不完整)和模拟数据库关闭的可能性。现在,更容易测试所有3种方法

import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import *
from PyQt4.QtSql import *

class Window(QWidget):
    def __init__(self, parent=None):
        QWidget.__init__(self, parent)
        self.model = QSqlTableModel(self)
        self.model.setTable("names")
        self.model.setHeaderData(0, Qt.Horizontal, "Id")
        self.model.setHeaderData(1, Qt.Horizontal, "Name")
        self.model.setEditStrategy(QSqlTableModel.OnManualSubmit)
        self.model.select()

        self.view = QTableView()
        self.view.setModel(self.model)
        self.view.setSelectionMode(QAbstractItemView.SingleSelection)
        self.view.setSelectionBehavior(QAbstractItemView.SelectRows)
        #self.view.setColumnHidden(0, True)
        self.view.resizeColumnsToContents()
        self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.view.horizontalHeader().setStretchLastSection(True)

        addButton = QPushButton("Add")
        editButton = QPushButton("Edit")
        deleteButton = QPushButton("Delete")
        exitButton = QPushButton("Exit")
        self.combo = QComboBox()
        self.combo.addItem("1) 1.Database, 2.Model (select)")
        self.combo.addItem("2) 1.Model, 2.Database")
        self.combo.addItem("3) 1.Database, 2.Model (insert)")
        self.combo.setCurrentIndex (0)
        self.checkbox = QCheckBox("Database Closed")

        hbox = QHBoxLayout()
        hbox.addWidget(addButton)
        hbox.addWidget(editButton)
        hbox.addWidget(deleteButton)
        hbox.addWidget(self.combo)
        hbox.addWidget(self.checkbox)
        hbox.addStretch()
        hbox.addWidget(exitButton)

        vbox = QVBoxLayout()
        vbox.addWidget(self.view)
        vbox.addLayout(hbox)
        self.setLayout(vbox)

        addButton.clicked.connect(self.addRecord)
        #editButton.clicked.connect(self.editRecord) # omitted for simplicity
        #deleteButton.clicked.connect(self.deleteRecord) # omitted for simplicity
        self.checkbox.clicked.connect(self.checkBoxCloseDatabase)
        exitButton.clicked.connect(self.close)

    def checkBoxCloseDatabase(self):
        if self.checkbox.isChecked():
            closeDatabase()
        else:
            pass
            #db.open() # it doesn't work

    def addRecord(self):
        # just QInputDialog for simplicity
        value, ok = QInputDialog.getText(self, 'Input Dialog', 'Enter the name:')
        if not ok:
            return

        # Now, what is the best way to insert the record?

        if self.combo.currentIndex() == 0:
            # 1st approach, first in database, then model.select()
            # it seems like the most natural way to me
            query = QSqlQuery()
            query.prepare("INSERT INTO names (name) VALUES(:name)")
            query.bindValue( ":name", value )
            if query.exec_():
                self.model.select() # now we know the record is inserted to db
                # the problem with this approach is that select() can be slow
                # somehow position the view to newly added record?!
            else:
                pass
                # message to user
                # if the record can't be inserted to database,
                # there's no way I will show that record in view
        elif self.combo.currentIndex() == 1:
            # 2nd approach, first in view (model cache), then in database
            QSqlDatabase.database().transaction()
            row = self.model.rowCount()
            self.model.insertRow(row)
            self.model.setData(self.model.index(row, 1), value)
            #self.model.submit()
            if self.model.submitAll():
                QSqlDatabase.database().commit()
                self.view.setCurrentIndex(self.model.index(row, 1))
            else:
                self.model.revertAll()
                QSqlDatabase.database().rollback()
                QMessageBox.warning(self, "Error", "Database not available. Please, try again later.")

        else:
            # 3rd approach, first in database, then model.insertRow()
            # it is not a complete solution and is not so practical
            query = QSqlQuery()
            query.prepare("INSERT INTO names (name) VALUES(:name)")
            query.bindValue( ":name", value )
            if query.exec_():
                #id = ... # somehow find id from the newly added record in db
                row = self.model.rowCount()
                self.model.insertRow(row)
                #self.model.setData(self.model.index(row, 0), id) # we don't know it
                self.model.setData(self.model.index(row, 1), value)
                # not a complete solution
            else:
                pass
                # do nothing, because model isn't changed
                # message to user

def closeDatabase():
    db.close()

def createFakeData():
    query = QSqlQuery()
    query.exec_("DROP TABLE names")
    query.exec_("CREATE TABLE names(id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, name TEXT)")
    query.exec_("INSERT INTO names VALUES(1, 'George')")
    query.exec_("INSERT INTO names VALUES(2, 'Rita')")
    query.exec_("INSERT INTO names VALUES(3, 'Jane')")
    query.exec_("INSERT INTO names VALUES(4, 'Steve')")
    query.exec_("INSERT INTO names VALUES(5, 'Maria')")
    query.exec_("INSERT INTO names VALUES(6, 'Bill')")
    #import random
    #for i in range(1000):
    #    name = chr(random.randint(65, 90))
    #    for j in range(random.randrange(3, 10)):
    #        name += chr(random.randint(97, 122))
    #
    #    query.prepare("INSERT INTO names (name) VALUES(:name)")
    #    query.bindValue( ":name", name )
    #    query.exec_()

app = QApplication(sys.argv)
db = QSqlDatabase.addDatabase("QSQLITE")
#db.setDatabaseName("test.db")
db.setDatabaseName(":memory:")
#openDatabase()
db.open()
createFakeData()
window = Window()
window.resize(800, 500)
window.show()
app.exec_()
EDIT2 2019年10月:我最终停止使用带有RDBMS的QSqlTableModel。我只使用QSqlQueryModel,不需要QSqlTableModel或QSqlRelationalTableModel。我不使用单元格编辑(如Excel),只通过表单编辑整个记录(行)。在表单上单击OK之后,我更新数据库并重新选择QSqlQueryModel。有趣的是,在重新选择之后,当前行再次聚焦。QSql(Relational)TableModel在处理数据库字段方面有太多问题,他们完全搞砸了,对于严肃的工作来说几乎是无用的。他们本可以让它比现在有用得多


我的建议:对于严肃的工作和业务应用程序,请使用QSqlQueryModel。

正如我在评论中所提到的,您的第一种方法比第二种方法更有利,因为它可以防止您做不必要的工作。但是,如果您担心
QSqlTableModel将传输的数据量,请选择
(可能会使应用程序变慢的内容),您可以改用
QSqlTableModel.insertRecord
(有关详细信息,请参见下面的示例)。这将尝试在数据库中插入记录,同时它将在模型中注册,即使插入失败。因此,您必须通过
QSqlTableModel.revertAll()
再次手动删除它(如果失败)

但是,您可以利用这一事实,即它是独立添加到模型中的,以承担从用户处重新添加数据(插入失败)的责任。这意味着数据被插入到模型中,您稍后尝试将其提交到数据库(用户不必重新输入)。这里有一个小例子(仅关键部分):

(我使用了一个两列表,其中列INT NOT NULL AUTO_INCREMENT和VARCHAR(32))

这是处理未决记录提交的类:

class QueueRecord(QtCore.QObject):
    def __init__(self, model, table, row, parent=None):
        QtCore.QObject.__init__(self, parent)

        self.model = model
        self.table = table
        self.delegate = table.itemDelegateForRow(row)  # get the item delegate for the pending row (to reset it later)
        self.row = row

        table.setItemDelegateForRow(row, PendingItemDelegate())  # set the item delegate of the pending row to new one (see below). In this case all cells will just display 'pending ...' so the user knows that this record isn't submitted yet.

        self.t1 = QtCore.QThread()  # we need a new thread so we won't block our main application
        self.moveToThread(self.t1)
        self.t1.start()

    def insert(self):
        while not self.model.submitAll():  # try to submit the record ...
            time.sleep(2)  # ... if it fails retry after 2 seconds.

        # record successfully submitted
        self.table.setItemDelegateForRow(self.row, self.delegate)  # reset the delegate
        self.t1.quit()  # exit the thread
这是代表:

class PendingItemDelegate(QtGui.QStyledItemDelegate):
    def __init__(self, parent=None):
        QtGui.QStyledItemDelegate.__init__(self, parent)

    def displayText(self, value, locale):
        return 'pending ...'  # return 'pending ...' for every cell
因此,这段代码的基本功能是使用
insertRecord
在模型/数据库中插入新数据。如果失败,记录将被添加到模型中,我们将创建一个新类(在单独的线程中运行)来处理数据的重新提交。这个新类将更改挂起行的显示,以指示用户此记录尚未注册,并尝试提交数据,直到成功。代理被重置,线程离开

这样可以避免调用
select()
,而只需在表中插入一条记录。此外,用户不再负责提供数据a