使用 mixin 组合映射层次

优质
小牛编辑
143浏览
2023-12-01

当使用 Declarative 样式是在许多类之间共享一些功能,例如一组公共列、一些公共表选项或其他映射属性。标准的Python习惯用法是让类继承自一个包含这些常见特性的超类。

当使用声明性映射时,通过使用mixin类,以及通过增加 registry.generate_base() 方法或 declarative_base() 功能。

下面是一些常见的混合成语的例子:

from sqlalchemy.orm import declarative_mixin
from sqlalchemy.orm import declared_attr

@declarative_mixin
class MyMixin:

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

    __table_args__ = {'mysql_engine': 'InnoDB'}
    __mapper_args__= {'always_refresh': True}

    id =  Column(Integer, primary_key=True)

class MyModel(MyMixin, Base):
    name = Column(String(1000))

在上面的什么地方,班级 MyModel 将包含一个“id”列作为主键,a __tablename__ 从类本身的名称派生的属性,以及 __table_args____mapper_args__ 定义为 MyMixin 混入类。

小技巧

使用 declarative_mixin() 类修饰器将特定类标记为提供将SQLAlChemy声明性赋值作为其他类的混合提供的服务。此修饰符当前只需要向 Mypy plugin 这个类应该作为声明性映射的一部分来处理。

关于是否 MyMixin 先于 Base 或者没有。标准的python方法解析规则适用,上面的示例也适用于::

class MyModel(Base, MyMixin):
    name = Column(String(1000))

这是因为 Base 这里没有定义任何变量 MyMixin 定义,即 __tablename____table_args__id 等,如果 Base 如果定义了相同名称的属性,则放在继承列表中第一个的类将决定在新定义的类上使用哪个属性。

扩大基地

除了使用纯混合之外,本节中的大多数技术还可以应用于基类本身,用于应用于从特定基派生的所有类的模式。这是通过使用 cls 论证 declarative_base() 功能:

from sqlalchemy.orm import declared_attr

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

    __table_args__ = {'mysql_engine': 'InnoDB'}

    id =  Column(Integer, primary_key=True)

from sqlalchemy.orm import declarative_base

Base = declarative_base(cls=Base)

class MyModel(Base):
    name = Column(String(1000))

在上面, MyModel 以及所有其他类 Base 将有一个从类名派生的表名, id 主键列,以及用于MySQL的“innodb”引擎。

柱状混合

在mixin上指定列的最基本方法是通过简单声明:

@declarative_mixin
class TimestampMixin:
    created_at = Column(DateTime, default=func.now())

class MyModel(TimestampMixin, Base):
    __tablename__ = 'test'

    id =  Column(Integer, primary_key=True)
    name = Column(String(1000))

在上面的位置,包括 TimestampMixin 也将有一列 created_at 它将时间戳应用于所有行插入。

熟悉SQLAlchemy表达式语言的人知道子句元素的对象标识定义了它们在模式中的角色。二 Table 物体 ab 可能都有一个列名为 id 但是这些区别的方式是 a.c.idb.c.id 是两个不同的python对象,引用它们的父表 ab 分别。

对于mixin列,似乎只有一个 Column 对象是显式创建的,但最终 created_at 对于每个单独的目标类,上面的列必须作为一个不同的python对象存在。为了实现这一点,声明性扩展创建了 copy 每一个 Column 在被检测为mixin的类上遇到的对象。

此复制机制仅限于没有外键的简单列,作为 ForeignKey 它本身包含对在此级别上无法正确重新创建的列的引用。对于具有外键的列以及需要目标显式上下文的各种映射器级别构造,可以使用 declared_attr 提供了decorator,以便可以将许多类通用的模式定义为可调用的:

from sqlalchemy.orm import declared_attr

@declarative_mixin
class ReferenceAddressMixin:
    @declared_attr
    def address_id(cls):
        return Column(Integer, ForeignKey('address.id'))

class User(ReferenceAddressMixin, Base):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True)

在上面的地方, address_id 类级可调用在 User 类,声明性扩展可以使用 Column 方法返回的对象,不需要复制它。

列生成者 declared_attr 也可以被引用 __mapper_args__ 在一定程度上,目前由 polymorphic_onversion_id_col ;声明性扩展将在类构造时解析它们:

@declarative_mixin
class MyMixin:
    @declared_attr
    def type_(cls):
        return Column(String(50))

    __mapper_args__= {'polymorphic_on':type_}

class MyModel(MyMixin, Base):
    __tablename__='test'
    id =  Column(Integer, primary_key=True)

混入关系

关系创建者 relationship() 提供声明性mixin类,这些类专用于 declared_attr 方法,消除复制关系及其可能的列绑定内容时可能出现的任何模糊性。下面是一个例子,它结合了一个外键列和一个关系,从而使两个类 FooBar 两者都可以配置为通过多对一引用公共目标类::

@declarative_mixin
class RefTargetMixin:
    @declared_attr
    def target_id(cls):
        return Column('target_id', ForeignKey('target.id'))

    @declared_attr
    def target(cls):
        return relationship("Target")

class Foo(RefTargetMixin, Base):
    __tablename__ = 'foo'
    id = Column(Integer, primary_key=True)

class Bar(RefTargetMixin, Base):
    __tablename__ = 'bar'
    id = Column(Integer, primary_key=True)

class Target(Base):
    __tablename__ = 'target'
    id = Column(Integer, primary_key=True)

使用高级关系参数(例如 primaryjoin 等)

relationship() 需要显式primaryjoin、order_by等的定义。除了最简单的情况外,所有表达式都应使用 晚绑定 这些参数的形式,即使用字符串形式或函数/lambda。原因是 Column 要使用配置的对象 @declared_attr 其他人不可用 @declared_attr 属性;而方法将工作并返回新的 Column 对象,它们不是 Column 声明性将在调用方法时使用的对象,因此使用 不同的 Column 物体。

规范示例是依赖于另一个混合在列中的PrimaryJoin条件:

@declarative_mixin
class RefTargetMixin:
      @declared_attr
      def target_id(cls):
          return Column('target_id', ForeignKey('target.id'))

      @declared_attr
      def target(cls):
          return relationship(Target,
              primaryjoin=Target.id==cls.target_id   # this is *incorrect*
          )

使用上面的mixin映射类,我们将得到如下错误:

sqlalchemy.exc.InvalidRequestError: this ForeignKey's parent column is not
yet associated with a Table.

这是因为 target_id Column 我们已经要求 target() 方法不同 Column 这个声明性实际上将映射到我们的表。

使用lambda解决上述问题:

@declarative_mixin
class RefTargetMixin:
    @declared_attr
    def target_id(cls):
        return Column('target_id', ForeignKey('target.id'))

    @declared_attr
    def target(cls):
        return relationship(Target,
            primaryjoin=lambda: Target.id==cls.target_id
        )

或者,字符串形式(最终生成lambda)::

@declarative_mixin
class RefTargetMixin:
    @declared_attr
    def target_id(cls):
        return Column('target_id', ForeignKey('target.id'))

    @declared_attr
    def target(cls):
        return relationship("Target",
            primaryjoin="Target.id==%s.target_id" % cls.__name__
        )

参见

关系论据的后期评估

在deferred()、column_property()和其他mapperproperty类中混合

喜欢 relationship() ,所有 MapperProperty 子类,如 deferred()column_property() 等等。最终涉及到对列的引用,因此,当与声明性混合使用时,具有 declared_attr 要求不依赖复制:

@declarative_mixin
class SomethingMixin:

    @declared_attr
    def dprop(cls):
        return deferred(Column(Integer))

class Something(SomethingMixin, Base):
    __tablename__ = "something"

这个 column_property() 或者其他构造可以引用mixin中的其他列。这些都是提前复制的 declared_attr 调用:

@declarative_mixin
class SomethingMixin:
    x = Column(Integer)

    y = Column(Integer)

    @declared_attr
    def x_plus_y(cls):
        return column_property(cls.x + cls.y)

在 1.0.0 版更改: 将mixin列复制到最终映射的类中,以便 declared_attr 方法可以访问将要映射的实际列。

混入关联代理和其他属性

mixin可以指定用户定义的属性以及其他扩展单元,例如 association_proxy() . 用法 declared_attr 在那些必须针对目标子类专门定制属性的情况下是必需的。例如,当构造多个 association_proxy() 属性,每个属性都针对不同类型的子对象。下面是一个 association_proxy() 向实现类提供字符串值的标量列表的mixin示例:

from sqlalchemy import Column, Integer, ForeignKey, String
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import declarative_mixin
from sqlalchemy.orm import declared_attr
from sqlalchemy.orm import relationship

Base = declarative_base()

@declarative_mixin
class HasStringCollection:
    @declared_attr
    def _strings(cls):
        class StringAttribute(Base):
            __tablename__ = cls.string_table_name
            id = Column(Integer, primary_key=True)
            value = Column(String(50), nullable=False)
            parent_id = Column(Integer,
                            ForeignKey('%s.id' % cls.__tablename__),
                            nullable=False)
            def __init__(self, value):
                self.value = value

        return relationship(StringAttribute)

    @declared_attr
    def strings(cls):
        return association_proxy('_strings', 'value')

class TypeA(HasStringCollection, Base):
    __tablename__ = 'type_a'
    string_table_name = 'type_a_strings'
    id = Column(Integer(), primary_key=True)

class TypeB(HasStringCollection, Base):
    __tablename__ = 'type_b'
    string_table_name = 'type_b_strings'
    id = Column(Integer(), primary_key=True)

上面, HasStringCollection mixin生成 relationship() 它引用了一个新生成的类 StringAttribute . 这个 StringAttribute 类是用它自己的 Table 定义,它是使用 HasStringCollection 混合蛋白。它还产生一个 association_proxy() 对象的代理引用 strings 属性到 value 每个的属性 StringAttribute 实例。

TypeATypeB 无法在给定构造函数参数的情况下实例化 strings ,字符串列表:

ta = TypeA(strings=['foo', 'bar'])
tb = TypeB(strings=['bat', 'bar'])

此列表将生成 StringAttribute 对象,这些对象被持久化到一个表中,该表是 type_a_stringstype_b_strings 表:

>>> print(ta._strings)
[<__main__.StringAttribute object at 0x10151cd90>,
    <__main__.StringAttribute object at 0x10151ce10>]

在构造 association_proxy() , the declared_attr 必须使用decorator,以便 association_proxy() 对象是为每个 TypeATypeB 类。

用mixin控制表继承

这个 __tablename__ 属性可用于提供一个函数,该函数将确定继承层次结构中每个类所用表的名称,以及一个类是否具有自己的不同表。

这是通过使用 declared_attr 与名为 __tablename__() . 声明性将始终调用 declared_attr 对于特殊的名字 __tablename____mapper_args____table_args__ 功能 对于层次结构中的每个映射类,除非在子类中重写 . 因此,函数需要单独接收每个类,并为每个类提供正确的答案。

例如,要创建一个mixin,该mixin基于类名为每个类提供一个简单的表名:

from sqlalchemy.orm import declarative_mixin
from sqlalchemy.orm import declared_attr

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

class Person(Tablename, Base):
    id = Column(Integer, primary_key=True)
    discriminator = Column('type', String(50))
    __mapper_args__ = {'polymorphic_on': discriminator}

class Engineer(Person):
    __tablename__ = None
    __mapper_args__ = {'polymorphic_identity': 'engineer'}
    primary_language = Column(String(50))

或者,我们可以修改 __tablename__ 要返回的函数 None 对于子类,使用 has_inherited_table() . 这会影响到那些子类被映射为针对父类的单表继承:

from sqlalchemy.orm import declarative_mixin
from sqlalchemy.orm import declared_attr
from sqlalchemy.orm import has_inherited_table

@declarative_mixin
class Tablename:
    @declared_attr
    def __tablename__(cls):
        if has_inherited_table(cls):
            return None
        return cls.__name__.lower()

class Person(Tablename, Base):
    id = Column(Integer, primary_key=True)
    discriminator = Column('type', String(50))
    __mapper_args__ = {'polymorphic_on': discriminator}

class Engineer(Person):
    primary_language = Column(String(50))
    __mapper_args__ = {'polymorphic_identity': 'engineer'}

在继承场景中混入列

与如何 __tablename__ 和其他特殊名称一起使用时处理 declared_attr ,当我们在列和属性(例如关系、列属性等)中混合时,将为 仅基类 在层次结构中。下面,只有 Person 类将接收名为 id ;映射将失败 Engineer ,未给定主键::

@declarative_mixin
class HasId:
    @declared_attr
    def id(cls):
        return Column('id', Integer, primary_key=True)

class Person(HasId, Base):
    __tablename__ = 'person'
    discriminator = Column('type', String(50))
    __mapper_args__ = {'polymorphic_on': discriminator}

class Engineer(Person):
    __tablename__ = 'engineer'
    primary_language = Column(String(50))
    __mapper_args__ = {'polymorphic_identity': 'engineer'}

在联合表继承中,我们通常希望每个子类上都有不同名称的列。但是在这种情况下,我们可能希望 id 每个表上的列,并让它们通过外键相互引用。我们可以通过使用 declared_attr.cascading 修饰符,指示应调用函数 对于层次结构中的每个类几乎 (见下面的警告)与 __tablename__ ::

@declarative_mixin
class HasIdMixin:
    @declared_attr.cascading
    def id(cls):
        if has_inherited_table(cls):
            return Column(ForeignKey('person.id'), primary_key=True)
        else:
            return Column(Integer, primary_key=True)

class Person(HasIdMixin, Base):
    __tablename__ = 'person'
    discriminator = Column('type', String(50))
    __mapper_args__ = {'polymorphic_on': discriminator}

class Engineer(Person):
    __tablename__ = 'engineer'
    primary_language = Column(String(50))
    __mapper_args__ = {'polymorphic_identity': 'engineer'}

警告

这个 declared_attr.cascading 功能当前有 not 允许子类用不同的函数或值重写属性。这是目前在如何 @declared_attr 已解决,如果检测到此情况,将发出警告。这个限制是 not 存在特殊属性名称,例如 __tablename__ ,其内部解决方式与 declared_attr.cascading .

1.0.0 新版功能: 补充 declared_attr.cascading .

从多个mixin组合表/映射器参数

如果是 __table_args____mapper_args__ 通过声明性混合指定,您可能希望将来自多个混合的一些参数与希望在类本身上定义的参数组合在一起。这个 declared_attr 这里可以使用decorator创建从多个集合中提取的用户定义的排序规则例程::

from sqlalchemy.orm import declarative_mixin
from sqlalchemy.orm import declared_attr

@declarative_mixin
class MySQLSettings:
    __table_args__ = {'mysql_engine':'InnoDB'}

@declarative_mixin
class MyOtherMixin:
    __table_args__ = {'info':'foo'}

class MyModel(MySQLSettings, MyOtherMixin, Base):
    __tablename__='my_model'

    @declared_attr
    def __table_args__(cls):
        args = dict()
        args.update(MySQLSettings.__table_args__)
        args.update(MyOtherMixin.__table_args__)
        return args

    id =  Column(Integer, primary_key=True)

使用mixin创建索引

定义一个命名的、可能是多列的 Index 它适用于从mixin派生的所有表,使用“inline”形式 Index 并将其作为 __table_args__ ::

@declarative_mixin
class MyMixin:
    a =  Column(Integer)
    b =  Column(Integer)

    @declared_attr
    def __table_args__(cls):
        return (Index('test_idx_%s' % cls.__tablename__, 'a', 'b'),)

class MyModel(MyMixin, Base):
    __tablename__ = 'atable'
    c =  Column(Integer,primary_key=True)