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

发布于 2024-12-21 16:12:18 字数 1943 浏览 2 评论 0原文

我正在尝试在 SQLAlchemy 中创建一个类似于多对多映射的社交网络。也就是说,我有一个字符类和一个字符关系交叉表来从字符链接到字符。到目前为止,这很容易:

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 参数是关于单层关系的。如何为双边关系添加属性/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 双边,
  • 1 表示其他人单方面认识我

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

I'm trying to create a social-network like many-to-many mapping in SQLAlchemy. That is I have a Character class and a character_relations cross table to link from Character to Character. Until now this is easy:

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')
             )

There are two possible relationships: unilateral and bilateral. That is in the cross table I can have

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

The Character.friends argument as given above is about unilater relationships. How can one add a property/column_property for bilateral relationships?

That is my_character.real_friends contains only entries from CharacterRelationship if the relationship "stands from both sides".

I know that I could use something like

@property
def real_friends(self):
    return set(my_character.friends) & set(my_character.knows_me)

but this doesn't translate well to sql, and I would expect a huge speedup doing this set intersection at the database level. But there is even more to achieve!

Even better would be to have a final object like:

character.friends.other_character
character.friends.relationship_type = -1 | 0 | 1

where

  • -1 means I know other unilaterally,
  • 0 bilateral,
  • 1 means other knows me unilaterally

Do you see any reason to put this logic at the database level? If yes, how would you do it?
If know, do you know how to put the simple real_friend bilateral part at the database level?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

紫罗兰の梦幻 2024-12-28 16:12:18

对于 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 (我假设这些是非共同的朋友),我会执行类似的查询,但使用外连接。

对于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

For real_friends, you would want to query this at the database level. It should look something like this:

@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()

You can of course set this as a relationship.

For other_character (I'm assuming these are non-mutual friends), I would do a similar query but with an outerjoin.

For relationship_type, I would do the following while caching 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
依 靠 2024-12-28 16:12:18

定义了您的关系 friends 后,real_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()

,但这会给 User< 带来两个多余的 JOIN 。 /code> 表,所以虽然在我看来,它是一个更好的代码,但它可能不如 Jonathan 的版本,其中使用简单的 JOINCharacterRelationship 表。

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

@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()

需要注意的事项:

  • 过滤器被移至join条件——这对于外连接非常重要,否则结果将是错误的。
  • 结果将是 [(User, _code_), ..],其中 _code_ 为:0 - 未连接、1 - 朋友、2 - 认识我、3 - 真实朋友(双向)

Having your relationship friends defined, real_friends is easily done as following:

@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()

, but this will make two more redundant JOINs to User table, so although IMO, it is a better code, it might be inferior to the version of Jonathan where simple JOINs to the CharacterRelationship table are used.

As for the even better option you ask for: this can again be done in a query like below (taken @Jonathan's answer as reference):

@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()

Few things to note:

  • the filter is moved to the join condition - this is very important for outer joins, or else the result would be wrong.
  • the result would be [(User, _code_), ..], where _code_ is: 0 - no connected, 1 - friend, 2 - knows me, 3 - real friend (both ways)
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文