【发布时间】:2018-10-29 13:34:56
【问题描述】:
我们的 Flask Web 应用程序中有一些功能,它们由单个函数调用组成,该调用调用许多子函数并在幕后做很多事情。例如,它将(金融)事务添加到(MSSQL)数据库,在数据库的日志表中写入内容并更改特定对象的属性,从而导致我们数据库中特定表中的列发生更改。所有这些都是通过对象使用 SQLAlchemy 完成的。
在一种新方法中,由于可测试性,并且因为我们有时只想显示这些更改而不将它们实际提交到数据库,我们让这些函数返回一个复合 Python 对象,其中包含所有改变的对象。 因此,我们不是在函数和子函数内部提交数据库更改,而是让它们返回更改的对象,因此我们可以决定在主函数外部显示或保存它们。
所以主函数返回一个复合对象,其中包含所有这些更改的对象,并且在主函数之外,我们将这些更改的对象添加到我们的 SQLAlchemy 会话并将会话提交到数据库。 (或者,如果我们只需要显示信息,我们不添加和提交)。我们这样做的方式是复合结果对象有一个 save_to_session() 函数,它使用 SQLAlchemy 的 bulk_save_objects() 操作保存我们更改的对象:
if result:
result.save_to_session(current_app.db_session)
current_app.db_session.commit()
def save_to_session(self, session):
session.bulk_save_objects(self.adminlog)
...
这种新方法在current_app.db_session.commit() 行中导致了我们没有预料到的错误。似乎在流程结束时,当我们将返回的对象添加到会话并尝试将会话提交到数据库时,发生了关于 重复键 的错误。
看起来在这个过程中,返回的对象已经添加到会话中的某个地方,SQLAlchemy 尝试添加它们两次。
我们得出了这个结论,因为当我们注释掉bulk_save_objects()调用时,就不再有错误信息了。但是,更改的数据被正确地提交到数据库中,并且准确地提交一次。
当我们在发生此错误后检查数据库时,没有记录与错误消息中提到的主键。这是因为发生错误时发生的回滚。所以也不是该记录已经存在于数据库中,而是更像是会话尝试添加相同的记录两次。
这是我们得到的错误,使用 pymssql 作为驱动:
sqlalchemy.exc.IntegrityError: (pymssql.IntegrityError) (2627,
b"Violation of PRIMARY KEY constraint 'PK_adminlog_id'.
Cannot insert duplicate key in object 'dbo.adminlog'.
The duplicate key value is (0E5537FF-E45C-40C5-98FC-7B1ACAD8104E).
DB-Lib error message 20018, severity 14:\n
General SQL Server error: Check messages from the SQL Server\n
")
[SQL:
'INSERT INTO adminlog (
alog_id,
alog_ppl_id,
alog_user_ppl_id,
alog_user_name,
alog_datetime,
[alog_ipAddress],
[alog_macAddress],
alog_comment,
alog_type,
alog_act_id,
alog_comp_id,
alog_artc_id)
VALUES (
%(alog_id)s,
%(alog_ppl_id)s,
%(alog_user_ppl_id)s,
%(alog_user_name)s,
%(alog_datetime)s,
%(alog_ipAddress)s,
%(alog_macAddress)s,
%(alog_comment)s,
%(alog_type)s,
%(alog_act_id)s,
%(alog_comp_id)s,
%(alog_artc_id)s)']
[parameters: (
{'alog_act_id': None,
'alog_comment': 'Le service a été ajouté. Cours Coll (119,88)',
'alog_datetime': datetime.datetime(2018, 10, 29, 13, 46, 54, 837178),
'alog_macAddress': b'4A-NO-NY-MO-US',
'alog_type': b'user',
'alog_artc_id': None,
'alog_comp_id': None,
'alog_id': b'0E5537FF-E45C-40C5-98FC-7B1ACAD8104E',
'alog_user_ppl_id': b'99999999-9999-9999-1111-999999999999',
'alog_user_name': 'System',
'alog_ipAddress': b'0.0.0.0',
'alog_ppl_id': b'AE841D1C-5D8D-47F7-B81F-89C5C931BD14'},
{'alog_act_id': None,
'alog_comment': 'Le service a été supprimé.
01/12/2019 Cours Coll (119,88)',
'alog_datetime': datetime.datetime(2018, 10, 29, 13, 46, 55, 71600),
'alog_macAddress': b'4A-NO-NY-MO-US',
'alog_type': b'user',
'alog_artc_id': None,
'alog_comp_id': None,
'alog_id': b'E22176FB-7490-470F-A8BA-A35D5F55A96A',
'alog_user_ppl_id': b'99999999-9999-9999-1111-999999999999',
'alog_user_name': 'System',
'alog_ipAddress': b'0.0.0.0',
'alog_ppl_id': b'AE841D1C-5D8D-47F7-B81F-89C5C931BD14'}
)]
我们使用 PyODBC 得到了类似的错误:
sqlalchemy.exc.IntegrityError: (pyodbc.IntegrityError) ('23000',
"[23000] [Microsoft][SQL Server Native Client 11.0][SQL Server]Violation of PRIMARY KEY constraint 'PK_adminlog_id'.
Cannot insert duplicate key in object 'dbo.adminlog'.
The duplicate key value is (F5CABD8F-E000-4677-8F5F-78B4CD3B9560). (2627) (SQLExecDirectW);
[23000] [Microsoft][SQL Server Native Client 11.0][SQL Server]The statement has been terminated. (3621)")
[SQL: 'INSERT INTO adminlog (
alog_id,
alog_ppl_id,
alog_user_ppl_id,
alog_user_name,
alog_datetime,
[alog_ipAddress],
[alog_macAddress],
alog_comment,
alog_type,
alog_act_id,
alog_comp_id,
alog_artc_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)']
[parameters: ((
b'F5CABD8F-E000-4677-8F5F-78B4CD3B9560',
b'0D10D3EF-F37E-45BE-8EED-B5987AE80732',
b'99999999-9999-9999-1111-999999999999',
'System',
datetime.datetime(2018, 10, 29, 13, 51, 30, 555495),
b'0.0.0.0',
b'4A-NO-NY-MO-US',
'Le service a été ajouté. Cours Coll (119,88)',
b'user',
None,
None,
None),
(
b'39395ACA-0AFB-4C5F-90D4-0C6F95D7B8BC',
b'0D10D3EF-F37E-45BE-8EED-B5987AE80732',
b'99999999-9999-9999-1111-999999999999',
'System',
datetime.datetime(2018, 10, 29, 13, 51, 30, 777909),
b'0.0.0.0',
b'4A-NO-NY-MO-US',
'Le service a été supprimé. 01/12/2019 Cours Coll (119,88)',
b'user',
None,
None,
None)
)]
我的问题是,是否有一个自动过程可以在不使用session.add() 的情况下向会话中添加(更改)对象?
SQLAlchemy 中是否有一个选项可以禁用此行为,并且仅在使用 session.add(object) 明确完成时才提交到会话?
【问题讨论】:
-
至少有一个自动“过程”:SQLAlchemy 的 ORM 关系级联。如果您将瞬态对象分配/附加到具有默认配置的关系属性,它将将该对象添加到会话中。
-
@IljaEverilä 感谢您的评论。在较早的问题中,您声明与现有对象相关的新对象实例不会添加到会话中(此问题:stackoverflow.com/questions/40745175/…)。我现在很困惑……
-
链接的问题不同,您误读了一点:其中 外键 被分配了一个值,并且预计这也会填充 ORM 关系属性,默认情况下没有。在那个问题中,新实例一开始确实没有添加到会话中,并且在手动添加之前(或通过级联...隐式添加)之前它无法完成任何加载。
-
请阅读save-update cascade 以消除任何混淆。
-
或者您通过 ORM 关系将瞬态对象与已经在会话中的对象(例如来自查询的实例)关联:“保存更新级联也会影响对象的属性操作已经存在于会话中。”我并不是说您的情况正在发生这种情况,但这是值得寻找的。span>
标签: python flask sqlalchemy flask-sqlalchemy pyodbc