SQLAlchemy自引用多对多关系

时间:2018-04-22 12:50:29

标签: python sql sqlalchemy many-to-many

我在SQLAlchemy中使用python 2.7,并试图建立一个多对多关系的友情关系。

我需要桌子完全对称;如果A是B的朋友,那么它也必须是另一种方式。

我尝试使用辅助友谊表对关系进行建模,并使用primary-和secondaryjoin将其连接到模型,但我开始觉得我的方向错误。

我发现this post有人试图用一对多的关系模仿同样的东西,但这对我不起作用,因为我的友谊关系不是一对一的。

我已经设法使用多对多的表来实现一个工作模型,如果我正在使用“重复”:当我想添加B作为A的朋友时,我也添加A作为B的朋友。但我觉得建议的解决方案应该更加整洁。

这里的最终游戏类似于Facebook的友谊模特。如果B是A的朋友,A只能是B的朋友。

1 个答案:

答案 0 :(得分:2)

使用自定义主要和次要连接条件的第一次尝试可以使用composite "secondary"进行扩充,在这种情况下,它将是从关联表中选择的两种可能方式的并集。给定玩具用户模型,如

class User(Base):
    __tablename__ = "user"
    id = Column(Integer, primary_key=True)
    email = Column(Unicode(255), unique=True)

关联表可能看起来像

friendship = Table(
    "friendship", Base.metadata,
    Column("left_id", ForeignKey("user.id"), primary_key=True),
    Column("right_id", ForeignKey("user.id"), primary_key=True))

和复合“二级”

friends = select([friendship.c.left_id.label("left_id"),
                  friendship.c.right_id.label("right_id")]).\
    union_all(select([friendship.c.right_id,
                      friendship.c.left_id])).\
    alias("friends")

使用上面的User模型将关系定义为

User.friends = relationship(
    "User", secondary=friends,
    primaryjoin=User.id == friends.c.left_id,
    secondaryjoin=User.id == friends.c.right_id,
    viewonly=True)

不幸的副作用是该关系是只读的,您必须手动将行插入friendship以使用户成为朋友。此外还存在重复问题,例如,friendship仍可包含(1, 2)(2, 1)。添加一个强制对左右id进行排序的检查约束可以解决重复问题:

# NOTE: This has to be done *before* creating your tables. You could also
# pass the CheckConstraint as an argument to Table directly.
chk = CheckConstraint(friendship.c.left_id < friendship.c.right_id)
friendship.append_constraint(chk)

但是,应用程序必须在插入时订购ID。为了解决这个问题,用作“辅助”的联合可以隐藏在可写视图中。 SQLAlchemy没有用于处理开箱即用视图的构造,但有一个usage recipe for just that。使用配方friends变为:

friends = view(
    "friends",
    Base.metadata,
    select([friendship.c.left_id.label("left_id"),
            friendship.c.right_id.label("right_id")]).\
        union_all(select([friendship.c.right_id,
                          friendship.c.left_id])))

要使视图可写,需要一些触发器:

# For SQLite only. Other databases have their own syntax for triggers.
DDL("""
    CREATE TRIGGER friends_insert_trg1 INSTEAD OF INSERT ON friends
    WHEN new.left_id < new.right_id
    BEGIN
        INSERT INTO friendship (left_id, right_id)
        VALUES (new.left_id, new.right_id);
    END;
    """).execute_at("after-create", Base.metadata)

DDL("""
    CREATE TRIGGER friends_insert_trg2 INSTEAD OF INSERT ON friends
    WHEN new.left_id > new.right_id
    BEGIN
        INSERT INTO friendship (left_id, right_id)
        VALUES (new.right_id, new.left_id);
    END;
    """).execute_at("after-create", Base.metadata)

将这些更紧密地绑定到视图的创建会很好,但只要在定义视图后注册它们,这也会很好。有了触发器,您可以从viewonly=True关系中删除User.friends参数。

全部放在一起:

from sqlalchemy import \
    Table, Column, Integer, Unicode, ForeignKey, CheckConstraint, DDL, \
    select

from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base

from view import view

Base = declarative_base()


class User(Base):
    __tablename__ = "user"
    id = Column(Integer, primary_key=True)
    email = Column(Unicode(255), unique=True)

friendship = Table(
    "friendship",
    Base.metadata,
    Column("left_id", ForeignKey("user.id"), primary_key=True),
    Column("right_id", ForeignKey("user.id"), primary_key=True),
    CheckConstraint("left_id < right_id"))

friends = view(
    "friends",
    Base.metadata,
    select([friendship.c.left_id.label("left_id"),
            friendship.c.right_id.label("right_id")]).\
        union_all(select([friendship.c.right_id,
                          friendship.c.left_id])))

User.friends = relationship(
    "User", secondary=friends,
    primaryjoin=User.id == friends.c.left_id,
    secondaryjoin=User.id == friends.c.right_id)

DDL("""
    CREATE TRIGGER friends_insert_trg1 INSTEAD OF INSERT ON friends
    WHEN new.left_id < new.right_id
    BEGIN
        INSERT INTO friendship (left_id, right_id)
        VALUES (new.left_id, new.right_id);
    END;
    """).execute_at("after-create", Base.metadata)

DDL("""
    CREATE TRIGGER friends_insert_trg2 INSTEAD OF INSERT ON friends
    WHEN new.left_id > new.right_id
    BEGIN
        INSERT INTO friendship (left_id, right_id)
        VALUES (new.right_id, new.left_id);
    END;
    """).execute_at("after-create", Base.metadata)