遗产
在各种类型的 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。
| 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 模型上的字段时,您将得到:
| 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 参数即可。
| # 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()
|
现在柱子看起来好多了。
| 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,这样就会添加正确的后缀。
| `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 模型的直通模型。
| 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 模型,并在末尾添加了子类名称,同时更改克隆字段的表名称,使用子级的表名称。
请注意,原始模型不仅没有被使用,而且该模型的表也从元数据中删除:
| 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 模型,这也意味着新的数据库表。
| 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()
|
这样,如果需要,您可以从创建类和更新类继承,否则只能继承其中一个类。