在 SQLAlchemy 中按关系字段过滤

Filter by field on relationship in SQLAlchemy

我有一个非常特殊的情况,我想要一个组实体,其中包含一个包含符合某些条件的元素的列表。

这些是我定义的 ORM class:

class Group(Base):
    __tablename__ = 'groups'
    id = Column(Integer, Identity(1, 1), primary_key=True)
    name = Column(String(50), nullable=False)
    elements = relationship('Element', foreign_keys='[Element.group_id]')

class Element(Base):
    __tablename__ = 'elemnts'
    id = Column(Integer, Identity(1, 1), primary_key=True)
    date = Column(Date, nullable=False)
    value = Column(Numeric(38, 10), nullable=False)
    group_id = Column(Integer, ForeignKey('groups.id'), nullable=False)

现在,我想检索包含特定日期的所有元素的组。

result = session.query(Group).filter(Group.name == 'group 1' and Element.date == '2021-05-27').all()

遗憾的是,Group.name 过滤器正在工作,但检索到的组包含所有元素,忽略了 Element.date 条件。

按照@van的建议,我试过了:

query(Group).join(Element).filter(Group.name == 'group 1' and Element.date == '2021-05-27')

但是我又得到了每一个元素。在我注意到的日志中:

SELECT groups.id AS group_id, groups.name AS groups_name,  element_1.id AS element_1_id, element_1.date AS element_1_date, element_1.value AS element_1_value, element_1.group_id AS element_1_group_id 
FROM groups JOIN elements ON groups.id = elements.group_id LEFT OUTER JOIN elements AS elements_1 ON groups.id = elements_1.group_id 
WHERE groups.name = %(name_1)s

在那里,我注意到两件事。首先,加入进行了两次(我想在加入之前已经完成了一次获取组)。其次也是最重要的一点:日期过滤器没有出现在查询中。

驱动我用的是mssql+pymssql驱动

python中的and与SQL中的and条件不同。 SQLAlchemy 有一种自定义的方式来使用 and_() 方法来处理连词,即

result = session.query(Group).join(Element).filter(and_(Group.name == 'group 1', Element.date == '2021-05-27')).all()

好的,这里似乎发生了一些事情。


首先,您的关系 Group.elements 基本上总是包含组的所有元素。这与过滤器完全分开,这就是 SA 的工作方式。

您可以这样理解您当前的查询(session.query(Group).filter(Group.name == 'group 1' and Element.date == '2021-05-27').all()):

"Return all Group instances which contain an Element for a given date."

但是当您遍历 Group.elements 时,SA 将确保 return all children .这就是您要解决的问题。


其次,正如 所指出的,您不能使用简单的 python and 来创建 AND SQL 子句。请使用 and_ 或仅使用单独的子句来修复:

result = (
    session.query(Group)
    .filter(Group.name == "group 1")
    .filter(Element.date == dat1)
    .all()
)

第三,正如您稍后指出的,您的关系是 lazy="joined",这就是为什么每当您查询 Group 时,相关的 Element 实例都将使用 OUTER JOIN 条件。这就是为什么在您的查询中添加 .join(Element) 会导致两个 JOIN。


解决方案

您可以通过使用 orm.contains_eager() 选项“欺骗”SA 认为它加载了所有 Group.elements 关系,而它只加载了您想要的子项,您的查询如下所示:

result = (
    session.query(Group)
    .join(Element)
    .filter(Group.name == "group 1")
    .filter(Element.date == dat1)
    .options(contains_eager(Group.elements))
    .all()
)

以上应该也适用于 lazy="joined",因为不应再生成额外的 JOIN。

更新

如果您希望在没有符合所需条件的元素的情况下获取组,您需要执行以下操作:

  • join替换为outerjoin
  • 将过滤器放在 outerjoin 子句内的 Element
result = (
    session.query(Group)
    .filter(Group.name == "group 1")
    .outerjoin(
        Element, and_(Element.group_id == Group.id, Element.date == dat1)
    )
    .options(contains_eager(Group.elements))
    .all()
)