代码之家  ›  专栏  ›  技术社区  ›  musicamante

QSqlTableModel的不一致行为

  •  0
  • musicamante  · 技术社区  · 3 年前

    前提:这个问题 可能地 指的是两个截然不同的问题,但我相信 可以 联系起来。如果在评论和进一步研究之后,我们会发现它们实际上是不相关的,我将提出一个单独的问题。

    我在QSqlTableModel的某些方面遇到了一些意想不到的奇怪行为,至少在一种情况下,在子类化方面。我不是Sql方面的专家,但其中一个问题似乎不是预期的行为应该是什么。

    我只能为SQLite确认这一点,因为我不使用其他数据库系统。
    我还可以用[Py]Qt 5.15.2和6.2.2重现这些问题。

    1.忽略编辑器更改后,新行被“删除”

    默认情况下 OnRowChange 编辑策略,如果添加一行,则在字段中插入一些数据,并编辑 另一个 使用取消同一行上的字段 电子稳定控制系统 这个 整体 然后从视图中删除行。

    不过,实际的数据库仍在更新,再次打开程序会显示以前“隐藏”的行,但已取消的字段除外。

    from PyQt5 import QtWidgets, QtSql
    
    class TestModel(QtSql.QSqlTableModel):
        def __init__(self):
            super().__init__()
            QtSql.QSqlQuery().exec(
                'CREATE TABLE IF NOT EXISTS test (name, value, data);')
            self.setTable('test')
            self.select()
    
    
    app = QtWidgets.QApplication([])
    
    db = QtSql.QSqlDatabase.addDatabase('QSQLITE')
    db.setDatabaseName('test.db')
    db.open()
    
    win = QtWidgets.QWidget()
    layout = QtWidgets.QVBoxLayout(win)
    addButton = QtWidgets.QPushButton('Add row')
    layout.addWidget(addButton)
    table = QtWidgets.QTableView()
    layout.addWidget(table)
    model = TestModel()
    table.setModel(model)
    
    addButton.clicked.connect(lambda: model.insertRow(model.rowCount()))
    app.aboutToQuit.connect(model.submitAll)
    
    win.resize(640, 480)
    win.show()
    app.exec()
    

    以下是重现问题的步骤:

    1. 用按钮添加一行;
    2. 至少编辑一个字段,但 所有领域;
    3. 开始编辑一个空字段;
    4. 电子稳定控制系统 ;
    5. 关闭并重新启动程序;

    在第4步之后,您将看到添加的行从视图中删除,这并非完全出乎意料:因为策略是 不断变化 ,取消将还原所有缓存的更改(包括 insertRow() ); 我并不完全同意这种行为(想象一下填充几十个字段,然后错误地点击Esc),但这不是重点。
    出乎意料的是,模型 实际上是更新的 使用新行和点击前已提交的所有字段 电子稳定控制系统 ,重新启动程序将显示这一点。

    2.实施 data() 对于不完整的记录,恢复到以前的数据

    编辑包含空索引的索引( NULL )其行的字段会产生不同的结果 数据() 已在子类中实现或未实现, 即使覆盖只调用基本实现 .

    将以下内容添加到 TestModel 以上班级:

        def data(self, index, role=QtCore.Qt.DisplayRole):
            return super().data(index, role)
    

    还有一个提交按钮 app.exec() :

    submitButton = QtWidgets.QPushButton('Submit')
    layout.addWidget(submitButton)
    submitButton.clicked.connect(model.submitAll)
    

    要重现问题,请执行以下步骤:

    1. 打开一个数据库,至少有一行底部有一个空字段,类似于上面所做的(注意:对于“空字段”,我指的是一个 从不 (已编辑);
    2. 编辑该行中的任何字段,然后按 进来 ;

    不断变化 OnFieldChange 策略,结果是整行无效:垂直标题显示“!”(对无效记录的提示)以及 全部的 字段被清除,包括那些具有数据库中以前的值的字段。

    当编辑策略设置为 OnManualSubmit 使命感 submitAll() 将恢复为数据库的原始值,就像恢复更改一样。

    如果包含空字段的行为空,则行为略有不同 在底部;执行上面的前两个步骤,然后:

    1. 按提交按钮;
    2. 关闭并重新启动程序;

    在这种情况下,在第3步之后,视图似乎已经接受了更改,但重新启动程序表明没有应用任何修改。


    根据编辑策略和情况,行为会发生变化。通常,如果一个字段为空的记录后面至少有一个字段已设置为所有的记录,则在取消编辑该字段时,视图和模型的行为与预期一致。
    至少在一种情况下,甚至根本不可能编辑一个空字段(我必须承认,我做了很多随机/速度测试,当我发现我无法编辑一个字段时,我记不起复制它的步骤)。

    更奇怪的是,两者 setData() 亚个子() 回来 True ,而且没有明确的 lastError() 。尽管如此,显示(和存储)的数据仍会恢复为以前的数据库内容。

    我相信这两个问题都可能是由一个常见的错误引起的,但是,在向Qt错误报告系统提交一些东西之前,我希望得到一些反馈,特别是来自对SQL和其他db驱动程序有更丰富经验的人的反馈,以便提供更好的报告(并最终知道这些问题实际上是否相关)。

    0 回复  |  直到 3 年前
        1
  •  2
  •   ekhumoro    3 年前

    这两个问题都是由Qt中的错误引起的,但它们并不相关。

    在解释这些问题之前,对垂直标题中使用的符号进行一些澄清可能会有所帮助,因为它们提供了一些有关问题来源的重要线索。这些符号是 documented thus :

    如果使用 QSqlTableModel::insertRows(),新行将用 星号(*),直到使用submitAll()或 当用户移动到另一条记录时自动执行(假设编辑 策略是QSqlTableModel::OnRowChange)。同样,如果删除行 使用removeRows(),这些行将被标记为感叹号 (!) 直到变更提交。

    第一个问题是由这一系列事件引起的:

    按压后 电子稳定控制系统 编辑新行时(即。 * 在垂直标题中显示),代理将发出 closeEditor RevertModelCache 暗示这就叫 特写编辑 视图的插槽,它依次调用 revert() 桌面模式——最终也是私人模式 revertCachedRow 作用此函数调用 beginRemoveRows -但关键是 之前 清除缓存。下一个 rowsAboutToBeRemoved 将从视图中删除该行,从而导致 currentRowChanged 被发射,这反过来调用 submit() 桌子模型的槽。哎呀!现在,仍然未清除的缓存数据无意中被提交到了数据库中 endRemoveRows 在缓存数据最终删除后调用。所以,简而言之,这里的问题是没有守卫可以阻止 提交 在执行死刑期间被传唤 回复() .

    第二个问题要微妙得多。出现此问题的原因是,创建SQL表时没有主键,并且列没有显式类型。这一切都是完全正确的,但它在构建SQL语句的Qt代码的一小部分中暴露了一个关键缺陷。

    这种情况发生在 QSqlTableModel::selectRow ,它需要从 QSqlRecord 归还人 primaryValues 这个 sqlStatement 数据库驱动程序的函数用于此目的,但这需要知道字段值的确切类型,以便正确引用它们。但是,表模型缓存不能确保对没有显式类型的列使用合理的默认类型。这意味着非类型化的值将通过无引号传递,允许在编辑表时计算任意SQL表达式。哎呀!

    正是这一点有时会使bug难以重现,因为确切的行为取决于输入的精确值。像这样的价值观 foo 将导致SQL错误,因为它是不存在的有效列名;然而,像这样的价值观 6 不会引发错误,但是 由于类型不匹配(即。 INT vs TEXT ).如果 selectRow 找不到相关行,它可能会调用 cache.refresh() ,这将清除值并将行标记为删除(因此 ! 如垂直标题所示)。还要注意 QSqlQuery 用于执行有问题的语句,因此任何错误都将以静默方式传递,并且无法通过数据库或驱动程序获得。

    我在下面重新编写了原始示例,并提供了一些可以通过命令行打开的修复程序( 1 要解决第一个问题, 2 解决第二个问题 3 解决这两个问题)。这些主要用于调试,但如果需要,也可以调整为变通方法。第二种修复方法相当粗糙(因为 primaryValues 不能在PyQt中重新实现),但只有在无法控制数据库模式时才需要它。如果表有一个类型化主键和/或所有列都有一个显式类型,那么第二个问题根本不会出现。希望脚本的输出能够清楚地说明发生了什么。

    PyQt5 :

    import sys
    from PyQt5 import QtCore, QtWidgets, QtSql
    
    BUGFIX = int(sys.argv[1]) if len(sys.argv) > 1 else 0
    
    class TestModel(QtSql.QSqlTableModel):
        def __init__(self):
            super().__init__()
            self._select_row = None
            self._reverting = False
            QtSql.QSqlQuery().exec(
                'CREATE TABLE IF NOT EXISTS test (name, value, data);')
            self.setTable('test')
            self.select()
    
        def selectRow(self, row):
            if BUGFIX & 2:
                self._select_row = row
            result = super().selectRow(row)
            print(f'selectRow: {result}')
            return result
    
        def select(self):
            return super().select() if self._select_row is None else False
    
        def selectStatement(self):
            if self._select_row is not None:
                record = self.primaryValues(self._select_row)
                for index in range(record.count()):
                    field = record.field(index)
                    if (not field.isNull() and
                        field.type() == QtCore.QVariant.Invalid):
                        field.setType(QtCore.QVariant.String)
                        record.replace(index, field)
                where = self.database().driver().sqlStatement(
                    QtSql.QSqlDriver.WhereStatement,
                    self.tableName(), record, False)
                if where[:6].upper() == 'WHERE ':
                    where = where[6:]
                self.setFilter(where)
                self._select_row = None
            statement = super().selectStatement()
            print(f'selectStatement: {statement!r}')
            query = self.database().exec(statement)
            if query.lastError().isValid():
                print(f'  query-lastError: {query.lastError().text()!r}')
            else:
                print(f'  query-next: {query.next()}')
            return statement
    
        def revert(self):
            if BUGFIX & 1:
                self._reverting = True
            print('reverting ...')
            super().revert()
            self._reverting = False
            print('reverted')
    
        def submit(self):
            print('submitting ...')
            result = False if self._reverting else super().submit()
            print(f'submitted: {result}')
            return result
    
    app = QtWidgets.QApplication(['Test'])
    
    db = QtSql.QSqlDatabase.addDatabase('QSQLITE')
    db.setDatabaseName('test.db')
    db.open()
    
    win = QtWidgets.QWidget()
    layout = QtWidgets.QVBoxLayout(win)
    addButton = QtWidgets.QPushButton('Add row')
    layout.addWidget(addButton)
    table = QtWidgets.QTableView()
    layout.addWidget(table)
    model = TestModel()
    table.setModel(model)
    submitButton = QtWidgets.QPushButton('Submit')
    layout.addWidget(submitButton)
    submitButton.clicked.connect(model.submitAll)
    addButton.clicked.connect(lambda: model.insertRow(model.rowCount()))
    app.aboutToQuit.connect(model.submitAll)
    
    win.setGeometry(1000, 50, 640, 480)
    win.show()
    app.exec()
    

    PyQt6 :

    import sys
    from PyQt6 import QtCore, QtWidgets, QtSql
    
    BUGFIX = int(sys.argv[1]) if len(sys.argv) > 1 else 0
    
    class TestModel(QtSql.QSqlTableModel):
        def __init__(self):
            super().__init__()
            self._select_row = None
            self._reverting = False
            QtSql.QSqlQuery().exec(
                'CREATE TABLE IF NOT EXISTS test (name, value, data);')
            self.setTable('test')
            self.select()
    
        def selectRow(self, row):
            if BUGFIX & 2:
                self._select_row = row
            result = super().selectRow(row)
            print(f'selectRow: {result}')
            return result
    
        def select(self):
            return super().select() if self._select_row is None else False
    
        def selectStatement(self):
            if self._select_row is not None:
                record = self.primaryValues(self._select_row)
                MetaType = QtCore.QMetaType.Type
                MetaString = QtCore.QMetaType(MetaType.QString.value)
                for index in range(record.count()):
                    field = record.field(index)
                    if (not field.isNull() and
                        field.metaType().id() == MetaType.UnknownType.value):
                        field.setMetaType(MetaString)
                        record.replace(index, field)
                where = self.database().driver().sqlStatement(
                    QtSql.QSqlDriver.StatementType.WhereStatement,
                    self.tableName(), record, False)
                if where[:6].upper() == 'WHERE ':
                    where = where[6:]
                self.setFilter(where)
                self._select_row = None
            statement = super().selectStatement()
            print(f'selectStatement: {statement!r}')
            query = self.database().exec(statement)
            if query.lastError().isValid():
                print(f'  query-lastError: {query.lastError().text()!r}')
            else:
                print(f'  query-next: {query.next()}')
            return statement
    
        def revert(self):
            if BUGFIX & 1:
                self._reverting = True
            print('reverting ...')
            super().revert()
            self._reverting = False
            print('reverted')
    
        def submit(self):
            print('submitting ...')
            result = False if self._reverting else super().submit()
            print(f'submitted: {result}')
            return result
    
    app = QtWidgets.QApplication(['Test'])
    
    db = QtSql.QSqlDatabase.addDatabase('QSQLITE')
    db.setDatabaseName('test.db')
    db.open()
    
    win = QtWidgets.QWidget()
    layout = QtWidgets.QVBoxLayout(win)
    addButton = QtWidgets.QPushButton('Add row')
    layout.addWidget(addButton)
    table = QtWidgets.QTableView()
    layout.addWidget(table)
    model = TestModel()
    table.setModel(model)
    submitButton = QtWidgets.QPushButton('Submit')
    layout.addWidget(submitButton)
    submitButton.clicked.connect(model.submitAll)
    addButton.clicked.connect(lambda: model.insertRow(model.rowCount()))
    app.aboutToQuit.connect(model.submitAll)
    
    win.setGeometry(1000, 50, 640, 480)
    win.show()
    app.exec()