SQLAlchemy:如果你和我在一起,我和你在一起 - 再次引用自我参与的M2M关系

时间:2011-12-18 09:33:07

标签: many-to-many sqlalchemy

我正在尝试在SQLAlchemy中创建像多对多映射这样的社交网络。那就是我有一个Character类和一个character_relations交叉表来链接从Character到Character。到现在为止这很简单:

character_relationships = Table('character_relationships', Base.metadata,
    Column('me_id', Integer, ForeignKey('characters.id', ondelete=True, onupdate=True), nullable=False, primary_key=True),
    Column('other_id', Integer, ForeignKey('characters.id', ondelete=True, onupdate=True), nullable=False, primary_key=True),
    UniqueConstraint('me_id', 'other_id', name='uix_1')
)

class CharacterRelationship(object):

    def __init__(self, me_id, other_id):
        self.me_id = me_id
        self.other_id = other_id

mapper(CharacterRelationship, character_relationships)

class Character(IdMixin, TimestampMixin, Base):
    __tablename__ = "characters"

    name = Column(Unicode, nullable=False)

    friends = relationship(lambda: Character, secondary=character_relationships, 
             primaryjoin=lambda: Character.id==character_relationships.c.me_id,
             secondaryjoin=lambda: Character.id==character_relationships.c.other_id,
             backref=backref('knows_me')
             )

有两种可能的关系:单边和双边。那是我可以拥有的交叉表

CharacterRelationship(me_id=1, other_id=2)
CharacterRelationship(me_id=2, other_id=1)

上面给出的Character.friends参数是关于单一的关系。如何为双边关系添加property / column_property?

这是my_character.real_friends只包含来自CharacterRelationship的条目,如果关系“从两边站立”。

我知道我可以使用像

这样的东西
@property
def real_friends(self):
    return set(my_character.friends) & set(my_character.knows_me)

但是这并不能很好地转换为sql,我希望在数据库级别进行这种集合交叉的巨大加速。但还有更多要实现的目标!

更好的方法是拥有一个像:

这样的最终对象
character.friends.other_character
character.friends.relationship_type = -1 | 0 | 1

其中

  • -1表示我单方面知道其他人,
  • 0 bilateral,
  • 1意味着其他人单方面了解我

您认为有任何理由将此逻辑放在数据库级别吗?如果是的话,你会怎么做? 如果知道,您知道如何将简单的real_friend双边部分放在数据库级别吗?

2 个答案:

答案 0 :(得分:2)

对于real_friends,您可能希望在数据库级别进行查询。看起来应该是这样的:

@property
def real_friends(self):
    char_rels1 = aliased(CharacterRelationship)
    char_rels2 = aliased(CharacterRelationship)
    return DBSession.query(Character).\
        join(char_rels1, char_rels1.other_id == Character.id).\
        filter(char_rels1.me_id == self.id).\
        join(char_rels2, char_rels2.me_id == Character.id).\
        filter(char_rels2.other_id == self.id).all()

您当然可以将此设置为关系。

对于other_character(我假设这些是非共同的朋友),我会做一个类似的查询,但有一个outerjoin。

对于relationship_type,我会在缓存knows_person()时执行以下操作:

def knows_person(self, person):
    return bool(DBSession.query(CharacterRelationship).\
        filter(CharacterRelationship.me_id == self.id).\
        filter(CharacterRelationship.other_id == person.id).\
        first())

def rel_type(self, person):
    knows = self.knows_person(person)
    known = person.knows_person(self)
    if knows and known:
        return 0
    elif knows:
        return -1
    elif known:
        return 1
    return None

答案 1 :(得分:2)

定义了您的关系friendsreal_friends很容易完成,如下所示:

@property
def real_friends(self):
    qry = (Session.object_session(self).query(User).
            join(User.friends, aliased=True).filter(User.id == self.id).
            join(User.knows_me, aliased=True).filter(User.id == self.id)
          )
    return qry.all()

,但这会使两个JOINUser表更加冗余,所以虽然IMO是一个更好的代码,但它可能不如简单{{1}的Jonathan版本使用了JOIN表。

至于你要求的更好的选项:这可以在下面的查询中再次完成(以@ Jonathan的答案作为参考):

CharacterRelationship

很少有事情需要注意:

  • @property def all_friends(self): char_rels1 = aliased(Friendship) char_rels2 = aliased(Friendship) qry = (Session.object_session(self).query(User, case([(char_rels1.id <> None, 1)], else_=0)+case([(char_rels2.id <> None, 2)], else_=0)). outerjoin(char_rels1, and_(char_rels1.friends == User.id, char_rels1.knows_me == self.id)). outerjoin(char_rels2, and_(char_rels2.knows_me == User.id, char_rels2.friends == self.id)). filter(or_(char_rels1.id <> None, char_rels2.id <> None)) # @note: exclude those that are not connected at all ) return qry.all() 移至filter条件 - 这对外连接非常重要,否则结果会出错。
  • 结果将是join,其中[(User, _code_), ..]是:0 - 没有连接,1 - 朋友,2 - 认识我,3 - 真正的朋友(双向)