SQLAlchemy在使用contains_earce时加载不相关的缓存对象



我正在使用SQLAlchemy的contains_eager功能,当对象已经加载到现有会话中时,我看到了奇怪的行为。具体来说,这些对象似乎不会像加载新数据时那样从关系集合中筛选出来。

这里是一个最小的例子。步骤是

  1. 创建父子关系
  2. 添加一个父级和两个具有不同values的子级
  3. 使用contains_eager为父级加载匹配的子级,执行已联接、已筛选的查询。请注意,filter应排除两个子项中的一个子项
  4. 请注意,两个子对象都已填充到结果对象的children属性中
  5. 可以通过使用新会话,甚至通过调用session.expire_all()来获得正确的结果,这表明问题在于子级已经存在于当前会话中

这是预期的行为吗?如果是这样的话,调用expire_all是避免这种情况的正确做法吗?

更普遍地说,是否应该因此避免contains_eager?如果在发出查询之前必须跟踪子对象是否已经存在,那么这似乎是对抽象的突破。但也许我错过了什么。

from sqlalchemy import and_, Column, create_engine, DateTime, ForeignKey, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import contains_eager, relationship, sessionmaker

create_statements = ["""
DROP TABLE IF EXISTS child;
""", """
DROP TABLE IF EXISTS parent;
""", """
CREATE TABLE parent
(
id INTEGER NOT NULL PRIMARY KEY,
name VARCHAR
);
""", """
CREATE TABLE child
(
id INTEGER NOT NULL PRIMARY KEY,
parent_id INTEGER REFERENCES parent(id),
value INTEGER
);
"""
]
Base = declarative_base()

class Parent(Base):
__tablename__ = "parent"
__table_args__ = {'implicit_returning': False}
id = Column(Integer, primary_key=True)
name = Column(String)
children = relationship("Child", back_populates="parent")

class Child(Base):
__tablename__ = "child"
__table_args__ = {'implicit_returning': False}
id = Column(Integer, primary_key=True)
parent_id = Column(Integer, ForeignKey(Parent.id))
value = Column(Integer)
parent = relationship(Parent, back_populates="children")

if __name__ == "__main__":
engine = create_engine(f"sqlite:///")
session = sessionmaker(bind=engine)()
for statement in create_statements:
session.execute(statement)
p1 = Parent(id=1, name="A")
c1 = Child(id=1, parent=p1, value=10)
c2 = Child(id=2, parent=p1, value=20)
session.add_all([p1, c1, c2])
session.flush()
# session.expire_all()  # Uncommenting this makes the below work as expected.
results = session 
.query(Parent) 
.join(Child, Parent.id == Child.parent_id) 
.options(
contains_eager(Parent.children)
).filter(Child.value < 15) 
.order_by(Parent.id) 
.all()
print(len(results[0].children))  # We should only have 1 child.
print(all(c.value < 15 for c in results[0].children))  # All children should match the above filter condition.

我在SQLAlchemy GitHub页面上问了这个问题。解决方案是对任何使用contains_eagerfilter的查询使用populate_existing。在我的具体例子中,这个查询做了正确的事情

session 
.query(Parent) 
.join(Child, Parent.id == Child.parent_id) 
.options(
contains_eager(Parent.children)
).filter(Child.value < 15) 
.order_by(Parent.id) 
.populate_existing() 
.all()

最新更新