【问题标题】:SQLAlchemy Self-Referential Many to Many Symmetric RelationshipSQLAlchemy 自引用多对多对称关系
【发布时间】:2018-10-02 14:42:19
【问题描述】:

我正在使用带有 SQLAlchemy 的 python 2.7,并尝试使用多对多关系来建模友谊关系。

我需要桌子完全对称;如果 A 是 B 的朋友,那么它也必须反过来。

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

我发现 this post 有人尝试使用一对多关系来模拟同一事物,但这对我不起作用,因为我的友谊关系不是一对多。

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

这里的最终游戏类似于 Facebook 的友谊建模。只有当 B 是 A 的朋友时,A 才能成为 B 的朋友。

【问题讨论】:

  • 强制对关联表中存储的 id 进行排序以避免重复,并结合使用两种可能的方式来选择辅助“表”。
  • 你能举个例子吗? @ilja everila
  • 你用的是什么数据库?
  • 我正在使用 Sqlite
  • @IljaEverilä 谢谢,但链接指向一个纯 SQL 解决方案,我知道,我正在寻找一种使用 sqlalchemy 的方法,如果你愿意,我愿意接受你的回答发布一个。

标签: python sql sqlalchemy many-to-many


【解决方案1】:

第一次尝试使用自定义主连接条件和辅助连接条件时,可以使用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)

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

把它们放在一起:

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)

【讨论】:

  • everila - 感谢您提供非常详细的答案,我很难使用它,是否经过测试?如果是这样,我会继续寻找我的代码有什么问题
  • 确实如此,但您当然可能发现了一些被忽视的优势。如果元数据中不存在表 user ,则至少有一个可能不明显的问题,即定义联合以用作辅助或视图将失败,因此 User 模型必须在添加关系属性之前定义。
猜你喜欢
  • 1970-01-01
  • 2011-05-09
  • 1970-01-01
  • 2021-05-31
  • 2014-09-30
  • 2015-03-03
  • 1970-01-01
  • 2018-08-19
  • 2017-02-05
相关资源
最近更新 更多