SQLAlchemy:三元组中的定向关系

SQLAlchemy: directed relationships within triads

给定用户和他们之间的定向关系:

from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class User(Base):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True)
    username = Column(String(30), nullable=False)
    follows = relationship(
        'User', secondary='following',
        primaryjoin=(Following.follower_id == id),
        secondaryjoin=(Following.followee_id == id)
    )
    followed_by = relationship(
        'User', secondary='following',
        primaryjoin=(Following.followee_id == id),
        secondaryjoin=(Following.follower_id == id)
    )

class Following(Base):
    __tablename__ = 'following'
    follower_id = Column(Integer, ForeignKey('user.id'), primary_key=True)
    followee_id = Column(Integer, ForeignKey('user.id'), primary_key=True)

我需要使用 SQLAlchemy 来 select 三合会用户,例如:

A follows B
B follows A
A follows C
C follows B  

首先我不知道如何使用 exists() 到 select 双向关系。

我认为在所需的关系 (table Following) 上使用多个内部 JOIN 可以以一种相当直接的方式实现此目的:

# create aliases for each pair of relations that must exist
AB = aliased(Following)
BA = aliased(Following)
AC = aliased(Following)
CB = aliased(Following)
CA = aliased(Following)

triads = (
    session.query(
        AB.follower_id.label("a_id"),
        AB.followee_id.label("b_id"),
        AC.followee_id.label("c_id"),
    )
    # .select_from(AB)  # @note: this is implied, so not really required
    .join(BA, and_(AB.followee_id == BA.follower_id,
                   AB.follower_id == BA.followee_id))
    .join(AC, AB.follower_id == AC.follower_id)
    .join(CB, and_(AC.followee_id == CB.follower_id,
                   AB.followee_id == CB.followee_id))
    # exclude C following A using LEFT JOIN + WHERE IS NULL)
    .outerjoin(CA, and_(AC.followee_id == CA.follower_id,
                        AC.follower_id == CA.followee_id))
    .filter(CA.follower_id == None)
)

for a, b, c in triads:
    print(a, b, c)

如果 User 可以跟在 him/herself 之后,这可能不会那么稳健,在这种情况下,可以使用额外的 where 子句排除此类情况。

Option-2: 另一种干净(非常可读)的方法是确保实际关系存在,但代码生成的 SQL 语句以下可能是达到预期结果的矫枉过正:

# create aliases for each assumed user (A, B, C)
A = aliased(User)
B = aliased(User)
C = aliased(User)
# also aliases to navigate relationship and check same user
Aa = aliased(User)
Bb = aliased(User)

triads = (
    session.query(A, B, C)
    .join(B, A.follows)  # A follows B
    .join(Aa, B.follows).filter(Aa.id == A.id)  # B follows Aa (same as A)
    .join(C, A.follows)  # A follows C
    .join(Bb, C.follows).filter(Bb.id == B.id)  # C follows Bb (same as B)
    .filter(~C.follows.any(User.id == A.id))  # exclude C following A
)

for a, b, c in triads:
    print(a, b, c)