SQLAlchemy: если ты со мной, я с тобой - снова ссылки на M2M - PullRequest
4 голосов
/ 18 декабря 2011

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

Ответы [ 2 ]

2 голосов
/ 19 декабря 2011

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

Несколько замечаний:

  • перешел в состояние join - это очень важно для внешних объединений, иначе результат будет неправильным.
  • результат будет [(User, _code_), ..], где _code_: 0 - нет подключения, 1- друг, 2 - знает меня, 3 - настоящий друг (в обе стороны)
2 голосов
/ 19 декабря 2011

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