这个答案来自 sqlalchemy google group 上的 Mike Bayer。我在这里发布它以帮助人们:
TLDR:
我使用 Mike 的回答中的version 1 来解决我的问题,因为在这种情况下,我没有涉及此关系的外键,因此无法使用LATERAL。版本 1 效果很好,但一定要注意 offset 的效果。它在测试期间让我失望了一段时间,因为我没有注意到它被设置为 0 以外的东西。
版本 1 的代码块:
subq = s.query(Messages.date).\
filter(Messages.user_id == User.id).\
order_by(Messages.date.desc()).\
limit(1).offset(10).correlate(User).as_scalar()
q = s.query(User).join(
Messages,
and_(User.id == Messages.user_id, Messages.date > subq)
).options(contains_eager(User.messages))
迈克的回答
所以你应该忽略它是否使用“声明式”,这与查询无关,实际上一开始也忽略查询,因为首先这是一个SQL问题。您需要一个执行此操作的 SQL 语句。 SQL 中的哪个查询会从主表中加载大量行,并为每个主表连接到辅助表的前十行?
LIMIT 很棘手,因为它实际上不是通常的“关系代数”计算的一部分。它不在此范围内,因为它是对行的人为限制。例如,我对如何做到这一点的第一个想法是错误的:
select * from users left outer join (select * from messages limit 10) as anon_1 on users.id = anon_1.user_id
这是错误的,因为它只获取聚合中的前十条消息,而忽略用户。我们希望为每个用户获取前 10 条消息,这意味着我们需要为每个用户单独执行此“从消息限制 10 中选择”。也就是说,我们需要以某种方式关联。关联子查询虽然通常不允许作为 FROM 元素,只允许作为 SQL 表达式,但它只能返回单列单行;我们通常不能在普通的普通 SQL 中加入相关的子查询。但是,我们可以在 JOIN 的 ON 子句中进行关联,从而在普通 SQL 中实现这一点。
但首先,如果我们使用现代 Postgresql 版本,我们可以打破通常的关联规则并使用称为 LATERAL 的关键字,它允许在 FROM 子句中进行关联。 LATERAL 仅受现代 Postgresql 版本支持,它使这很容易:
select * from users left outer join lateral
(select * from message where message.user_id = users.id order by messages.date desc limit 10) as anon1 on users.id = anon_1.user_id
我们支持 LATERAL 关键字。上面的查询如下所示:
subq = s.query(Messages).\
filter(Messages.user_id == User.id).\
order_by(Messages.date.desc()).limit(10).subquery().lateral()
q = s.query(User).outerjoin(subq).\
options(contains_eager(User.messages, alias=subq))
请注意,在上面,为了选择用户和消息并将它们生成到 User.messages 集合中,必须使用“contains_eager()”选项,并且“动态”必须消失。这不是唯一的选择,例如,您可以为没有“动态”的 User.messages 建立第二个关系,或者您可以单独从查询(用户,消息)加载并根据需要组织结果元组。
如果您不使用 Postgresql 或不支持 LATERAL 的 Postgresql 版本,则必须将关联处理到连接的 ON 子句中。 SQL 看起来像:
select * from users left outer join messages on
users.id = messages.user_id and messages.date > (select date from messages where messages.user_id = users.id order by date desc limit 1 offset 10)
在这里,为了将 LIMIT 塞进去,我们实际上是使用 OFFSET 遍历前 10 行,然后执行 LIMIT 1 以获得代表我们希望每个用户的下限日期的日期。然后我们必须在比较该日期时加入,如果该列没有被索引,这可能会很昂贵,如果有重复的日期也可能不准确。
这个查询看起来像:
subq = s.query(Messages.date).\
filter(Messages.user_id == User.id).\
order_by(Messages.date.desc()).\
limit(1).offset(10).correlate(User).as_scalar()
q = s.query(User).join(
Messages,
and_(User.id == Messages.user_id, Messages.date >= subq)
).options(contains_eager(User.messages))
如果没有经过良好的测试,我不相信这类查询,因此下面的 POC 包括两个版本,包括健全性检查。
from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base
import datetime
Base = declarative_base()
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
messages = relationship(
'Messages', order_by='desc(Messages.date)')
class Messages(Base):
__tablename__ = 'message'
id = Column(Integer, primary_key=True)
user_id = Column(ForeignKey('user.id'))
date = Column(Date)
e = create_engine("postgresql://scott:tiger@localhost/test", echo=True)
Base.metadata.drop_all(e)
Base.metadata.create_all(e)
s = Session(e)
s.add_all([
User(id=i, messages=[
Messages(id=(i * 20) + j, date=datetime.date(2017, 3, j))
for j in range(1, 20)
]) for i in range(1, 51)
])
s.commit()
top_ten_dates = set(datetime.date(2017, 3, j) for j in range(10, 20))
def run_test(q):
all_u = q.all()
assert len(all_u) == 50
for u in all_u:
messages = u.messages
assert len(messages) == 10
for m in messages:
assert m.user_id == u.id
received = set(m.date for m in messages)
assert received == top_ten_dates
# version 1. no LATERAL
s.close()
subq = s.query(Messages.date).\
filter(Messages.user_id == User.id).\
order_by(Messages.date.desc()).\
limit(1).offset(10).correlate(User).as_scalar()
q = s.query(User).join(
Messages,
and_(User.id == Messages.user_id, Messages.date > subq)
).options(contains_eager(User.messages))
run_test(q)
# version 2. LATERAL
s.close()
subq = s.query(Messages).\
filter(Messages.user_id == User.id).\
order_by(Messages.date.desc()).limit(10).subquery().lateral()
q = s.query(User).outerjoin(subq).\
options(contains_eager(User.messages, alias=subq))
run_test(q)