【问题标题】:Why does SQLAlchemy not update relations after object deletion?为什么 SQLAlchemy 在对象删除后不更新关系?
【发布时间】:2018-07-20 00:56:48
【问题描述】:

问题

在同一会话中,删除一个对象后,会话中包含或指向该已删除对象的其他对象的关系属性不会更新。简而言之:

sesion.add(a, b)
a.parent = b
print(a.parent)  # b
session.delete(b)
print(a.parent)  # b

完整的、可重现的示例如下。

材质

我已经详细阅读了 SQLA 文档,包括:

  • 'deleting from collections' 上的误导部分。误导,因为它使您认为删除后缺少更新仅限于集合。下面的例子表明它不是。
  • cascades 上的部分,尤其是“删除孤儿”。这不是我正在寻找的,因为我不希望所有删除关系来删除相关对象。

可能的解决方案

可以使用session.expire() 手动将单个对象甚至单个对象属性设置为重新加载。我认为 ORM 负责完成这项工作:-)

问题

  • 我错过了一个简单的解决方案吗?
  • 我是否错过了文档中明确编写和解释的地方? (我花了很长时间缩小问题范围)
  • SQLAlchemy 不/不能标记更新关联到已删除对象的关系是否有原因?

代码示例

SQLA 设置

from sqlalchemy import create_engine, Table, Column, Integer, String, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship, backref, sessionmaker

engine = create_engine('sqlite:///:memory:', echo=True)
Base = declarative_base()
Session = sessionmaker(bind=engine)
session = Session()

模型定义

class Country(Base):
    __tablename__ = 'countries'

    id = Column(Integer(), primary_key=True)
    name = Column(String(255))

    def __repr__(self):
        return '<Country {} [{}]>'.format(self.name, self.id)


class Capital(Base):
    __tablename__ = 'capitals'

    id = Column(Integer(), primary_key=True)
    name = Column(String(255))
    country_id = Column(Integer(), ForeignKey(Country.id), unique=True)
    country = relationship('Country', backref=backref('capital', uselist=False))

    def __repr__(self):
        return '<Capital {} [{}]>'.format(self.name, self.id)


Base.metadata.create_all(engine)

测试

# Creating both objects
us = Country(name="USA")
ny = Capital(name="NYC")
session.add(us)
session.add(ny)
session.commit()

print("\n### 1. Creating the relation:")
# Loading relations:
print("## us.capital: ", us.capital)
print("## ny.country: ", ny.country)
# Creating the relation:
us.capital = ny
# Checking that it's upated on the other side:
print("## ny.country (after rel creation): ", ny.country)
# Saving
session.commit()


print("\n### 2. Deleting relation:")
# Loading relations:
print("## us.capital: ", us.capital)
print("## ny.country: ", ny.country)
# Deleting one object of the relation:
us.capital = None
# The relation from the other side are updated accordingly
print("## ny.country (after rel deletion): ", ny.country)
# Rolling back
session.rollback()


print("\n### 3. Deleting one object:")
# Loading relations:
print("## us.capital: ", us.capital)
print("## ny.country: ", ny.country)
# Deleting one object of the relation:
session.delete(us)
# The relations are not updated!
print("## ny.country (after deletion of us): ", ny.country)
# Flushing doesn't change anything (undersantably so)
session.flush()
print("## ny.country (+ flush): ", ny.country)
# Expiring manually
session.expire(ny, ['country'])
# Looks okay
print("## ny.country (+ expire): ", ny.country)
# Rolling back
session.rollback()


print("\n### 4. Deleting the other object:")
# Loading relations:
print("## us.capital: ", us.capital)
print("## ny.country: ", ny.country)
# Deleting one object of the relation:
session.delete(ny)
# The relations are not updated!
print("## us.capital (after deletion of ny): ", us.capital)
# Flushing doesn't change anything (undersantably so)
session.flush()
print("## us.capital (+ flush): ", us.capital)
# Expiring manually
session.expire(us, ['capital'])
# Looks okay
print("## us.capital (+ expire): ", us.capital)
# Rolling back
session.rollback()

结果

### 1. Creating the relation:
## us.capital:  None
## ny.country:  None
## ny.country (after rel creation):  <Country USA [1]>

### 2. Deleting relation:
## us.capital:  <Capital NYC [1]>
## ny.country:  <Country USA [1]>
## ny.country (after rel deletion):  None

### 3. Deleting one object:
## us.capital:  <Capital NYC [1]>
## ny.country:  <Country USA [1]>
## ny.country (after deletion of us):  <Country USA [1]>
## ny.country (+ flush):  <Country USA [1]>
## ny.country (+ expire):  None

### 4. Deleting the other object:
## us.capital:  <Capital NYC [1]>
## ny.country:  <Country USA [1]>
## us.capital (after deletion of ny):  <Capital NYC [1]>
## us.capital (+ flush):  <Capital NYC [1]>
## us.capital (+ expire):  None

【问题讨论】:

    标签: python orm sqlalchemy


    【解决方案1】:

    需要明确的是,SQLAlchemy 确实会在对象删除并提交后更新关系。

    以下是每个步骤的状态和关系变化的摘要:

    # Delete
    session.delete(us)
    assert instance_state(ny.country) in session._deleted
    
    # Flush
    assert not instance_state(ny.country).deleted
    session.flush()
    assert instance_state(ny.country).deleted
    
    # Commit
    assert ny.country is not None
    session.commit()
    assert ny.country is None
    

    为什么不在提交前更新关系

    SQLAlchemy 不/不能标记更新关联到已删除对象的关系是否有原因?

    可能只是没有请求、讨论和实施额外的处理。

    几点:

    • 取消分配关系的行为有所不同,因为这是通过 RelationshipProperty 本身进行的。
    • 直接修改关系有session.expunge()无法恢复的副作用。

    但是RelationshipProperty 可以根据其状态决定不返回实例。

    实现一个 mixin 来隐藏已删除的关系

    我们可以实现拦截__getattribute__的mixin来隐藏删除的关系。

    class HideDeletedRelationshipWithoutFlushMixin:
    
        def __getattribute__(self, item):
            value = super().__getattribute__(item)
            if value is not None and hasattr(value, DEFAULT_STATE_ATTR):
                state = instance_state(value)
                if (
                        state.deleted or                                               # After flush
                        state.session is not None and state in state.session._deleted  # Before flush
                ):
                    return None
            return value
    
    
    class HideDeletedRelationshipAfterFlushMixin:
    
        def __getattribute__(self, item):
            value = super().__getattribute__(item)
            if value is not None and hasattr(value, DEFAULT_STATE_ATTR) and instance_state(value).deleted:
                return None
            return value
    

    用法,取决于您希望删除的关系何时为None

    # Either
    Base = declarative_base(cls=HideDeletedRelationshipWithoutFlushMixin)
    # Or
    Base = declarative_base(cls=HideDeletedRelationshipAfterFlushMixin)
    

    替代方案:刷新时关系到期

    我们可以在'persistent_to_deleted'事件中实现一个函数来过期关系。

    def expire_back_populated_relations(session, instance):
        for relation in instance_state(instance).manager.mapper.relationships:
            related_instance = getattr(instance, relation.key)
            session.expire(related_instance, (relation.back_populates,))
    

    用法:

    @event.listens_for(Session, 'persistent_to_deleted')
    def receive_persistent_to_deleted(session, instance):
        expire_back_populated_relations(session, instance)
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-11-13
      • 1970-01-01
      • 2018-12-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多