В SQLAlchemy может ли параметру `backref` внутри `relationship` быть присвоена переменная?

#python #sqlalchemy

#python #sqlalchemy

Вопрос:

Здесь я использую наследование объединенной таблицы.

 class BaseEntity(Base):
    some_col = Column(String)
    base_relationship = relationship("some_relationship", backref="depends_on_who_inherits_me")

class SubEntity(BaseEntity):
    some_unique_col = Column(String)
  

Поскольку конкретное имя обратной ссылки станет известно только во время выполнения (в данном случае оно должно быть SubEntity , но оно должно быть наследуемым неограниченным количеством подклассов), мне нужно depends_on_who_inherits_me указать переменную, или, более конкретно, имя наследующего подкласса, вместо строки. Таким образом, каждый подкласс будет иметь отношение, ссылающееся на сторонний класс, при этом он будет ссылаться на этот конкретный подкласс по его соответствующему имени.

Однако, поскольку это вне какого-либо метода, я не могу использовать self для гибкой ссылки на экземпляры.

Как реализовать эту идею? Спасибо.

Ответ №1:

Один из способов, которым вы могли бы достичь этого, — это Mixin который использует declared_attr.cascading .

Вот класс mixin:

 class Mixin:

    @declared_attr.cascading
    def related_entity(cls):
        if has_inherited_table(cls):
            return relationship(
                'RelatedEntity',
                backref=cls.__name__.lower(),
                uselist=False
            )
  

cascading Флаг на declared_attr заставит sqlalchemy попытаться отобразить атрибут ‘mixed in’ для каждого класса в иерархии. Или, как указано в документах:

Это модификатор специального назначения, который указывает, что объявленный атрибут на основе столбца или MapperProperty должен быть настроен четко для каждого сопоставленного подкласса в сценарии сопоставленного наследования.

has_inherited_table() Функция позволяет нам определять в миксине, имеем ли мы дело с BaseEntity или подклассом, так что мы добавляем отношения только к подклассам.

Затем миксин наследуется в BaseEntity модели:

 class BaseEntity(Base, Mixin):
    id = sa.Column(sa.Integer, primary_key=True)
    related_id = sa.Column(
        sa.Integer, sa.ForeignKey('relatedentity.id'))
    discriminator = sa.Column(sa.String)

    @declared_attr
    def __mapper_args__(cls):
        if has_inherited_table(cls):
            args = {'polymorphic_identity': cls.__name__.lower()}
        else:
            args = {'polymorphic_on': cls.discriminator}
        return args
  

Как вы упомянули в своем вопросе, что вы используете наследование объединенной таблицы, я определил __mapper_args__ on BaseEntity с помощью @declared_attr метода, чтобы polymorphic_identity также можно было автоматически генерировать из имени класса для подклассов.

Таким образом, при такой конфигурации каждый подкласс BaseEntity будет применять атрибут relationship к RelatedEntity , названный в честь подкласса. Вот полный рабочий пример:

 import sqlalchemy as sa
from sqlalchemy.ext.declarative import (declarative_base, declared_attr,
                                        has_inherited_table)
from sqlalchemy.orm import relationship, sessionmaker


class BaseClass:

    @declared_attr
    def __tablename__(cls):
        return cls.__name__.lower()


Base = declarative_base(cls=BaseClass)
engine = sa.create_engine('sqlite://', echo=False)
Session = sessionmaker(bind=engine)


class Mixin:

    @declared_attr.cascading
    def related_entity(cls):
        if has_inherited_table(cls):
            return relationship(
                'RelatedEntity',
                backref=cls.__name__.lower(),
                uselist=False
            )


class BaseEntity(Base, Mixin):
    id = sa.Column(sa.Integer, primary_key=True)
    related_id = sa.Column(
        sa.Integer, sa.ForeignKey('relatedentity.id'))
    discriminator = sa.Column(sa.String)

    @declared_attr
    def __mapper_args__(cls):
        if has_inherited_table(cls):
            args = {'polymorphic_identity': cls.__name__.lower()}
        else:
            args = {'polymorphic_on': cls.discriminator}
        return args


class RelatedEntity(Base):
    """ Class that is related to all `BaseEntity` subclasses"""
    id = sa.Column(sa.Integer, primary_key=True)


class SubEntity(BaseEntity):
    """ Will generate `RelatedEntity.subentity`"""
    id = sa.Column(sa.Integer, sa.ForeignKey('baseentity.id'),
                   primary_key=True)


class OtherEntity(BaseEntity):
    """ Will generate `RelatedEntity.otherentity`"""
    id = sa.Column(sa.Integer, sa.ForeignKey('baseentity.id'),
                   primary_key=True)


if __name__ == '__main__':
    Base.metadata.drop_all(engine)
    Base.metadata.create_all(engine)
    s = Session()
    rel_inst = RelatedEntity()
    s.add(rel_inst)
    rel_inst.subentity.append(SubEntity())
    rel_inst.otherentity.append(OtherEntity())
    s.commit()
    print(rel_inst.subentity, rel_inst.otherentity)
    # [<__main__.SubEntity object at 0x0000023487D42C18>] [<__main__.OtherEntity object at 0x0000023487D60278>]
  

Причина, по которой мы не можем определить related_entity() declared_attr метод в BaseModel , заключается в том, что SQLAlchemy не будет выполнять каскад, и не будет сгенерированных связей (потому что if has_inherited_table(cls): блок предотвращает BaseModel их создание). Из документов:

Флаг применяется только к использованию declared_attr в декларативных смешанных классах и __abstract__ классах; в настоящее время он не имеет эффекта при непосредственном использовании в сопоставленном классе.

Комментарии:

1. Спасибо за этот подробный ответ. После прочтения документации на declared_attr я все еще озадачен, почему вы используете .cascading внутри Mixin , но не в BaseEntity . Вы упомянули, что это для создания отдельного экземпляра для каждого подкласса. Должны ли эти два случая рассматриваться как равные в этом смысле?

2. Я предоставил обновление для своего ответа, но, короче говоря, SQLAlchemy не обрабатывает два случая одинаково (т. Е. related_entity() Определенные на Mixin и BaseModel не ведут себя одинаково.

3. Пожалуйста, поправьте меня, если я неправильно понимаю: наличие cascading или без него влияет только на подклассы третьего поколения и далее, это не влияет на второе поколение? Из вашего примера SubEntity все еще может наследоваться __tablename__ без cascading настройки.

4. Кроме того, почему наличие отображенной таблицы является надежным индикатором того, является ли она BaseEntity или SubEntity? Не могли бы вы дать небольшой намек здесь? Спасибо!

5.Что касается разницы между __tablename__ и relationship declared_attr , вы читали это? docs.sqlalchemy.org/en/13/orm/extensions/declarative /….