SQLAlchemy: если ты со мной, я с тобой - снова ссылки на M2M

Я пытаюсь создать социальную сеть, подобную сопоставлению «многие ко многим» в 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, приведенный выше, касается односторонних отношений. Как добавить свойство /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 на уровне базы данных?

4 голоса | спросил Akasha 18 SunEurope/Moscow2011-12-18T13:33:07+04:00Europe/Moscow12bEurope/MoscowSun, 18 Dec 2011 13:33:07 +0400 2011, 13:33:07

2 ответа


0

Для 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 (я предполагаю, что это не общие друзья), я бы сделал аналогичный запрос, но с внешним соединением.

Для отношения тип-отношения я бы сделал следующее, пока кэшировал знает_персона ():

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
ответил Jonathan Ong 19 MonEurope/Moscow2011-12-19T04:25:32+04:00Europe/Moscow12bEurope/MoscowMon, 19 Dec 2011 04:25:32 +0400 2011, 04:25:32
0

Определение ваших отношений 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()

, но это сделает еще два избыточных JOIN к User, поэтому, хотя IMO, это лучший код, он может уступать версии Jonathan, в которой простой JOIN s для таблицы 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_), ..], где _code_: 0 - нет связи, 1 - друг, 2 - знает меня, 3 - настоящий друг (в обе стороны)
ответил van 19 MonEurope/Moscow2011-12-19T16:38:32+04:00Europe/Moscow12bEurope/MoscowMon, 19 Dec 2011 16:38:32 +0400 2011, 16:38:32

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132