这两个问题都是由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()