多个外键上的SQLAlchemy双内连接

时间:2018-10-04 13:36:26

标签: python sqlalchemy

请在底部查看更新

我有三节课。我们称它们为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))

要发布新帖子,我同时创建了一个帖子和一个PostVersion指向的初始Post.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中使用了一种令人讨厌的方法,其中我使用PostVersion过滤器来查询带有标签的所有or_

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的列表,我在想那是可以定义它。但是我不确定如何实现这一目标。

我现在还想知道我是否可以通过一种配置良好的关系而放弃pointer_id上的PostVersion。但是,这与问题无关(尽管循环引用确实会引起头痛)。

作为参考,我正在使用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>";

2 个答案:

答案 0 :(得分:1)

您可以更改数据库设计,还是必须让您的应用程序在无法更改的数据库上工作?如果是后者,我无能为力。如果可以更改设计,则应该这样:

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

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

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

答案 1 :(得分:0)

我自己解决了这个问题,实际上只包含了在主数据库中使用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")

如您所见,我将表名和类名混合使用。我将在尝试进行实验时更新答案,以使其更加常规。