Skip to content

遗产

在各种类型的 ORM 模型继承中,ormar 目前支持其中两种:

  • 混入
  • 具体表继承(父级设置为abstract=True)

继承类型

不同类型继承的简短摘要:

  • Mixins [支持] - 不要子类化 ormar.Model,只需定义稍后在不同模型上使用的字段(例如每个模型上的created_date 和updated_date),只有实际模型创建表,但添加 mixins 中的字段
  • 具体表继承 [SUPPORTED] - 意味着父级被标记为抽象,每个子级都有自己的表,其中包含来自父级的列和自己的子级列,有点类似于 Mixins,但父级也是一个模型
  • 单表继承[不支持] - 意味着仅创建一个表,其中的字段是父模型和所有子模型的组合/总和,但子模型仅使用数据库中列的子集(所有父模型和自己的列,跳过其他子模型)的)
  • 多/连接表继承[不支持] - 意味着部分列保存在父模型上,部分保存在子模型上,它们通过一对一的关系相互连接,并且在后台操作两个模型立刻
  • 代理模型[不支持] - 意味着只有父级有一个实际的表,子级只是添加方法、修改设置等。

混入

要使用 Mixins,只需定义一个不是从 ormar.Model 继承的类,而是将 ormar.Fields 定义为类变量。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
base_ormar_config = ormar.OrmarConfig(
    database=databases.Database(DATABASE_URL),
    metadata=sqlalchemy.MetaData(),
    engine=sqlalchemy.create_engine(DATABASE_URL),
)


# a mixin defines the fields but is a normal python class 
class AuditMixin:
    created_by: str = ormar.String(max_length=100)
    updated_by: str = ormar.String(max_length=100, default="Sam")


class DateFieldsMixins:
    created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now)
    updated_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now)


# a models can inherit from one or more mixins
class Category(ormar.Model, DateFieldsMixins, AuditMixin):
    ormar_config = base_ormar_config.copy(tablename="categories")

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=50, unique=True, index=True)
    code: int = ormar.Integer()

!!!tip 请注意,Mixin 不是模型,因此您仍然需要继承 ormar.Model 并在最终模型中定义 ormar_config 字段。

上面的 Category 类将有四个附加字段:created_date、updated_date、created_by 和 Updated_by。

只会为模型 Category(类别)创建一张表,其中 Category 类字段与所有 Mixins 字段相结合。

请注意,类名中的 Mixin 是可选的,但这是一个很好的 Python 实践。

具体表继承

在概念上,具体表继承与 Mixins 非常相似,但使用实际的 ormar.Models 作为基类。

!!!警告注意,基类在 ormar_config 对象中设置了 Abstract=True,如果您尝试从非抽象标记类继承,将会引发 ModelDefinitionError 。

由于这个抽象模型永远不会被初始化,您可以跳过它的 ormar_config 定义中的元数据和数据库。

但是如果您提供它 - 它将被继承,这样您就不必在最终/具体类中提供元数据和数据库

请注意,如果需要,您始终可以在子/具体类中覆盖它。

更重要的是,继承链中的至少一个类必须提供数据库和元数据 - 否则将引发错误。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# note that base classes have abstract=True
# since this model will never be initialized you can skip metadata and database
class AuditModel(ormar.Model):
    ormar_config = base_ormar_config.copy(abstract=True)

    created_by: str = ormar.String(max_length=100)
    updated_by: str = ormar.String(max_length=100, default="Sam")


# but if you provide it it will be inherited - DRY (Don't Repeat Yourself) in action
class DateFieldsModel(ormar.Model):
    ormar_config = base_ormar_config.copy(
        abstract=True,
        metadata=metadata,
        database=db,
    )

    created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now)
    updated_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now)


# that way you do not have to provide metadata and databases in concrete class
class Category(DateFieldsModel, AuditModel):
    ormar_config = base_ormar_config.copy(tablename="categories")

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=50, unique=True, index=True)
    code: int = ormar.Integer()

继承的选项/设置列表如下:元数据、数据库和约束。

当然,除此之外,基类中的所有字段都会在最终模型的具体表中组合和创建。

!!!tip 请注意,您不必在最终类中提供abstarct=False - 这是不继承的默认设置。

重新定义子类中的字段

请注意,您可以像普通的 Python 类继承一样重新定义以前创建的字段。

每当您定义具有相同名称和新定义的字段时,它将完全替换以前定义的字段。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# base class
class DateFieldsModel(ormar.Model):
    ormar_config = OrmarConfig(
        abstract=True,
        metadata=metadata,
        database=db,
        # note that UniqueColumns need sqlalchemy db columns names not the ormar ones
        constraints=[ormar.UniqueColumns("creation_date", "modification_date")]
    )

    created_date: datetime.datetime = ormar.DateTime(
        default=datetime.datetime.now, name="creation_date"
    )
    updated_date: datetime.datetime = ormar.DateTime(
        default=datetime.datetime.now, name="modification_date"
    )


class RedefinedField(DateFieldsModel):
    ormar_config = OrmarConfig(
        tablename="redefines",
        metadata=metadata,
        database=db,
    )

    id: int = ormar.Integer(primary_key=True)
    # here the created_date is replaced by the String field
    created_date: str = ormar.String(max_length=200, name="creation_date")


# you can verify that the final field is correctly declared and created
changed_field = RedefinedField.ormar_config.model_fields["created_date"]
assert changed_field.default is None
assert changed_field.alias == "creation_date"
assert any(x.name == "creation_date" for x in RedefinedField.ormar_config.table.columns)
assert isinstance(
    RedefinedField.ormar_config.table.columns["creation_date"].type,
    sqlalchemy.sql.sqltypes.String,
)

!!!警告如果您使用列名声明 UniqueColumns 约束,则最终模型必须声明具有相同名称的列。否则,将引发 ModelDefinitionError。

1
2
3
4
5
6
So in example above if you do not provide `name` for `created_date` in `RedefinedField` model
ormar will complain.

`created_date: str = ormar.String(max_length=200) # exception`

`created_date: str = ormar.String(max_length=200, name="creation_date2") # exception`

继承关系

您可以在继承的每个步骤中声明关系,无论是在父类还是子类中。

当您在子模型级别定义关系时,它会覆盖父模型中定义的关系(如果使用相同的字段名称),或者如果您定义新关系,则只能由该子模型访问。

继承关系时,您始终需要注意 related_name 参数,当您定义继承相同关系的多个子类时,该参数在相关模型中必须是唯一的。

如果您不提供 related_name 参数,ormar 会为您计算。这适用于继承,因为所有子模型都必须具有不同的类名称,这些名称用于计算默认的 related_name (class.name.lower()+'s')。

但是,如果您提供 related_name,则该名称无法在所有子模型中重用,因为它们会在相关模型端相互覆盖。

因此,您有两种选择:

  • 重新定义子模型中的关系字段并手动提供不同的 related_name 参数
  • 让 ormar 处理 -> 自动调整的 related_name 为:原始 related_name + "_" + 子模型表名称

这听起来可能很复杂,但让我们看一下下面的例子:

外键关系

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# normal model used in relation
class Person(ormar.Model):
    ormar_config = base_ormar_config.copy()

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=100)


# parent model - needs to be abstract
class Car(ormar.Model):
    ormar_config = base_ormar_config.copy(abstract=True)

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=50)
    owner: Person = ormar.ForeignKey(Person)
    # note that we refer to the Person model again so we **have to** provide related_name
    co_owner: Person = ormar.ForeignKey(Person, related_name="coowned")
    created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now)


class Truck(Car):
    ormar_config = base_ormar_config.copy()

    max_capacity: int = ormar.Integer()


class Bus(Car):
    ormar_config = base_ormar_config.copy(tablename="buses")

    max_persons: int = ormar.Integer()

现在,当您检查 Person 模型上的字段时,您将得到:

1
2
3
4
5
6
7
8
9
Person.ormar_config.model_fields
"""
{'id': <class 'ormar.fields.model_fields.Integer'>, 
'name': <class 'ormar.fields.model_fields.String'>, 
'trucks': <class 'ormar.fields.foreign_key.ForeignKey'>, 
'coowned_trucks': <class 'ormar.fields.foreign_key.ForeignKey'>, 
'buss': <class 'ormar.fields.foreign_key.ForeignKey'>, 
'coowned_buses': <class 'ormar.fields.foreign_key.ForeignKey'>}
"""

请注意您如何拥有卡车和公共汽车字段,这些字段会导致此人拥有的卡车和公共汽车类别。没有 related_name 参数,因此使用默认名称。

同时共同拥有的汽车需要通过coowned_trucks和coowned_buses引用。 Ormar 附加了取自子模型表名称的 _trucks 和 _buses 后缀。

看起来不错,但是自有卡车的默认名称是好的(卡车),但是公共汽车很难看,那么我们如何更改它呢?

解决方案非常简单 - 只需重新定义 Bus 类中的字段并提供不同的 related_name 参数即可。

1
2
3
4
5
6
7
# rest of the above example remains the same
class Bus(Car):
    ormar_config = base_ormar_config.copy(tablename="buses")

    # new field that changes the related_name
    owner: Person = ormar.ForeignKey(Person, related_name="buses")
    max_persons: int = ormar.Integer()

现在柱子看起来好多了。

1
2
3
4
5
6
7
8
9
Person.ormar_config.model_fields
"""
{'id': <class 'ormar.fields.model_fields.Integer'>, 
'name': <class 'ormar.fields.model_fields.String'>, 
'trucks': <class 'ormar.fields.foreign_key.ForeignKey'>, 
'coowned_trucks': <class 'ormar.fields.foreign_key.ForeignKey'>, 
'buses': <class 'ormar.fields.foreign_key.ForeignKey'>, 
'coowned_buses': <class 'ormar.fields.foreign_key.ForeignKey'>}
"""

!!!注意 您还可以为所有者字段提供 related_name,这样就会添加正确的后缀。

1
2
3
`owner: Person = ormar.ForeignKey(Person, related_name="owned")`

and model fields for Person owned cars would become `owned_trucks` and `owned_buses`.

多对多关系

类似地,您可以从声明了 ManyToMany 关系的模型继承,但有一个实质性的区别 - Through 模型。

由于 Through 模型将能够保存其他字段,并且现在它仅链接两个表(来自和到一个表),因此继承 m2m 关系字段的每个子项都必须具有单独的 Through 模型。

当然,您可以覆盖每个子模型中的关系,但这需要额外的代码并破坏了整个继承的意义。如果您同意默认命名约定,Ormar 将为您处理此问题,如果需要,您可以随时在子项中手动覆盖该约定。

再次,让我们看一下示例以更容易地掌握概念。

我们将修改上面描述的前面的示例,以使用 co_owners 的 m2m 关系。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# person remain the same as above
class Person(ormar.Model):
    ormar_config = base_ormar_config.copy()

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=100)

# new through model between Person and Car2
class PersonsCar(ormar.Model):
    ormar_config = base_ormar_config.copy(tablename="cars_x_persons")

# note how co_owners is now ManyToMany relation
class Car2(ormar.Model):
    ormar_config = base_ormar_config.copy(abstract=True)

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=50)
    # note the related_name - needs to be unique across Person
    # model, regardless of how many different models leads to Person
    owner: Person = ormar.ForeignKey(Person, related_name="owned")
    co_owners: List[Person] = ormar.ManyToMany(
        Person, through=PersonsCar, related_name="coowned"
    )
    created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now)


# child models define only additional Fields
class Truck2(Car2):
    ormar_config = base_ormar_config.copy(tablename="trucks2")

    max_capacity: int = ormar.Integer()


class Bus2(Car2):
    ormar_config = base_ormar_config.copy(tablename="buses2")

    max_persons: int = ormar.Integer()

Ormar 自动修改字段的 related_name 以包含子模型的表名称。默认名称是原始的 related_name + '_' + 子表名称。

这样对于 Truck2 类,定义的关系 owner: Person = ormar.ForeignKey(Person, related_name="owned") 成为owned_trucks2

您可以通过检查人员模型上存在的字段列表来验证名称。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Person.ormar_config.model_fields
{
# note how all relation fields need to be unique on Person
# regardless if autogenerated or manually overwritten
'id': <class 'ormar.fields.model_fields.Integer'>, 
'name': <class 'ormar.fields.model_fields.String'>, 
# note that we expanded on previous example so all 'old' fields are here
'trucks': <class 'ormar.fields.foreign_key.ForeignKey'>, 
'coowned_trucks': <class 'ormar.fields.foreign_key.ForeignKey'>, 
'buses': <class 'ormar.fields.foreign_key.ForeignKey'>, 
'coowned_buses': <class 'ormar.fields.foreign_key.ForeignKey'>, 
# newly defined related fields
'owned_trucks2': <class 'ormar.fields.foreign_key.ForeignKey'>, 
'coowned_trucks2': <class 'abc.ManyToMany'>, 
'owned_buses2': <class 'ormar.fields.foreign_key.ForeignKey'>, 
'coowned_buses2': <class 'abc.ManyToMany'>
}

但这还不是全部。它是 ormar 的内部结构,但会影响数据库中的数据结构,因此让我们检查 Bus2 和 Truck2 模型的直通模型。

1
2
3
4
5
6
7
8
9
Bus2.ormar_config.model_fields['co_owners'].through
<class 'abc.PersonsCarBus2'>
Bus2.ormar_config.model_fields['co_owners'].through.ormar_config.tablename
'cars_x_persons_buses2'

Truck2.ormar_config.model_fields['co_owners'].through
<class 'abc.PersonsCarTruck2'>
Truck2.ormar_config.model_fields['co_owners'].through.ormar_config.tablename
'cars_x_persons_trucks2'

正如您在上面看到的,ormar 为每个子类克隆了 Through 模型,并在末尾添加了子类名称,同时更改克隆字段的表名称,使用子级的表名称。

请注意,原始模型不仅没有被使用,而且该模型的表也从元数据中删除:

1
2
3
Bus2.ormar_config.metadata.tables.keys()
dict_keys(['test_date_models', 'categories', 'subjects', 'persons', 'trucks', 'buses', 
           'cars_x_persons_trucks2', 'trucks2', 'cars_x_persons_buses2', 'buses2'])

因此请注意,如果您一路引入继承并将模型转换为抽象父模型,那么如果不小心,您可能会丢失表中的数据。

!!!note 请注意,从未使用 Through 模型的原始表名称和模型名称。仅创建和使用克隆的模型表。

!!!警告 请注意,定义了 ManyToMany 关系的模型的每个子类都会生成一个新的 Through 模型,这也意味着新的数据库表。

1
2
That means that each time you define a Child model you need to either manually create
the table in the database, or run a migration (with alembic).

排除父字段

Ormar 允许您跳过继承模型中来自父模型的某些字段。

!!!Note 请注意,可以通过将模型拆分为更抽象的模型和 mixin 来实现相同的行为 - 这是正常情况下的首选方式。

要跳过子模型中的某些字段,请列出要跳过的所有字段 model.ormar_config.exclude_parent_fields 参数如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
base_ormar_config = OrmarConfig(
    metadata=sa.MetaData(),
    database=databases.Database(DATABASE_URL),
)


class AuditModel(ormar.Model):
    ormar_config = base_ormar_config.copy(abstract=True)

    created_by: str = ormar.String(max_length=100)
    updated_by: str = ormar.String(max_length=100, default="Sam")


class DateFieldsModel(ormar.Model):
    ormar_config = base_ormar_config.copy(abstract=True)

    created_date: datetime.datetime = ormar.DateTime(
        default=datetime.datetime.now, name="creation_date"
    )
    updated_date: datetime.datetime = ormar.DateTime(
        default=datetime.datetime.now, name="modification_date"
    )


class Category(DateFieldsModel, AuditModel):
    ormar_config = base_ormar_config.copy(
        tablename="categories",
        # set fields that should be skipped
        exclude_parent_fields=["updated_by", "updated_date"],
    )

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=50, unique=True, index=True)
    code: int = ormar.Integer()

# Note that now the update fields in Category are gone in all places -> ormar fields, pydantic fields and sqlachemy table columns
# so full list of available fields in Category is: ["created_by", "created_date", "id", "name", "code"]

请注意,您只需提供字段名称,它将排除父字段,无论该字段来自哪个父模型。

!!!注意 请注意,如果您想覆盖子模型中的字段,则不必排除它,只需用相同的字段名称覆盖子模型中的字段声明即可。

警告注意,这种行为可能会混淆 mypy 和静态类型检查器,但访问不存在的字段将在运行时失败。这就是为什么首选拆分基类的原因。

通过拆分基类可以实现相同的效果,例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
base_ormar_config = OrmarConfig(
    metadata=sa.MetaData(),
    database=databases.Database(DATABASE_URL),
)


class AuditCreateModel(ormar.Model):
    ormar_config = base_ormar_config.copy(abstract=True)

    created_by: str = ormar.String(max_length=100)


class AuditUpdateModel(ormar.Model):
    ormar_config = base_ormar_config.copy(abstract=True)

    updated_by: str = ormar.String(max_length=100, default="Sam")

class CreateDateFieldsModel(ormar.Model):
    ormar_config = base_ormar_config.copy(abstract=True)

    created_date: datetime.datetime = ormar.DateTime(
        default=datetime.datetime.now, name="creation_date"
    )

class UpdateDateFieldsModel(ormar.Model):
    ormar_config = base_ormar_config.copy(abstract=True)

    updated_date: datetime.datetime = ormar.DateTime(
        default=datetime.datetime.now, name="modification_date"
    )


class Category(CreateDateFieldsModel, AuditCreateModel):
    ormar_config = base_ormar_config.copy(tablename="categories")

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=50, unique=True, index=True)
    code: int = ormar.Integer()

这样,如果需要,您可以从创建类和更新类继承,否则只能继承其中一个类。