在 SqlAlchemy 中加载与项目子集的“关系”(类似于 Django 的“Prefetch”对象)

Load `relationship` with subset of items in SqlAlchemy (similar to Django's `Prefetch` object)

有没有办法从 SqlAlchemy 中的关系中检索元素的子集,但保持我在创建 class 时在 backref(关系)中定义的顺序?

假设我有两个 tables/models:一个 User 和一个 UserLog,其中包含用户执行的操作(Base 只是一个 declarative_base())

每个 UserLog 都有一个指向执行操作的 User 的 ID 的外键,每个日志的操作类型可以从 Enum 中选择。每个 UserLog 也有一个 created 我订购的时间戳:在常规情况下,我想从最旧到最新(最旧的优先)获取日志。

但是,我还希望能够让特定用户只加载一部分日志(特定操作的日志)。我知道这与连接负载的 "gist" 背道而驰,因为我会得到一个不切实际的数据库状态(有更多链接到用户的日志,我获取的 User 对象会反映出来),但是在某些情况下会有所帮助。

class UserLog(Base):
    __tablename__ = 'user_logs'
    id = Column(UUID(as_uuid=True), primary_key=True)
    timestamp_created = Column(
        DateTime, server_default=sqlalchemy.text("now()")
    )
    user_id = Column(
        UUID(as_uuid=True),
        ForeignKey("users.id", ondelete="CASCADE")
    )
    action = Column(SaEnum(UserLogsConstants), nullable=False)

    user = relationship(
        "User", 
        backref=backref(
          'logs', order_by="asc(UserLog.timestamp_created)"
        ) # The order_by is important here
    )

谢谢 I've been able to load a subset of logs using contains_eager,但后来我丢失了我在 backref.

中声明的 timestamp_created 的排序

假设我有两个可用的日志操作:UPLOAD_STARTUPLOAD_END(对于 action 列)

我设置了一个测试,其中我创建了一个 User,其中包含 5 个 UPLOAD_START 操作和 5 个 UPLOAD_END 创建的操作 乱序(我人为地先插入"newer"动作)。

所以我想要的是得到一个 User 和它的 .logs 关系包含 只有 事件是 UPLOAD_START backref对象中指定的时间戳保持顺序。

这是测试查询

u = session.query(User) \
    .filter(User.id == test_user_id) \
    .join(User.logs) \
    .filter(UserLog.action == UserLogsConstants.UPLOAD_START) \
    .options(contains_eager(User.logs)) \
    .one()

这两个断言工作正常:

assert len(u.logs) == 5  
# I inserted 10 logs total, only 5 with action=UPLOAD_START

assert all(ul.action == UserLogsConstants.UPLOAD_START for ul in u.logs)
# Neat, only logs with `UPLOAD_START`

但是这个失败了:不遵守顺序。

assert all(
    u.logs[i].timestamp_created < u.logs[i + 1].timestamp_created
    for i in range(len(u.logs) - 1)
)

有道理。如果我理解 contains_eager 行为,这有点像 忘记你认为的关系是什么,并使用我在前面的查询中告诉你的内容 (而且我'我没有说任何有关该查询中的顺序的信息)。而且,确实:我正在查看 SqlAlchemy 生成的 SQL 并且没有 ORDER BY 子句。我想我总是可以自己将它添加到查询中,但如果我不需要的话会更干净。

对于那些熟悉 Django 的人,我尝试模拟 Prefech 对象,我可以在其中指定一个子查询以从以下位置获取加载的对象:

User.objects.prefetch_related(
     Prefetch(
         'logs', 
         queryset=UserLogs.objects.filter(action=UserLogsConstants.UPLOAD_START)
     )
).get(pk='foo-uuid')

当使用 contains_eager 时,您告诉 SQLAlchemy 忽略配置的关系,而是从您手工制作的查询中填充 .logs 容器。这意味着关系上的任何排序设置也将被忽略。

所以如果需要对日志数据进行排序,需要自己动手:

u = (
    session.query(User)
        .filter(User.id == test_user_id)
        .join(User.logs)
        .filter(UserLog.action == UserLogsConstants.UPLOAD_START)
        .order_by(User.id, asc(UserLog.timestamp_created))
        .options(contains_eager(User.logs))
        .one()
)

如果您更常过滤用户日志,那么另一种选择是将 logs 设为 dynamic relationship。这会将容器替换为您要过滤的预填充 Query 对象:

user = relationship(
    "User", 
    backref=backref(
      'logs', lazy='dynamic', order_by="asc(UserLog.timestamp_created)"
    )
)

并使用

u = session.query(User).filter(User.id == test_user_id).one()
upload_start_logs = u.logs.filter(UserLog.action == UserLogsConstants.UPLOAD_START).all()

当然,这确实会发出一个单独的查询。