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

时间:2014-08-29 20:10:57

标签: qt pyqt pyqt4

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

我主要担心的是我不想在视图中显示幻像记录,我希望用户知道记录未输入数据库。

我放了一些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_()

3 个答案:

答案 0 :(得分:2)

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

答案 1 :(得分:2)

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_()

答案 2 :(得分:1)

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

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

(我使用了两列表,列为INT NOT NULL AUTO_INCREMENT和VARCHAR(32))

record = QtSql.QSqlRecord()  # create a new record
record.append(QtSql.QSqlField('id', QtCore.QVariant.Int))  # add the columns
record.append(QtSql.QSqlField('value', QtCore.QVariant.String))
record.setValue('id', 0)  # set the values for every column
record.setValue('value', value)

if not self.model.insertRecord(-1, record):  # insert the record (-1 means at the end)
    self.queue = QueueRecord(self.model, self.table, self.model.rowCount()-1)  # in case of failure a new class is invoked which will manage the submission of this record (see below)
    # However, the record was added to the model and will therefore appear in the table
    self.connect(self, QtCore.SIGNAL('qstart()'), self.queue.insert)  # queue.insert is the method which handles submitting the record, the signal qstart() was created beforehand using QtCore.pyqtSignal()
    self.qstart.emit()  # start the submission handler

这是处理待处理记录提交的类:

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()但只是在表格中插入一条记录。此外,用户不再负责再次提供数据,但这将由一个单独的类处理。

然而这是一个非常简单的示例,应该谨慎对待!例如,类QueueRecord使用通过model.rowCount()-1提供的行号来引用挂起的元素,但如果在此期间删除元素,则行数将更改,您将引用错误的元素。

此示例仅用于说明目的,可用于进一步开发,但在当前状态下,它并不适用于实际应用。

例如,您可以更改最大值。重新提交的次数以及“待处理...”之后的次数将变为“失败”,并且会出现重新提交的按钮,因此用户只需按此按钮即可在连接后重新添加数据数据库再次建立。

顺便说一下,为了测试这些东西,我在主窗口添加了一个关闭/打开数据库的按钮,所以我启动了应用程序(自动打开数据库)然后关闭它,插入一个值并再次打开它。 / p>