【问题标题】:SQLAlchemy Double Inner Join on multiple foreign keys多个外键上的 SQLAlchemy 双内连接
【发布时间】:2019-03-09 22:19:18
【问题描述】:

请查看底部更新

我有三门课。我们称它们为PostPostVersionTag。 (这是针对 Web 应用程序中的内部版本控制系统,可能类似于 StackOverflow,尽管我不确定它们的实施策略)。我有点使用 git 中的术语来理解它。出于此问题的目的,这些是类的高度简化版本:

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
    author = db.relationship("User", backref="posts")
    head_id = db.Column(db.Integer, db.ForeignKey("post_version.id"))
    HEAD = db.relationship("PostVersion", foreign_keys=[head_id])
    added = db.Column(db.DateTime, default=datetime.utcnow)

class PostVersion(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    editor_id = db.Column(db.Integer, db.ForeignKey("user.id"))
    editor = db.relationship("User")
    previous_id = db.Column(db.Integer, db.ForeignKey("post_version.id"), default=None)
    previous = db.relationship("PostVersion")
    pointer_id = db.Column(db.Integer, db.ForeignKey("post.id"))
    pointer = db.relationship("Post", foreign_keys=[pointer_id])
    post = db.Column(db.Text)
    modified = db.Column(db.DateTime, default=datetime.utcnow)
    tag_1_id = db.Column(db.Integer, db.ForeignKey("tag.id"), default=None)
    tag_2_id = db.Column(db.Integer, db.ForeignKey("tag.id"), default=None)
    tag_3_id = db.Column(db.Integer, db.ForeignKey("tag.id"), default=None)
    tag_4_id = db.Column(db.Integer, db.ForeignKey("tag.id"), default=None)
    tag_5_id = db.Column(db.Integer, db.ForeignKey("tag.id"), default=None)
    tag_1 = db.relationship("Tag", foreign_keys=[tag_1_id])
    tag_2 = db.relationship("Tag", foreign_keys=[tag_2_id])
    tag_3 = db.relationship("Tag", foreign_keys=[tag_3_id])
    tag_4 = db.relationship("Tag", foreign_keys=[tag_4_id])
    tag_5 = db.relationship("Tag", foreign_keys=[tag_5_id])

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

为了创建一个新帖子,我创建了一个帖子和一个初始 PostVersionPost.head_id 指向它。每次进行编辑时,都会创建一个新的PostVersion 指向以前的PostVersion,并将Post.head_id 重置为指向新的PostVersion。要将发布版本重置为早期版本——好吧,我还没有做到这一点,但是复制之前的版本或者只是将指针重置为之前的版本似乎很简单。

不过,我的问题是:如何在 PostTag 之间写出这样的关系

  1. Post.tags 将是当前 PostVersion 包含的所有标签的列表,并且
  2. Tag.posts 将是当前具有该特定标签的所有 Post 的列表?

第一个条件似乎很简单,一个简单的方法

def get_tags(self):
    t = []
    if self.HEAD.tag_1:
        t.append(self.HEAD.tag_1)
    if self.HEAD.tag_2:
        t.append(self.HEAD.tag_2)
    if self.HEAD.tag_3:
        t.append(self.HEAD.tag_3)
    if self.HEAD.tag_4:
        t.append(self.HEAD.tag_4)
    if self.HEAD.tag_5:
        t.append(self.HEAD.tag_5)
    return t

这个技巧现在还不错,但第二个条件现在对我来说几乎是棘手的。我目前在 Tag 中使用了一种令人讨厌的方法,我使用or_ 过滤器查询所有带有标签的PostVersion

def get_posts(self):
    edits = PostVersion.query.filter(or_(
         PostVersion.tag_1_id==self.id,
         PostVersion.tag_2_id==self.id,
         PostVersion.tag_3_id==self.id,
         PostVersion.tag_4_id==self.id,
         PostVersion.tag_5_id==self.id,
         ).order_by(PostVersion.modified.desc()).all()
    posts = []
    for e in edits:
        if self in e.pointer.get_tags() and e.pointer not in posts:
            posts.append(e.pointer)
    return posts

这非常低效,我无法对结果进行分页。

我知道这将是从 PostTagTagPostPostVersion 的辅助连接,但它必须是 or 上的辅助连接,我不知道怎么开始写。

回顾我的代码,我开始想知道为什么其中一些关系需要定义foreign_keys 参数,而另一些则不需要。我认为这与它们的定义位置有关(是否紧跟在 FK id 列之后)并注意到有一个 foreign_keys 的列表,我在想 这就是我可以如何定义它。但我不确定如何追求这一点。

我现在也想知道是否可以通过配置良好的关系免除PostVersion 上的pointer_id。然而,这与问题无关(尽管循环引用确实令人头疼)。

作为参考,我正在使用 Flask-SQLAlchemy、Flask-migrate 和 MariaDB。我非常关注Miguel Grinberg's Flask Megatutorial

任何帮助或建议都是天赐之物。

更新

我设计了以下 有效的 mysql 查询,现在我需要将它翻译成 sqlalchemy:

SELECT
    post.id, tag.tag 
FROM
    post
INNER JOIN
    post_version
ON
    post.head_id=post_version.id
INNER JOIN 
    tag
ON 
    post_version.tag_1_id=tag.id OR
    post_version.tag_2_id=tag.id OR
    post_version.tag_3_id=tag.id OR
    post_version.tag_4_id=tag.id OR
    post_version.tag_5_id=tag.id OR
WHERE
    tag.tag="<tag name>";

【问题讨论】:

    标签: python sqlalchemy


    【解决方案1】:

    您能否更改数据库设计,或者您是否必须让您的应用在您无法更改的数据库上运行?如果是后者,我帮不了你。如果你可以改变设计,你应该这样做:

    1. 将 PostVersions 链接链替换为从 Post 到 PostVersions 的一对多关系。您的“帖子”类最终将与与该帖子相关的所有 PostVersion 实例建立关系“版本”。

    2. 使用附加关联表将 tag_id 成员替换为多对多关系。

    这两种方法在 SQLAlchemy 文档中都有很好的解释。确保从最少的代码开始,在小型非 Flask 命令行程序中进行测试。掌握基本功能后,将概念转移到更复杂的类中。之后,再次问自己最初的问题。答案会容易得多。

    【讨论】:

    • 我可以更改数据库,但我有两个问题要问您:(1)Post-PostVersion 上的一对多表如何识别which PostVersion 是最新的吗?还应该有head_id 吗?实际上,我现在基本上有一个基于pointer_id 的一对多表(2)我不喜欢多对多标签方法,因为它(a)对每个帖子的标签没有硬性限制,并且(b)确实没有内置的版本控制系统(就目前而言,更改帖子的标签是帖子的新版本)。如果不转换为多对多标签表,就没有办法实现这一点吗?
    • 从技术上讲,我在PostPostVersion 之间有一个一对多表(从PostVersion.pointer_idPost.id),一个一对一表(从Post.head_idPostVersion.id)和一个链表(从PostVersion.previous_idPostVersion.id
    • (1) 如果您的 PostVersion 表包含自增主键 ID,您只需选择 ID 最高的版本即可。 (2) 两者最大。标签限制和新 PostVersion 的创建应该在应用程序逻辑中实现,而不是硬编码到数据库机制中。想象一下,如果有一天有人决定每个帖子是否应该限制为 4、6 或 10 个标签,那会是多么头疼。编程中的一般提示:如果您发现自己一遍又一遍地输入(或复制和粘贴)基本相同的内容,例如您的五倍 tag_id 条件,您就知道您的代码存在严重缺陷。
    • 关于您的第二条评论:是的,从技术上讲,您拥有这些关系。如,信息在那里,可以找到。然而,实际上,这两种方法都是不灵活和丑陋的,因此不是这样做的方法。您被困在原地不是因为您不够精通 SQL,而是因为您的数据库一开始就设计不当。
    • 我会考虑所有这些,感谢您的建议。我对不得不一遍又一遍地输入五个标签感到恼火(我同意),但我看到标签上的替代多对多表导致 很多 额外的应用程序逻辑我认为是不必要的。我也看到硬编码标签的缺点(例如:如果有人更改标签 1 但将原始标签 2 包含为标签 2,则不会检测到任何更改)。实际上我自己解决了这个问题并将发布答案,但我也赞成你的,因为这是有效的建议。
    【解决方案2】:

    我自己解决了这个问题,它实际上只包括在主节点中使用 or_ 定义主节点和次节点:

    posts = db.relationship("Post", secondary="post_version",
        primaryjoin="or_(Tag.id==post_version.c.tag_1_id,"
        "Tag.id==post_version.c.tag_2_id,"
        "Tag.id==post_version.c.tag_3_id,"
        "Tag.id==post_version.c.tag_4_id,"
        "Tag.id==post_version.c.tag_5_id)",
        secondaryjoin="Annotation.head_id==post_version.c.id",
        lazy="dynamic")
    

    如您所见,我混合了表名和类名。我会在实验时更新答案以使其更规律。

    【讨论】:

      猜你喜欢
      • 2021-09-05
      • 2013-03-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-03-10
      • 1970-01-01
      • 2012-05-15
      相关资源
      最近更新 更多