SQLAlchemy 在使用 contains_eager 时加载不相关的缓存 objects
SQLAlchemy loads unrelated cached objects when using contains_eager
我正在使用 SQLAlchemy 的 contains_eager
功能,当 object 已加载到现有会话中时,我看到了奇怪的行为。具体来说,那些 object 似乎没有像加载新数据时那样从关系集合中过滤掉。
这是一个最小的例子。步骤为
- 创建 parent-child 关系。
- 添加一个 parent 和两个 children
value
s.
- 执行联合过滤查询,使用
contains_eager
加载 parent 的匹配 children。注意 filter
应该排除两个 children. 之一
- 观察结果 object.
的 children
属性 中的两个 children 均已填充
- 使用新的session,甚至调用
session.expire_all()
也能得到正确的结果,说明问题是children已经存在于当前session中
这是预期的行为吗?如果是这样,调用 expire_all
是避免这种情况的正确做法吗?
更一般地说,是否应该因此而避免 contains_eager
?如果在发出查询之前必须跟踪 child object 是否已经存在,这似乎是抽象的突破。但也许我遗漏了什么。
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 页面上 asked this question。解决方案是在任何使用 contains_eager
和 filter
的查询上使用 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()
我正在使用 SQLAlchemy 的 contains_eager
功能,当 object 已加载到现有会话中时,我看到了奇怪的行为。具体来说,那些 object 似乎没有像加载新数据时那样从关系集合中过滤掉。
这是一个最小的例子。步骤为
- 创建 parent-child 关系。
- 添加一个 parent 和两个 children
value
s. - 执行联合过滤查询,使用
contains_eager
加载 parent 的匹配 children。注意filter
应该排除两个 children. 之一
- 观察结果 object. 的
- 使用新的session,甚至调用
session.expire_all()
也能得到正确的结果,说明问题是children已经存在于当前session中
children
属性 中的两个 children 均已填充
这是预期的行为吗?如果是这样,调用 expire_all
是避免这种情况的正确做法吗?
更一般地说,是否应该因此而避免 contains_eager
?如果在发出查询之前必须跟踪 child object 是否已经存在,这似乎是抽象的突破。但也许我遗漏了什么。
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 页面上 asked this question。解决方案是在任何使用 contains_eager
和 filter
的查询上使用 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()