首页 > 解决方案 > 处理过程中保存在 SQLAlchemy 会话中的对象

问题描述

我们的 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)

标签: pythonflasksqlalchemyflask-sqlalchemypyodbc

解决方案


我的问题是,是否有一个自动过程可以将(更改的)对象添加到会话中,而不需要我们使用session.add()

至少有一个功能可以将对象拉到 aSession而不显式添加它们:save-update级联。当一个对象被添加到一个通过配置了这个级联的属性Session与它关联的所有对象时,也会被放置在其中。当一个对象与另一个已经在.relationship()SessionSession

SQLAlchemy 中是否有一个选项可以禁用此行为,并且仅在使用明确完成会话时才提交到会话session.add(object)

您当然可以将relationship()属性配置为不包含此行为,但似乎没有一个全局开关可以完全禁用级联。

如果您的代码是这种情况,那么对象被添加两次的原因是您已经明确地这样做了。批量操作省略了大多数支持原始性能的更高级功能-Session例如,它们不与Session对象是否已经持久化相协调,也不会将持久化对象附加到Session

给定的对象与 target 没有定义的关系Session,即使操作完成,这意味着在附加它们或根据身份映射或会话管理它们的状态时没有开销。

至于为什么首先出现问题,您不应该需要手动为对象保留一个“暂存区”——您的复合对象。Session与正确使用事务相结合,这正是它的用途。函数和子函数应该在Session有意义的时候添加对象,但它们不应该控制正在进行的事务。这应该只发生在您的主要功能之外,您现在正在处理您的复合对象。如果您回滚,所有更改都会消失。

在测试中,无论被测代码做什么,您都可以传递一个Session已加入外部事务的外部事务,该事务将被显式回滚。


推荐阅读