在处理期间保存在SQLAlchemy会话中的对象

时间:2018-10-29 13:34:57

标签: python flask sqlalchemy flask-sqlalchemy pyodbc

在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)明确完成会话时才提交会话?

1 个答案:

答案 0 :(得分:0)

  

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

至少有一项功能可以将对象拉到Session而不显式添加对象:save-update cascade。将对象添加到Session时,所有通过relationship()属性配置的级联与其相关联的对象也都放置在Session中。当某个对象与Session中已经存在的另一个对象相关联时,也会发生同样的情况。

  

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

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

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

  

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

首先要解决问题的原因,您无需手动在对象周围保留一个“临时区域”(即复合对象)。正是Session的目的,结合正确使用事务。函数和子函数应在合理的情况下向Session添加对象,但不应控制正在进行的交易。那只应该在您正在处理复合对象的main函数外部发生。如果回滚,所有更改都会消失。

在测试中,无论被测代码做什么,您都可以绕过a Session that has joined an external transaction,它将被显式回滚。