【问题标题】:Objects saved in the SQLAlchemy session during process处理过程中保存在 SQLAlchemy 会话中的对象
【发布时间】: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


【解决方案1】:

我的问题是,是否有一个自动过程可以在不使用session.add() 的情况下向会话中添加(更改)对象?

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

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

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

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

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

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

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

【讨论】:

  • 似乎这种行为是设计使然,但在特定情况下很麻烦,例如当我们想使用相同的逻辑来处理和显示结果时。显示时,我们不想实际将对象保存到数据库,但底层代码仍可能无意中将整个会话保存到数据库(例如,发送到数据库的日志消息可能会提交会话)。我们将尝试通过创建一个独立保存的辅助连接来解决这个问题。我们用它来记录日志和我们想要保存的其他事情。这个答案是正确的,所以它被接受了。
猜你喜欢
  • 2013-08-26
  • 1970-01-01
  • 2018-03-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-03-26
相关资源
最近更新 更多