SqlAlchemy:插入新行并修改另一行:"Can't attach instance <ObjectT>; another instance with key is already present in this session"

SqlAlchemy: Insert New Row and Modify Another: "Can't attach instance <ObjectT>; another instance with key is already present in this session"

我必须在提交时同时插入一行并修改另一行(设置其 ACTIVE=N)。添加的新行是旧行的副本,具有新 ID。我使用 Python 的 copy.deepcopy 来保存旧对象,然后我擦除当前对象的 id 以使其符合插入新序列值的条件。这两个操作都应该在交易中发生。

# First fetch existing agreement. Initially "postApproveOldAgreement" is None.
postApproveOldAgreement = None
agreement = db_session.query(AgreementT).filter(AgreementT.id == form['someId']).first()
...

if someCondition:
   postApproveOldAgreement = copy.deepcopy(agreement)
   postApproveOldAgreement.active = 'N' # Modify old agreement
   agreement.id = None # Make this one eligible for new insertion

# Continue filling out the "agreement" object
...

# Insert/Modify and Commit 
db_session.add(agreement)
if postApproveOldAgreement != None:
    db_session.add(postApproveOldAgreement)
db_session.commit()    

错误:

sqlalchemy.exc.InvalidRequestError: 
Can't attach instance <AgreementT at 0x188e8abedc0>; 
another instance with key (<class 'ets.db.models.AgreementT'>, (20812,), None) is already present in this session.

看起来 2 个实例引用了同一个密钥(我认为是因为您使用的是 deepcopy)。您可以使用 Agreement.

的新实例来完成此技巧
agreement = db.session.query(Agreement).filter(...).first()
# if some_condition... let's say condition works
agreement_data = deepcopy(agreement.__dict__)

# we don't need id + _sa_instance_state db instance
del agreement_data['id']
del agreement_data['_sa_instance_state']

agreement2 = Agreement(**agreement_data)
agreement2.active = 'N'
session.add(agreement2)
# let's try to change an agreement field
agreement.active = 'new val'
session.commit()

日志:

2021-07-30 17:18:22,658 INFO sqlalchemy.engine.Engine UPDATE agreement SET active=%(active)s WHERE agreement.id = %(agreement_id)s
2021-07-30 17:18:22,658 INFO sqlalchemy.engine.Engine [generated in 0.00012s] {'active': 'new val', 'agreement_id': 1}
2021-07-30 17:18:22,659 INFO sqlalchemy.engine.Engine INSERT INTO agreement (....) VALUES (....) RETURNING agreement.id
....
2021-07-30 17:18:22,660 INFO sqlalchemy.engine.Engine COMMIT

我使用此处概述的以下方法实现了它,https://www.atemon.com/blog/clone-a-sqlalchemy-db-object-with-new-primary-key/

    # Initially
    agreement = db_session.query(AgreementT).filter(AgreementT.id == form['agreementIdManual']).first()
    postApproveOldAgreement = None

    if (someCondition):

        # Clone the current Agreement and erase its ID to make it Insert-eligible: 
        # see https://www.atemon.com/blog/clone-a-sqlalchemy-db-object-with-new-primary-key/
        # Use expunge/make_transient
        db_session.expunge(agreement)
        make_transient(agreement)
        agreement.id = None

        # Modify new cloned agreement as necessary...
        agreement.active = 'Y' # etc.
        
        # De-activate the previous Agreement by **re-fetching** it and 
        # modifying it as necessary 
        postApproveOldAgreement = db_session.query(AgreementT).filter(AgreementT.id == form['agreementIdManual']).first()
        postApproveOldAgreement.active = 'N'

   # Insert and commit
   db_session.add(agreement)
   # Special Case: If postApproveOldAgreement is available, commit it as well 
   if postApproveOldAgreement != None:
      db_session.add(postApproveOldAgreement)
   db_session.commit()