Самореферентная связь внешнего ключа в декларативном классе миксинов SQLAlchemy с manifest_attr

У меня есть класс-миксин, который я определяю в начале приложения SQLAlchemy, а затем наследую практически для каждой используемой мной декларативной модели.

class Owned(object):

    @declared_attr
    def created_by_id(cls):
        return Column(Integer, ForeignKey("accounts.id"),
            nullable = True)

    @declared_attr
    def created_by(cls):
        return relationship("Account", foreign_keys = cls.created_by_id)

    @declared_attr
    def updated_by_id(cls):
        return Column(Integer, ForeignKey("accounts.id"),
            nullable = True)

    @declared_attr
    def updated_by(cls):
        return relationship("Account", foreign_keys = cls.updated_by_id)

Это хорошо работает для большинства предполагаемых случаев использования.

class Thing(Owned, Base): # Base is from SQLAlchemy's declarative_base()
    pass

account = session.query(Account).first()

thing = Thing(created_by = account, updated_by = account)

session.add(thing)
session.commit()
session.refresh(thing)

assert thing.created_by == account # pass
assert thing.updated_by == account # pass

Однако я получаю неожиданное поведение, когда определяю сам Account как наследуемый от Owned.

class Account(Owned, Base):
    pass

account_old = session.query(Account).first()

account_new = Account(created_by = account_old, updated_by = account_old)

session.add(account_new)
session.commit()
session.refresh(account_new)

assert account_new.created_by_id == account_old.id # pass
assert account_new.updated_by_id == account_old.id # pass

# BUT!

assert account_new.created_by == account_old # fail
assert account_new.updated_by == account_old # fail

account_new.created_by # []
account_new.updated_by # []

Я вижу, что в этом случае я превратил created_by_id и updated_by_id в самореферентные внешние ключи. Однако я не понимаю, почему SQLAlchemy не заполняет связанные столбцы relationship ожидаемыми экземплярами Account.

Что я делаю неправильно?


person snoopy91    schedule 05.06.2018    source источник


Ответы (1)


В отношении списка смежности "направление" по умолчанию предполагается быть один ко многим. Использование директивы remote_side устанавливает, что отношения многие-к-одному, что вам нужно:

def _create_relationship(cls, foreign_keys):
    kwgs = {}
    # Bit of a chicken or egg situation:
    if cls.__name__ == "Account":
        kwgs["remote_side"] = [cls.id]

    return relationship("Account", foreign_keys=foreign_keys, **kwgs)


class Owned:

    @declared_attr
    def created_by_id(cls):
        return Column(Integer, ForeignKey("accounts.id"),
            nullable = True)

    @declared_attr
    def created_by(cls):
        return _create_relationship(cls, cls.created_by_id)

    @declared_attr
    def updated_by_id(cls):
        return Column(Integer, ForeignKey("accounts.id"),
            nullable = True)

    @declared_attr
    def updated_by(cls):
        return _create_relationship(cls, cls.updated_by_id)
person Ilja Everilä    schedule 05.06.2018
comment
Большое спасибо. Я последовал вашему подходу и получил эту ошибку: sqlalchemy.exc.ArgumentError: Relationship Account.created_by could not determine any unambiguous local/remote column pairs based on join condition and remote_side arguments. Consider using the remote() annotation to accurately mark those elements of the join condition that are on the remote side of the relationship. Нужно ли указывать соединение вручную? Или foreign_keys распознается только в одном направлении? - person snoopy91; 05.06.2018
comment
Хм, я думаю, что немного больше контекста было бы в порядке. Учитывая 2 атрибута отношений, миксин работал в моих локальных тестах и ​​не должен требовать ручного соединения. - person Ilja Everilä; 05.06.2018