【问题标题】:What is wrong with lazy="dynamic"? What are the alternatives?懒惰=“动态”有什么问题?有哪些替代方案?
【发布时间】:2019-09-22 20:27:27
【问题描述】:

这个问题已于 10/17/18 完全重写

为了有一个“编辑版本系统”(类似于 StackOverflow 的功能),我配置了以下类:

tags = db.Table(
    "tags",
    db.Column("tag_id", db.Integer, db.ForeignKey("tag.id")),
    db.Column("post_version_id", db.Integer,
        db.ForeignKey("post_version.id"))
    )

class Tag(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    tag = db.Column(db.String(128), index=True, unique=True)

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    head_id = db.Column(db.Integer, db.ForeignKey("post_version.id"))

class PostVersion(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    previous_id = db.Column(db.Integer, db.ForeignKey("post_version.id"), default=None)
    pointer_id = db.Column(db.Integer, db.ForeignKey("annotation.id"))
    current = db.Column(db.Boolean, index=True)
    tags = db.relationship("Tag", secondary=tags)

排除的是帖子内容等不相关的列。其实真正的数据模型是注解; 为了通用性,我已经简化了这些模型

实际数据由 136 个Post 的可变标记和通过编辑可变版本组成;也就是说:我生成了136个Post的。我有 15 个Tag。最初的 136 个Post 都与 2 个Tag 一致地标记。我随后用不同的标签标记了Post(使用我的编辑系统;因此编辑后的Post有多个PostVersion)。

您可能会注意到,Post 和 PostVersion 之间存在循环引用;我使用它来配置以下两个关系以进行实验:

关系 1 posts

posts = db.relationship("Post",
    secondary="join(tags, PostVersion,"
        "tags.c.post_version_id==PostVersion.id)",
    primaryjoin="Tag.id==tags.c.tag_id",
    secondaryjoin="Post.head_id==PostVersion.id",
    lazy="dynamic")

基于SQL语句

SELECT
    post.id
FROM
    tag
JOIN
    tags ON tag.id=tags.tag_id
JOIN
    post_version ON tags.post_version_id=post_version.id
JOIN
    post ON post.head_id=post_version.id
WHERE
    tag.id=<tag_id>

关系 2 posts2

posts2 = db.relationship("Post",
    secondary="join(tags, PostVersion,"
    "and_(tags.c.post_version_id==PostVersion.id,"
    "AnnotationVersion.current==True))",
    primaryjoin="Tag.id==tags.c.tag_id",
    secondaryjoin="PostVersion.pointer_id==Post.id",
    lazy="dynamic")

基于 SQL 语句

SELECT
    annotation.id
FROM
    tag
JOIN
    tags ON tag.id=tags.tag_id
JOIN
    annotation_version ON tags.annotation_version_id=annotation_version.id AND 
    annotation_version.current=1
JOIN
    annotation ON annotation_version.pointer_id = annotation.id
WHERE
    tag_id=8;

这会产生以下数据:

Tag         Actual      len(t.posts.all())  len(t.posts.paginate(1,5,False).items)
t1          0           0                   0
t2          1           136                 5
t3          1           136                 5
t8          136         136                 1
t14         136         136                 1
t15         24          136                 1

Tag         Actual      t.posts.count()     t.posts2.count()
t1          0           0                   0
t2          1           136                 163
t3          1           136                 163
t8          136         22168               26569
t14         136         22168               26569
t15         24          3264                3912

我已经排除了冗余标签(即所有其他 Tag 和 0 个 Post)和相同数据(即来自 posts2 的结果与 posts 相同)。

如您所见,结果存在一些严重问题!特别是对于这两种关系,如果关闭lazy="dynamic",则始终返回正确的Post

在创建引擎时使用echo=True,@IljaEverilä 发现lazy="dynamic" 更改了SQL。我在这个问题中引用了 cmets:

简而言之:使用lazy="dynamic" 你会得到FROM post, tags, post_version WHERE ...,但没有你会得到FROM post, tags JOIN post_version ON tags.post_version_id = post_version.id WHERE .... 正如你所看到的,动态设置几乎忽略了复合二级。现在的问题是“为什么?”


我的问题:

1。这是一个错误吗?

2。我能做些什么来纠正这个困境?


更新:

似乎lazy="dynamic" is explicitly discouraged here 但没有建议替代方案。仍然允许分页并依靠大型集合的替代方法是什么?默认情况下不允许这样做(或者至少在我访问它的方式上)并且文档似乎没有澄清问题!在标题为What kind of loading to use? 的部分中,它似乎为大型集合推荐的加载策略是lazy="subquery",但这不允许paginate()count()

【问题讨论】:

  • lazy="dynamic" 似乎有些有趣。没有它,关系会产生正确的查询并且计数匹配。有了它,生成的查询不会使用复合辅助(连接),因此查询最终会执行隐式交叉连接——因此会产生额外的行。 t.posts.all() 由于 SQLAlchemy 对单个模型实体结果进行重复数据删除的方式而隐藏了这一事实。
  • @IljaEverilä 返回所有注释肯定要快得多。但它会消除paginate()count() 的使用(不过我很好奇,当我在“产生正确的查询并且计数匹配”时仔细阅读您。 因为我唯一的方法是知道在没有lazy="dynamic" 的情况下计算结果将是使用len()。这是您的意思还是您正在使用其他方法?现在阅读lazy 参数
  • 简而言之:有lazy="dynamic",你得到FROM post, tags, post_version WHERE ...,但没有你得到FROM post, tags JOIN post_version ON tags.post_version_id = post_version.id WHERE ...。如您所见,动态设置几乎忽略了复合二级。现在的问题是“为什么?”
  • lazy="join" 默认使用 LEFT JOIN,但您可以使用 innerjoin=True 覆盖(默认为 False)。另一方面,这与您当前的困境有点相切:)。
  • 我在创建引擎时使用echo=True,因此会记录发出的 SQL。

标签: python flask sqlalchemy flask-sqlalchemy


【解决方案1】:

这确实是an issue SQLAlchemy 如何处理形成动态加载关系的查询。虽然查询应该是

SELECT post.id AS post_id, post.head_id AS post_head_id 
FROM post, tags JOIN post_version ON tags.post_version_id = post_version.id 
WHERE ? = tags.tag_id AND post.head_id = post_version.id

结果是

SELECT post.id AS post_id, post.head_id AS post_head_id 
FROM post, tags, post_version
WHERE ? = tags.tag_id AND post.head_id = post_version.id

因此,虽然postpost_version 之间存在内连接(在 SQL-92 之前的样式中),但 tagspost_version 之间的内连接丢失了,因此 @ 之间存在 CROSS JOIN 987654331@ 和其他人。结果是查询加载了所有当前的帖子版本,不管标签是什么,因为每个帖子都与来自tags 的每一行相连接。这也解释了t.posts.count()的乘法。

解决方法是等待fix,同时使用其他一些关系加载策略。

【讨论】:

  • 感谢 Ilja 提交错误报告。我阅读了这样做的要求,并不想开设 BitBucket 帐户。
猜你喜欢
  • 2011-03-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-01-25
  • 2010-09-07
相关资源
最近更新 更多