Modeling polymorphism in relational databases is a challenging task. In this article, we present several modeling techniques to represent polymorphic objects in a relational database using the Django object-relational mapping (ORM).
在关系数据库中对多态进行建模是一项艰巨的任务。 在本文中,我们介绍了几种建模技术,这些技术使用Django对象关系映射( ORM )来表示关系数据库中的多态对象。
This intermediate-level tutorial is designed for readers who are already familiar with the fundamental design of Django.
本中级教程适用于已经熟悉Django基本设计的读者。
Free Bonus: Click here to get the most popular Django tutorials and resources on Real Python and improve your Django + Python web development skills.
免费奖金: 单击此处可获取Real Python上最受欢迎的Django教程和资源,并提高您的Django + Python Web开发技能。
什么是多态? (What Is Polymorphism?)
Polymorphism is the ability of an object to take on many forms. Common examples of polymorphic objects include event streams, different types of users, and products in an e-commerce website. A polymorphic model is used when a single entity requires different functionality or information.
多态是对象采取多种形式的能力。 多态对象的常见示例包括事件流,不同类型的用户以及电子商务网站中的产品。 当单个实体需要不同的功能或信息时,将使用多态模型。
In the examples above, all events are logged for future use, but they can contain different data. All users need be able to log in, but they might have different profile structures. In every e-commerce website, a user wants to put different products in their shopping cart.
在上面的示例中,所有事件都记录下来以备将来使用,但是它们可以包含不同的数据。 所有用户都需要能够登录,但是他们可能具有不同的配置文件结构。 在每个电子商务网站中,用户都希望将不同的产品放入他们的购物车中。
为什么建模多态性具有挑战性? (Why Is Modeling Polymorphism Challenging?)
There are many ways to model polymorphism. Some approaches use standard features of the Django ORM, and some use special features of the Django ORM. The main challenges you’ll encounter when modeling polymorphic objects are the following:
有多种方法可以对多态性进行建模。 一些方法使用Django ORM的标准功能,而某些方法使用Django ORM的特殊功能。 在对多态对象进行建模时,您将遇到的主要挑战如下:
-
How to represent a single polymorphic object: Polymorphic objects have different attributes. The Django ORM maps attributes to columns in the database. In that case, how should the Django ORM map attributes to the columns in the table? Should different objects reside in the same table? Should you have multiple tables?
-
How to reference instances of a polymorphic model: To utilize database and Django ORM features, you need to reference objects using foreign keys. How you decide to represent a single polymorphic object is crucial to your ability to reference it.
如何表示单个多态对象:多态对象具有不同的属性。 Django ORM将属性映射到数据库中的列。 在这种情况下,Django ORM应该如何将属性映射到表中的列? 不同的对象应该驻留在同一张表中吗? 您应该有多个表吗?
如何引用多态模型的实例:要利用数据库和Django ORM功能,您需要使用外键引用对象。 您决定如何表示单个多态对象对于引用它的能力至关重要。
To truly understand the challenges of modeling polymorphism, you are going to take a small bookstore from its first online website to a big online shop selling all sorts of products. Along the way, you’ll experience and analyze different approaches for modeling polymorphism using the Django ORM.
为了真正理解建模多态性的挑战,您将从一家第一家在线网站的小型书店转到一家出售各种产品的大型网上商店。 在此过程中,您将体验并分析使用Django ORM建模多态性的不同方法。
Note: To follow this tutorial, it is recommended that you use a PostgreSQL backend, Django 2.x, and Python 3.
注意:要遵循本教程,建议您使用PostgreSQL后端,Django 2.x和Python 3。
It’s possible to follow along with other database backends as well. In places where features unique to PostgreSQL are used, an alternative will be presented for other databases.
也可以与其他数据库后端一起使用。 在使用PostgreSQL独有功能的地方,将为其他数据库提供替代方案。
天真的实现 (Naive Implementation)
You have a bookstore in a nice part of town right next to a coffee shop, and you want to start selling books online.
您在城镇的一处不错的地方,有一家书店,就在一家咖啡店旁边,您想开始在线销售书籍。
You sell only one type of product: books. In your online store, you want to show details about the books, like name and price. You want your users to browse around the website and collect many books, so you also need a cart. You eventually need to ship the books to the user, so you need to know the weight of each book to calculate the delivery fee.
您只销售一种产品:书籍。 在您的在线商店中,您想显示有关书籍的详细信息,例如名称和价格。 您希望用户浏览网站并收集许多书籍,因此还需要购物车。 您最终需要将书籍运送给用户,因此您需要知道每本书的重量来计算送货费用。
Let’s create a simple model for your new book store:
让我们为您的新书店创建一个简单的模型:
from from django.contrib.auth django.contrib.auth import import get_user_model
get_user_model
from from django.db django.db import import models
models
class class BookBook (( modelsmodels .. ModelModel ):
):
name name = = modelsmodels .. CharFieldCharField (
(
max_lengthmax_length == 100100 ,
,
)
)
price price = = modelsmodels .. PositiveIntegerFieldPositiveIntegerField (
(
help_texthelp_text == 'in cents''in cents' ,
,
)
)
weight weight = = modelsmodels .. PositiveIntegerFieldPositiveIntegerField (
(
help_texthelp_text == 'in grams''in grams' ,
,
)
)
def def __str____str__ (( selfself ) ) -> -> strstr :
:
return return selfself .. name
name
class class CartCart (( modelsmodels .. ModelModel ):
):
user user = = modelsmodels .. OneToOneFieldOneToOneField (
(
get_user_modelget_user_model (),
(),
primary_keyprimary_key == TrueTrue ,
,
on_deleteon_delete == modelsmodels .. CASCADECASCADE ,
,
)
)
books books = = modelsmodels .. ManyToManyFieldManyToManyField (( BookBook )
)
To create a new book, you provide a name, price, and weight:
要创建一本新书,您需要提供名称,价格和重量:
>>> from naive.models import Book
>>> book = Book . objects . create ( name = 'Python Tricks' , price = 1000 , weight = 200 )
>>> book
<Product: Python Tricks>
To create a cart, you first need to associate it with a user:
要创建购物车,您首先需要将其与用户关联:
>>> from django.contrib.auth import get_user_model
>>> haki = get_user_model () . create_user ( 'haki' )
>>> from naive.models import Cart
>>> cart = Cart . objects . create ( user = haki )
Then the user can start adding items to it:
然后,用户可以开始向其中添加项目:
>>> cart . products . add ( book )
>>> cart . products . all ()
<QuerySet [<Book: Python Tricks>]>
Pro
专业版
- Easy to understand and maintain: It’s sufficient for a single type of product.
- 易于理解和维护:对于单一类型的产品就足够了。
Con
骗局
- Restricted to homogeneous products: It only supports products with the same set of attributes. Polymorphism is not captured or permitted at all.
- 仅限同类产品:仅支持具有相同属性集的产品。 根本不捕获或不允许多态。
稀疏模型 (Sparse Model)
With the success of your online bookstore, users started to ask if you also sell e-books. E-books are a great product for your online store, and you want to start selling them right away.
随着在线书店的成功,用户开始询问您是否也销售电子书。 电子书是您的在线商店的好产品,您想立即开始销售它们。
A physical book is different from an e-book:
一本物理书不同于一本电子书:
-
An e-book has no weight. It’s a virtual product.
-
An e-book does not require shipment. Users download it from the website.
电子书没有分量。 这是一个虚拟产品。
电子书不需要发货。 用户从网站下载它。
To make your existing model support the additional information for selling e-books, you add some fields to the existing Book
model:
为了使您的现有模型支持销售电子书的其他信息,您可以在现有的Book
模型中添加一些字段:
First, you added a type field to indicate what type of book it is. Then, you added a URL field to store the download link of the e-book.
首先,您添加了一个类型字段,以指示它是什么类型的书。 然后,您添加了URL字段来存储电子书的下载链接。
To add a physical book to your bookstore, do the following:
要将实体书添加到您的书店,请执行以下操作:
>>> from sparse.models import Book
>>> physical_book = Book.objects.create(
... type=Book.TYPE_PHYSICAL,
... name='Python Tricks',
... price=1000,
... weight=200,
... download_link=None,
... )
>>> physical_book
<Book: [Physical] Python Tricks>
To add a new e-book, you do the following:
要添加新的电子书,请执行以下操作:
>>> virtual_book = Book.objects.create(
... type=Book.TYPE_VIRTUAL,
... name='The Old Man and the Sea',
... price=1500,
... weight=0,
... download_link='https://books.com/12345',
... )
>>> virtual_book
<Book: [Virtual] The Old Man and the Sea>
Your users can now add both books and e-books to the cart:
您的用户现在可以将书籍和电子书添加到购物车中:
>>> from sparse.models import Cart
>>> cart = Cart.objects.create(user=user)
>>> cart.books.add(physical_book, virtual_book)
>>> cart.books.all()
<QuerySet [<Book: [Physical] Python Tricks>, <Book: [Virtual] The Old Man and the Sea>]>
The virtual books are a big hit, and you decide to hire employees. The new employees are apparently not so tech savvy, and you start seeing weird things in the database:
虚拟书籍很受欢迎,您决定雇用员工。 新员工显然不那么精通技术,您开始在数据库中看到怪异的东西:
>>> Book.objects.create(
... type=Book.TYPE_PHYSICAL,
... name='Python Tricks',
... price=1000,
... weight=0,
... download_link='http://books.com/54321',
... )
That book apparently weighs 0
pounds and has a download link.
那本书显然重达0
磅,并有下载链接。
This e-book apparently weighs 100g and has no download link:
这本电子书显然重100克,没有下载链接:
>>> Book.objects.create(
... type=Book.TYPE_VIRTUAL,
... name='Python Tricks',
... price=1000,
... weight=100,
... download_link=None,
... )
This doesn’t make any sense. You have a data integrity problem.
这没有任何意义。 您有数据完整性问题。
To overcome integrity problems, you add validations to the model:
为了克服完整性问题,您可以向模型添加验证:
from from django.core.exceptions django.core.exceptions import import ValidationError
ValidationError
class class BookBook (( modelsmodels .. ModelModel ):
):
# ...
# ...
def def cleanclean (( selfself ) ) -> -> NoneNone :
:
if if selfself .. type type == == BookBook .. TYPE_VIRTUALTYPE_VIRTUAL :
:
if if selfself .. weight weight != != 00 :
:
raise raise ValidationErrorValidationError (
(
'A virtual product weight cannot exceed zero.'
'A virtual product weight cannot exceed zero.'
)
)
if if selfself .. download_link download_link is is NoneNone :
:
raise raise ValidationErrorValidationError (
(
'A virtual product must have a download link.'
'A virtual product must have a download link.'
)
)
elif elif selfself .. type type == == BookBook .. TYPE_PHYSICALTYPE_PHYSICAL :
:
if if selfself .. weight weight == == 00 :
:
raise raise ValidationErrorValidationError (
(
'A physical product weight must exceed zero.'
'A physical product weight must exceed zero.'
)
)
if if selfself .. download_link download_link is is not not NoneNone :
:
raise raise ValidationErrorValidationError (
(
'A physical product cannot have a download link.'
'A physical product cannot have a download link.'
)
)
elseelse :
:
assert assert FalseFalse , , ff 'Unknown product type "'Unknown product type " {self.type}{self.type} "'
"'
You used Django’s built-in validation mechanism to enforce data integrity rules. clean()
is only called automatically by Django forms. For objects that are not created by a Django form, you need to make sure to explicitly validate the object.
您使用了Django的内置验证机制来实施数据完整性规则。 clean()
仅由Django表单自动调用。 对于不是由Django表单创建的对象,您需要确保明确验证该对象。
To keep the integrity of the Book
model intact, you need to make a little change to the way you create books:
为了保持Book
模型的完整性,您需要对创建书的方式进行一些更改:
>>> book = Book (
... type = Book . TYPE_PHYSICAL ,
... name = 'Python Tricks' ,
... price = 1000 ,
... weight = 0 ,
... download_link = 'http://books.com/54321' ,
... )
>>> book . full_clean ()
ValidationError: {'__all__': ['A physical product weight must exceed zero.']}
>>> book = Book (
... type = Book . TYPE_VIRTUAL ,
... name = 'Python Tricks' ,
... price = 1000 ,
... weight = 100 ,
... download_link = None ,
... )
>>> book . full_clean ()
ValidationError: {'__all__': ['A virtual product weight cannot exceed zero.']}
When creating objects using the default manager (Book.objects.create(...)
), Django will create an object and immediately persist it to the database.
使用默认管理器( Book.objects.create(...)
)创建对象时,Django将创建一个对象并将其立即保存到数据库中。
In your case, you want to validate the object before saving if to the database. You first create the object (Book(...)
), validate it (book.full_clean()
), and only then save it (book.save()
).
在您的情况下,您想先验证对象,然后再将其保存到数据库中。 首先创建对象( Book(...)
),对其进行验证( book.full_clean()
),然后再保存该对象( book.save()
)。
Denormalization:
非正规化:
A sparse model is a product of denormalization. In a denormalization process, you inline attributes from multiple normalized models into a single table for better performance. A denormalized table will usually have a lot of nullable columns.
稀疏模型是非正规化的产物。 在反规范化过程中,可以将来自多个规范化模型的属性内联到单个表中,以提高性能。 非规范化表通常将具有许多可为空的列。
Denormalizing is often used in decision support systems such as data warehouses where read performance is most important. Unlike OLTP systems, data warehouses are usually not required to enforce data integrity rules, which makes denormalization ideal.
规范化通常用于决策支持系统中,例如数据仓库,其中读取性能最为重要。 与OLTP系统不同,通常不需要数据仓库来执行数据完整性规则,这使非规范化成为理想选择。
Pro
专业版
- Easy to understand and maintain: The sparse model is usually the first step we take when certain types of objects need more information. It’s very intuitive and easy to understand.
- 易于理解和维护:当某些类型的对象需要更多信息时,稀疏模型通常是我们要采取的第一步。 这是非常直观且易于理解的。
Cons
缺点
-
Unable to utilize NOT NULL database constraints: Null values are used for attributes that are not defined for all types of objects.
-
Complex validation logic: Complex validation logic is required to enforce data integrity rules. The complex logic also requires more tests.
-
Many Null fields create clutter: Representing multiple types of products in a single model makes it harder to understand and maintain.
-
New types require schema changes: New types of products require additional fields and validations.
无法利用NOT NULL数据库约束:空值用于未为所有类型的对象定义的属性。
复杂的验证逻辑:需要复杂的验证逻辑才能实施数据完整性规则。 复杂的逻辑还需要更多的测试。
许多Null字段会造成混乱:在一个模型中表示多种类型的产品将使其更难以理解和维护。
新型需要更改架构:新型产品需要附加字段和验证。
Use Case
用例
The sparse model is ideal when you’re representing heterogeneous objects that share most attributes, and when new items are not added very often.
当您表示共享大多数属性的异构对象,并且不经常添加新项目时,稀疏模型是理想的选择。
半结构模型 (Semi-Structured Model)
Your bookstore is now a huge success, and you are selling more and more books. You have books from different genres and publishers, e-books with different formats, books with odd shapes and sizes, and so on.
现在,您的书店取得了巨大的成功,并且您正在出售越来越多的书。 您有来自不同流派和出版商的书籍,具有不同格式的电子书,具有不同形状和大小的书籍,等等。
In the sparse model approach, you added fields for every new type of product. The model now has a lot of nullable fields, and new developers and employees are having trouble keeping up.
在稀疏模型方法中,您为每种新型产品添加了字段。 该模型现在有很多可为空的字段,并且新开发人员和员工都难以跟上。
To address the clutter, you decide to keep only the common fields (name
and price
) on the model. You store the rest of the fields in a single JSONField
:
为了解决混乱情况,您决定在模型上仅保留公共字段( name
和price
)。 您将其余字段存储在单个JSONField
:
JSONField:
JSONField:
In this example, you use PostgreSQL as a database backend. Django provides a built-in JSON field for PostgreSQL in django.contrib.postgres.fields
.
在此示例中,您将PostgreSQL用作数据库后端。 Django在django.contrib.postgres.fields
为PostgreSQL提供了内置的JSON字段。
For other databases, such as SQLite and MySQL, there are packages that provide similar functionality.
对于其他数据库,例如SQLite和MySQL,有一些提供类似功能的软件包 。
Your Book
model is now clutter-free. Common attributes are modeled as fields. Attributes that are not common to all types of products are stored in the extra
JSON field:
您的Book
型号现在整洁了。 通用属性被建模为字段。 并非所有产品类型都通用的属性存储在extra
JSON字段中:
>>> from semi_structured.models import Book
>>> physical_book = Book(
... type=Book.TYPE_PHYSICAL,
... name='Python Tricks',
... price=1000,
... extra={'weight': 200},
... )
>>> physical_book.full_clean()
>>> physical_book.save()
<Book: [Physical] Python Tricks>
>>> virtual_book = Book(
... type=Book.TYPE_VIRTUAL,
... name='The Old Man and the Sea',
... price=1500,
... extra={'download_link': 'http://books.com/12345'},
... )
>>> virtual_book.full_clean()
>>> virtual_book.save()
<Book: [Virtual] The Old Man and the Sea>
>>> from semi_structured.models import Cart
>>> cart = Cart.objects.create(user=user)
>>> cart.books.add(physical_book, virtual_book)
>>> cart.books.all()
<QuerySet [<Book: [Physical] Python Tricks>, <Book: [Virtual] The Old Man and the Sea>]>
Clearing up the clutter is important, but it comes with a cost. The validation logic is a lot more complicated:
清理杂波很重要,但是要付出代价。 验证逻辑要复杂得多:
from from django.core.exceptions django.core.exceptions import import ValidationError
ValidationError
from from django.core.validators django.core.validators import import URLValidator
URLValidator
class class BookBook (( modelsmodels .. ModelModel ):
):
# ...
# ...
def def cleanclean (( selfself ) ) -> -> NoneNone :
:
if if selfself .. type type == == BookBook .. TYPE_VIRTUALTYPE_VIRTUAL :
:
trytry :
:
weight weight = = intint (( selfself .. extraextra [[ 'weight''weight' ])
])
except except ValueErrorValueError :
:
raise raise ValidationErrorValidationError (
(
'Weight must be a number'
'Weight must be a number'
)
)
except except KeyErrorKeyError :
:
pass
pass
elseelse :
:
if if weight weight != != 00 :
:
raise raise ValidationErrorValidationError (
(
'A virtual product weight cannot exceed zero.'
'A virtual product weight cannot exceed zero.'
)
)
trytry :
:
download_link download_link = = selfself .. extraextra [[ 'download_link''download_link' ]
]
except except KeyErrorKeyError :
:
pass
pass
elseelse :
:
# Will raise a validation error
# Will raise a validation error
URLValidatorURLValidator ()(()( download_linkdownload_link )
)
elif elif selfself .. type type == == BookBook .. TYPE_PHYSICALTYPE_PHYSICAL :
:
trytry :
:
weight weight = = intint (( selfself .. extraextra [[ 'weight''weight' ])
])
except except ValueErrorValueError :
:
raise raise ValidationErrorValidationError (
(
'Weight must be a number'
'Weight must be a number'
)
)
except except KeyErrorKeyError :
:
pass
pass
elseelse :
:
if if weight weight == == 00 :
:
raise raise ValidationErrorValidationError (
(
'A physical product weight must exceed zero.'
'A physical product weight must exceed zero.'
)
)
trytry :
:
download_link download_link = = selfself .. extraextra [[ 'download_link''download_link' ]
]
except except KeyErrorKeyError :
:
pass
pass
elseelse :
:
if if download_link download_link is is not not NoneNone :
:
raise raise ValidationErrorValidationError (
(
'A physical product cannot have a download link.'
'A physical product cannot have a download link.'
)
)
elseelse :
:
raise raise ValidationErrorValidationError (( ff 'Unknown product type "'Unknown product type " {self.type}{self.type} "'"' )
)
The benefit of using a proper field is that it validates the type. Both Django and the Django ORM can perform checks to make sure the right type is used for the field. When using a JSONField
, you need to validate both the type and the value:
使用适当字段的好处是它可以验证类型。 Django和Django ORM都可以执行检查,以确保对该字段使用正确的类型。 使用JSONField
,您需要同时验证类型和值:
>>> book = Book . objects . create (
... type = Book . TYPE_VIRTUAL ,
... name = 'Python Tricks' ,
... price = 1000 ,
... extra = { 'weight' : 100 },
... )
>>> book . full_clean ()
ValidationError: {'__all__': ['A virtual product weight cannot exceed zero.']}
Another issue with using JSON is that not all databases have proper support for querying and indexing values in JSON fields.
使用JSON的另一个问题是,并非所有数据库都对JSON字段中的值进行查询和索引提供了适当的支持。
In PostgreSQL for example, you can query all the books that weigh more than 100
:
例如,在PostgreSQL中,您可以查询重量超过100
所有书籍:
>>> Book . objects . filter ( extra__weight__gt = 100 )
<QuerySet [<Book: [Physical] Python Tricks>]>
However, not all database vendors support that.
但是,并非所有数据库供应商都支持。
Another restriction imposed when using JSON is that you are unable to use database constraints such as not null, unique, and foreign keys. You will have to implement these constraints in the application.
使用JSON时施加的另一个限制是您无法使用数据库约束,例如非null,唯一和外键。 您将必须在应用程序中实现这些约束。
This semi-structured approach resembles NoSQL architecture and has many of its advantages and disadvantages. The JSON field is a way to get around the strict schema of a relational database. This hybrid approach provides us with the flexibility to squash many object types into a single table while still maintaining some of the benefits of a relational, strictly and strongly typed database. For many common NoSQL use cases, this approach might actually be more suitable.
这种半结构化方法类似于NoSQL架构,并且具有许多优点和缺点。 JSON字段是一种解决关系数据库的严格架构的方法。 这种混合方法为我们提供了将多种对象类型压缩到单个表中的灵活性,同时仍保留了关系型,严格类型和强类型数据库的某些优势。 对于许多常见的NoSQL用例,此方法实际上可能更合适。
Pros
优点
-
Reduce clutter: Common fields are stored on the model. Other fields are stored in a single JSON field.
-
Easier to add new types: New types of products don’t require schema changes.
减少混乱:公共字段存储在模型中。 其他字段存储在单个JSON字段中。
轻松添加新类型:新类型的产品不需要更改架构。
Cons
缺点
-
Complicated and ad hoc validation logic: Validating a JSON field requires validating types as well as values. This challenge can be addressed by using other solutions to validate JSON data such as JSON schema.
-
Unable to utilize database constraints: Database constraints such as null null, unique and foreign key constraints, which enforce type and data integrity at the database level, cannot be used.
-
Restricted by database support for JSON: Not all database vendors support querying and indexing JSON fields.
-
Schema is not enforced by the database system: Schema changes might require backward compatibility or ad hoc migrations. Data can “rot.”
-
No deep integration with the database metadata system: Metadata about the fields is not stored in the database. Schema is only enforced at the application level.
复杂且临时的验证逻辑 :验证JSON字段需要验证类型和值。 可以通过使用其他解决方案来验证JSON数据(例如JSON模式)来解决此挑战。
无法利用数据库约束 : 不能使用诸如null null,唯一和外键约束之类的数据库约束,这些约束在数据库级别上强制类型和数据完整性。
受数据库对JSON的支持限制 :并非所有数据库供应商都支持查询和索引JSON字段。
数据库系统未强制执行架构:架构更改可能需要向后兼容或临时迁移。 数据会“腐烂”。
没有与数据库元数据系统进行深度集成 :有关字段的元数据未存储在数据库中。 模式仅在应用程序级别实施。
Use Case
用例
A semi-structured model is ideal when you need to represent heterogeneous objects that don’t share many common attributes, and when new items are added often.
当您需要表示不具有许多公共属性的异构对象,并且经常添加新项目时,半结构化模型是理想的选择。
A classic use case for the semi-structured approach is storing events (like logs, analytics, and event stores). Most events have a timestamp, type and metadata like device, user agent, user, and so on. The data for each type is stored in a JSON field. For analytics and log events, it’s important to be able to add new types of events with minimal effort, so this approach is ideal.
半结构化方法的经典用例是存储事件(例如日志,分析和事件存储)。 大多数事件都有时间戳,类型和元数据,例如设备,用户代理,用户等。 每种类型的数据都存储在JSON字段中。 对于分析和日志事件,能够以最小的努力添加新类型的事件非常重要,因此这种方法是理想的。
抽象基础模型 (Abstract Base Model)
So far, you’ve worked around the problem of actually treating your products as heterogeneous. You worked under the assumption that the differences between the products is minimal, so it made sense to maintain them in the same model. This assumption can take you only so far.
到目前为止,您已经解决了将产品实际视为异构产品的问题。 您在假设产品之间的差异很小的前提下进行工作,因此将它们保持在同一模型中是有意义的。 这种假设只能带您到现在为止。
Your little store is growing fast, and you want to start selling entirely different types of products, such as e-readers, pens, and notebooks.
您的小商店发展Swift,并且您想开始销售完全不同类型的产品,例如电子阅读器,笔和笔记本。
A book and an e-book are both products. A product is defined using common attributes such as name and price. In an object-oriented environment, you could look at a Product
as a base class or an interface. Every new type of product you add must implement the Product
class and extend it with its own attributes.
一本书和一本电子书都是产品。 使用通用属性(例如名称和价格)定义产品。 在面向对象的环境中,您可以将Product
视为基类或接口。 您添加的每种新型产品都必须实现Product
类,并使用其自己的属性对其进行扩展。
Django offers the ability to create abstract base classes. Let’s define a Product
abstract base class and add two models for Book
and EBook
:
Django提供了创建抽象基类的功能 。 让我们定义一个Product
抽象基类,并为Book
和EBook
添加两个模型:
Notice that both Book
and EBook
inherit from Product
. The fields defined in the base class Product
are inherited, so the derived models Book
and Ebook
don’t need to repeat them.
注意Book
和EBook
都从Product
继承。 基类Product
中定义的字段是继承的,因此派生模型Book
和Ebook
不需要重复它们。
To add new products, you use the derived classes:
要添加新产品,请使用派生类:
>>> from abstract_base_model.models import Book
>>> book = Book.objects.create(name='Python Tricks', price=1000, weight=200)
>>> book
<Book: Python Tricks>
>>> ebook = EBook.objects.create(
... name='The Old Man and the Sea',
... price=1500,
... download_link='http://books.com/12345',
... )
>>> ebook
<Book: The Old Man and the Sea>
You might have noticed that the Cart
model is missing. You can try to create a Cart
model with a ManyToMany
field to Product
:
您可能已经注意到缺少Cart
模型。 您可以尝试使用到Product
的ManyToMany
字段创建Cart
模型:
class class CartCart (( modelsmodels .. ModelModel ):
):
user user = = modelsmodels .. OneToOneFieldOneToOneField (
(
get_user_modelget_user_model (),
(),
primary_keyprimary_key == TrueTrue ,
,
on_deleteon_delete == modelsmodels .. CASCADECASCADE ,
,
)
)
items items = = modelsmodels .. ManyToManyFieldManyToManyField (( ProductProduct )
)
If you try to reference a ManyToMany
field to an abstract model, you will get the following error:
如果您尝试将ManyToMany
字段引用到抽象模型,则会出现以下错误:
A foreign key constraint can only point to a concrete table. The abstract base model Product
only exists in the code, so there is no products table in the database. The Django ORM will only create tables for the derived models Book
and EBook
.
外键约束只能指向具体表。 抽象基本模型Product
仅存在于代码中,因此数据库中没有product表。 Django ORM将仅为派生模型Book
和EBook
创建表。
Given that you can’t reference the abstract base class Product
, you need to reference books and e-books directly:
鉴于您不能引用抽象基类Product
,因此需要直接参考书籍和电子书:
class class CartCart (( modelsmodels .. ModelModel ):
):
user user = = modelsmodels .. OneToOneFieldOneToOneField (
(
get_user_modelget_user_model (),
(),
primary_keyprimary_key == TrueTrue ,
,
on_deleteon_delete == modelsmodels .. CASCADECASCADE ,
,
)
)
books books = = modelsmodels .. ManyToManyFieldManyToManyField (( BookBook )
)
ebooks ebooks = = modelsmodels .. ManyToManyFieldManyToManyField (( EBookEBook )
)
You can now add both books and e-books to the cart:
您现在可以将书籍和电子书都添加到购物车中:
>>> user = get_user_model () . objects . first ()
>>> cart = Cart . objects . create ( user = user )
>>> cart . books . add ( book )
>>> cart . ebooks . add ( ebook )
This model is a bit more complicated now. Let’s query the total price of the items in the cart:
这个模型现在有点复杂了。 让我们查询购物车中物品的总价:
>>> from django.db.models import Sum
>>> from django.db.models.functions import Coalesce
>>> (
... Cart . objects
... . filter ( pk = cart . pk )
... . aggregate ( total_price = Sum (
... Coalesce ( 'books__price' , 'ebooks__price' )
... ))
... )
{'total_price': 1000}
Because you have more than one type of book, you use Coalesce
to fetch either the price of the book or the price of the e-book for each row.
由于您拥有多种类型的书籍,因此您可以使用Coalesce
来获取每一行的书籍价格或电子书籍的价格。
Pro
专业版
- Easier to implement specific logic: A separate model for each product makes it easier to implement, test, and maintain specific logic.
- 更容易实现特定的逻辑 :每个产品都有一个单独的模型,可以更轻松地实现,测试和维护特定的逻辑。
Cons
缺点
-
Require multiple foreign keys: To reference all types of products, each type needs a foreign key.
-
Harder to implement and maintain: Operations on all types of products require checking all foreign keys. This adds complexity to the code and makes maintenance and testing harder.
-
Very hard to scale: New types of products require additional models. Managing many models can be tedious and very hard to scale.
需要多个外键 :要引用所有类型的产品,每种类型都需要一个外键。
难以实施和维护 :要在所有类型的产品上进行操作,都需要检查所有外键。 这增加了代码的复杂性,并使维护和测试更加困难。
很难扩展 :新型产品需要其他型号。 管理许多模型可能很乏味,而且很难扩展。
Use Case
用例
An abstract base model is a good choice when there are very few types of objects that required very distinct logic.
当很少类型的对象需要非常不同的逻辑时,抽象基础模型是一个不错的选择。
An intuitive example is modeling a payment process for your online shop. You want to accept payments with credit cards, PayPal, and store credit. Each payment method goes through a very different process that requires very distinct logic. Adding a new type of payment is not very common, and you don’t plan on adding new payment methods in the near future.
一个直观的示例是为您的在线商店的付款流程建模。 您要接受使用信用卡,贝宝(PayPal)和商店信用的付款。 每种付款方式都要经过非常不同的过程,这需要非常不同的逻辑。 添加新型付款方式不是很常见,并且您不打算在不久的将来添加新的付款方式。
You create a payment process base class with derived classes for credit card payment process, PayPal payment process, and store credit payment process. For each of the derived classes, you implement the payment process in a very different way that cannot be easily shared. In this case, it might make sense to handle each payment process specifically.
您将创建一个具有衍生类的支付流程基类,这些派生类包括信用卡支付流程,PayPal支付流程和商店信用支付流程。 对于每个派生类,您将以不同的方式实施付款流程,这种方式无法轻松共享。 在这种情况下,最好专门处理每个付款过程。
混凝土基础模型 (Concrete Base Model)
Django offers another way to implement inheritance in models. Instead of using an abstract base class that only exists in the code, you can make the base class concrete. “Concrete” means that the base class exists in the database as a table, unlike in the abstract base class solution, where the base class only exists in the code.
Django提供了另一种在模型中实现继承的方法。 可以不使用仅存在于代码中的抽象基类,而可以使基类具体化。 “具体”是指基类以表形式存在于数据库中,这与抽象基类解决方案不同,后者仅在代码中存在基类。
Using the abstract base model, you were unable to reference multiple type of products. You were forced to create a many-to-many relation for each type of product. This made it harder to perform tasks on the common fields such as getting the total price of all the items in the cart.
使用抽象基础模型,您无法引用多种类型的产品。 您不得不为每种产品创建多对多关系。 这使得在通用字段上执行任务变得更加困难,例如获取购物车中所有物品的总价。
Using a concrete base class, Django will create a table in the database for the Product
model. The Product
model will have all the common fields you defined in the base model. Derived models such as Book
and EBook
will reference the Product
table using a one-to-one field. To reference a product, you create a foreign key to the base model:
Django将使用一个具体的基类,在数据库中为Product
模型创建一个表。 Product
模型将具有您在基本模型中定义的所有公共字段。 诸如Book
和EBook
类的派生模型将使用一对一字段引用Product
表。 要引用产品,请创建基本模型的外键:
The only difference between this example and the previous one is that the Product
model is not defined with abstract=True
.
此示例与上一个示例之间的唯一区别是,未使用abstract=True
定义Product
模型。
To create new products, you use derived Book
and EBook
models directly:
要创建新产品,请直接使用派生的Book
和EBook
模型:
>>> from concrete_base_model.models import Book, EBook
>>> book = Book.objects.create(
... name='Python Tricks',
... price=1000,
... weight=200,
... )
>>> book
<Book: Python Tricks>
>>> ebook = EBook.objects.create(
... name='The Old Man and the Sea',
... price=1500,
... download_link='http://books.com/12345',
... )
>>> ebook
<Book: The Old Man and the Sea>
In the case of concrete base class, it’s interesting to see what’s happening in the underlying database. Let’s look at the tables created by Django in the database:
对于具体的基类,看看底层数据库中发生的事情很有趣。 让我们看一下Django在数据库中创建的表:
> > d concrete_base_model_product
d concrete_base_model_product
Column | Type | Default
Column | Type | Default
--------+-----------------------+---------------------------------------------------------
--------+-----------------------+---------------------------------------------------------
id | integer | nextval('concrete_base_model_product_id_seq'::regclass)
id | integer | nextval('concrete_base_model_product_id_seq'::regclass)
name | character varying(100) |
name | character varying(100) |
price | integer |
price | integer |
Indexes:
Indexes:
"concrete_base_model_product_pkey" PRIMARY KEY, btree (id)
"concrete_base_model_product_pkey" PRIMARY KEY, btree (id)
Referenced by:
Referenced by:
TABLE "concrete_base_model_cart_items" CONSTRAINT "..." FOREIGN KEY (product_id)
TABLE "concrete_base_model_cart_items" CONSTRAINT "..." FOREIGN KEY (product_id)
REFERENCES concrete_base_model_product(id) DEFERRABLE INITIALLY DEFERRED
REFERENCES concrete_base_model_product(id) DEFERRABLE INITIALLY DEFERRED
TABLE "concrete_base_model_book" CONSTRAINT "..." FOREIGN KEY (product_ptr_id)
TABLE "concrete_base_model_book" CONSTRAINT "..." FOREIGN KEY (product_ptr_id)
REFERENCES concrete_base_model_product(id) DEFERRABLE INITIALLY DEFERRED
REFERENCES concrete_base_model_product(id) DEFERRABLE INITIALLY DEFERRED
TABLE "concrete_base_model_ebook" CONSTRAINT "..." FOREIGN KEY (product_ptr_id)
TABLE "concrete_base_model_ebook" CONSTRAINT "..." FOREIGN KEY (product_ptr_id)
REFERENCES concrete_base_model_product(id) DEFERRABLE INITIALLY DEFERRED
REFERENCES concrete_base_model_product(id) DEFERRABLE INITIALLY DEFERRED
The product table has two familiar fields: name and price. These are the common fields you defined in the Product
model. Django also created an ID primary key for you.
产品表有两个熟悉的字段:名称和价格。 这些是您在Product
模型中定义的通用字段。 Django还为您创建了ID主键。
In the constraints section, you see multiple tables that are referencing the product table. Two tables that stand out are concrete_base_model_book
and concrete_base_model_ebook
:
在“约束”部分中,您将看到引用产品表的多个表。 突出的两个表是concrete_base_model_book
和concrete_base_model_ebook
:
The Book
model has only two fields:
Book
模型只有两个字段:
weight
is the field you added in the derivedBook
model.product_ptr_id
is both the primary of the table and a foreign key to the base product model.
-
weight
是您在派生Book
模型中添加的字段。 -
product_ptr_id
既是表的主表,也是基本产品模型的外键。
Behind the scenes, Django created a base table for product. Then, for each derived model, Django created another table that includes the additional fields, and a field that acts both as a primary key and a foreign key to the product table.
在后台,Django创建了产品的基表。 然后,对于每个派生模型,Django创建了另一个表,该表包括其他字段,以及一个既充当product表的主键又充当外键的字段。
Let’s take a look at a query generated by Django to fetch a single book. Here are the results of print(Book.objects.filter(pk=1).query)
:
让我们看一下Django生成的查询以获取一本书。 这是print(Book.objects.filter(pk=1).query)
:
SELECT
SELECT
"concrete_base_model_product""concrete_base_model_product" .. "id""id" ,
,
"concrete_base_model_product""concrete_base_model_product" .. "name""name" ,
,
"concrete_base_model_product""concrete_base_model_product" .. "price""price" ,
,
"concrete_base_model_book""concrete_base_model_book" .. "product_ptr_id""product_ptr_id" ,
,
"concrete_base_model_book""concrete_base_model_book" .. "weight"
"weight"
FROM
FROM
"concrete_base_model_book"
"concrete_base_model_book"
INNER INNER JOIN JOIN "concrete_base_model_product" "concrete_base_model_product" ON
ON
"concrete_base_model_book""concrete_base_model_book" .. "product_ptr_id" "product_ptr_id" = = "concrete_base_model_product""concrete_base_model_product" .. "id"
"id"
WHERE
WHERE
"concrete_base_model_book""concrete_base_model_book" .. "product_ptr_id" "product_ptr_id" = = 1
1
To fetch a single book, Django joined concrete_base_model_product
and concrete_base_model_book
on the product_ptr_id
field. The name and price are in the product table and the weight is in the book table.
为了获取一本书,Django在product_ptr_id
字段上加入了concrete_base_model_product
和concrete_base_model_book
。 名称和价格在产品表中,重量在书本表中。
Since all the products are managed in the Product table, you can now reference it in a foreign key from the Cart
model:
由于所有产品都在“产品”表中进行管理,因此您现在可以在Cart
模型的外键中引用它:
Adding items to the cart is the same as before:
将商品添加到购物车与之前相同:
>>> from concrete_base_model.models import Cart
>>> cart = Cart.objects.create(user=user)
>>> cart.items.add(book, ebook)
>>> cart.items.all()
<QuerySet [<Book: Python Tricks>, <Book: The Old Man and the Sea>]>
Working with common fields is also simple:
使用公共字段也很简单:
>>> from django.db.models import Sum
>>> cart.items.aggregate(total_price=Sum('price'))
{'total_price': 2500}
Migrating base classes in Django:
在Django中迁移基类:
When a derived model is created, Django adds a bases
attribute to the migration:
创建派生模型后,Django将向迁移添加一个bases
属性:
migrationsmigrations .. CreateModelCreateModel (
(
namename == 'Book''Book' ,
,
fieldsfields == [[ ...... ],
],
basesbases == (( 'concrete_base_model.product''concrete_base_model.product' ,),
,),
),
),
If in the future you remove or change the base class, Django might not be able to perform the migration automatically. You might get this error:
如果将来删除或更改基类,则Django可能无法自动执行迁移。 您可能会收到此错误:
This is a known issue in Django (#23818, #23521, #26488). To work around it, you must edit the original migration manually and adjust the bases attribute.
这是Django( #23818 , #23521 , #26488 )中的一个已知问题。 要解决此问题,必须手动编辑原始迁移并调整bases属性。
Pros
优点
-
Primary key is consistent across all types: The product is issued by a single sequence in the base table. This restriction can be easily resolved by using a UUID instead of a sequence.
-
Common attributes can be queried from a single table: Common queries such as total price, list of product names, and prices can be fetched directly from the base table.
主键在所有类型中都是一致的 :产品由基表中的单个序列发出。 通过使用UUID而不是序列,可以轻松解决此限制。
可以从单个表中查询公共属性 :可以直接从基表中获取诸如总价,产品名称列表和价格之类的公共查询。
Cons
缺点
-
New product types require schema changes: A new type requires a new model.
-
Can produce inefficient queries: The data for a single item is in two database tables. Fetching a product requires a join with the base table.
-
Cannot access extended data from base class instance: A type field is required to downcast an item. This adds complexity to the code.
django-polymorphic
is a popular module that might eliminate some of these challenges.
新产品类型需要架构更改 :新类型需要新模型。
可能产生效率低下的查询 :单个项目的数据在两个数据库表中。 提取产品需要与基表联接。
无法从基类实例访问扩展数据 :向下转换项目需要类型字段。 这增加了代码的复杂性。
django-polymorphic
是一个流行的模块,可以消除其中的一些挑战。
Use Case
用例
The concrete base model approach is useful when common fields in the base class are sufficient to satisfy most common queries.
当基类中的通用字段足以满足大多数通用查询时,具体的基础模型方法将非常有用。
For example, if you often need to query for the cart total price, show a list of items in the cart, or run ad hoc analytic queries on the cart model, you can benefit from having all the common attributes in a single database table.
例如,如果您经常需要查询购物车的总价,显示购物车中的项目列表或对购物车模型进行临时分析查询,则可以从单个数据库表中拥有所有公共属性中受益。
通用外键 (Generic Foreign Key)
Inheritance can sometimes be a nasty business. It forces you to create (possibly premature) abstractions, and it doesn’t always fit nicely into the ORM.
继承有时可能是一件令人讨厌的事情。 它迫使您创建( 可能为时过早的 )抽象,但它并不总是很好地适合于ORM。
The main problem you have is referencing different products from the cart model. You first tried to squash all the product types into one model (sparse model, semi-structured model), and you got clutter. Then you tried splitting products into separate models and providing a unified interface using a concrete base model. You got a complicated schema and a lot of joins.
您遇到的主要问题是从购物车模型中引用了不同的产品。 您首先尝试将所有产品类型压缩为一个模型(稀疏模型,半结构化模型),但变得混乱。 然后,您尝试将产品拆分为单独的模型,并使用具体的基础模型提供统一的界面。 您有一个复杂的架构和许多联接。
Django offers a special way of referencing any model in the project called GenericForeignKey
. Generic foreign keys are part of the Content Types framework built into Django. The content type framework is used by Django itself to keep track of models. This is necessary for some core capabilities such as migrations and permissions.
Django提供了一种特殊的方法来引用项目中称为GenericForeignKey
任何模型。 通用外键是Django内置的“ 内容类型”框架的一部分。 Django本身使用内容类型框架来跟踪模型。 这对于某些核心功能(例如迁移和权限)是必需的。
To better understand what content types are and how they facilitate generic foreign keys, let’s look at the content type related to the Book
model:
为了更好地理解什么是内容类型以及它们如何促进通用外键,让我们看一下与Book
模型相关的内容类型:
>>> from django.contrib.contenttypes.models import ContentType
>>> ct = ContentType.objects.get_for_model(Book)
>>> vars(ct)
{'_state': <django.db.models.base.ModelState at 0x7f1c9ea64400>,
'id': 22,
'app_label': 'concrete_base_model',
'model': 'book'}
Each model has a unique identifier. If you want to reference a book with PK 54, you can say, “Get object with PK 54 in the model represented by content type 22.”
每个模型都有一个唯一的标识符。 如果要参考PK 54的书,则可以说:“在内容类型22表示的模型中使用PK 54获取对象。”
GenericForeignKey
is implemented exactly like that. To create a generic foreign key, you define two fields:
GenericForeignKey
的实现完全与此相同。 要创建通用外键,请定义两个字段:
- A reference to a content type (the model)
- The primary key of the referenced object (the model instance’s
pk
attribute)
- 对内容类型(模型)的引用
- 引用对象的主键(模型实例的
pk
属性)
To implement a many-to-many relation using GenericForeignKey
, you need to manually create a model to connect carts with items.
要使用GenericForeignKey
实现多对多关系,您需要手动创建一个模型以将购物车与物品连接起来。
The Cart
model remains roughly similar to what you have seen so far:
Cart
模型大致类似于您到目前为止所看到的:
from from django.db django.db import import models
models
from from django.contrib.auth django.contrib.auth import import get_user_model
get_user_model
class class CartCart (( modelsmodels .. ModelModel ):
):
user user = = modelsmodels .. OneToOneFieldOneToOneField (
(
get_user_modelget_user_model (),
(),
primary_keyprimary_key == TrueTrue ,
,
on_deleteon_delete == modelsmodels .. CASCADECASCADE ,
,
)
)
Unlike previous Cart
models, this Cart
no longer includes a ManyToMany
field. You are going need to do that yourself.
与以前的Cart
型号不同,此Cart
不再包含ManyToMany
字段。 您将需要自己做。
To represent a single item in the cart, you need to reference both the cart and any product:
要表示购物车中的单个项目,您需要同时引用购物车和任何产品:
To add a new item in the Cart, you provide the content type and the primary key:
要在购物车中添加新商品,请提供内容类型和主键:
>>> book = Book.objects.first()
>>> Item.objects.create(
... product_content_type=ContentType.objects.get_for_model(book),
... product_object_id=book.pk,
... )
>>> ebook = EBook.objects.first()
>>> Item.objects.create(
... product_content_type=ContentType.objects.get_for_model(ebook),
... product_object_id=ebook.pk,
... )
Adding an item to a cart is a common task. You can add a method on the cart to add any product to the cart:
将项目添加到购物车是一项常见的任务。 您可以在购物车上添加方法以将任何产品添加到购物车:
class class CartCart (( modelsmodels .. ModelModel ):
):
# ...
# ...
def def add_itemadd_item (( selfself , , productproduct ) ) -> -> 'CartItem''CartItem' :
:
product_content_type product_content_type = = ContentTypeContentType .. objectsobjects .. get_for_modelget_for_model (( productproduct )
)
return return CartItemCartItem .. objectsobjects .. createcreate (
(
cartcart == selfself ,
,
product_content_typeproduct_content_type == product_content_typeproduct_content_type ,
,
product_object_idproduct_object_id == productproduct .. pkpk ,
,
)
)
Adding a new item to a cart is now much shorter:
现在,将新商品添加到购物车的过程要短得多:
>>> cart . add_item ( book )
>>> cart . add_item ( ebook )
Getting information about the items in the cart is also possible:
也可以获取有关购物车中物品的信息:
>>> cart . items . all ()
<QuerySet [<CartItem: CartItem object (1)>, <CartItem: CartItem object (2)>]
>>> item = cart . items . first ()
>>> item . product
<Book: Python Tricks>
>>> item . product . price
1000
So far so good. Where’s the catch?
到目前为止,一切都很好。 渔获物在哪里?
Let’s try to calculate the total price of the products in the cart:
让我们尝试计算购物车中产品的总价:
>>> from django.db.models import Sum
>>> cart . items . aggregate ( total = Sum ( 'product__price' ))
FieldError: Field 'product' does not generate an automatic reverse
relation and therefore cannot be used for reverse querying.
If it is a GenericForeignKey, consider adding a GenericRelation.
Django tells us it isn’t possible to traverse the generic relation from the generic model to the referenced model. The reason for that is that Django has no idea which table to join to. Remember, the Item
model can point to any ContentType
.
Django告诉我们不可能将泛型关系从泛型模型遍历到引用的模型。 这样做的原因是Django不知道要加入哪个表。 请记住, Item
模型可以指向任何ContentType
。
The error message does mention a GenericRelation
. Using a GenericRelation
, you can define a reverse relation from the referenced model to the Item
model. For example, you can define a reverse relation from the Book
model to items of books:
错误消息确实提到了GenericRelation
。 使用GenericRelation
,您可以定义从引用模型到Item
模型的反向关系。 例如,您可以定义从Book
模型到书籍项目的反向关系:
Using the reverse relation, you can answer questions like how many carts include a specific book:
使用逆向关系,您可以回答诸如一本特定书籍中有多少辆购物车的问题:
>>> book.cart_items.count()
4
>>> CartItem.objects.filter(books__id=book.id).count()
4
The two statement are identical.
这两个陈述是相同的。
You still need to know the price of the entire cart. You already saw that fetching the price from each product table is impossible using the ORM. To do that, you have to iterate the items, fetch each item separately, and aggregate:
您仍然需要知道整个购物车的价格。 您已经看到使用ORM无法从每个产品表中获取价格。 为此,您必须迭代这些项目,分别获取每个项目并进行汇总:
>>> sum(item.product.price for item in cart.items.all())
2500
This is one of the major disadvantages of generic foreign keys. The flexibility comes with a great performance cost. It’s very hard to optimize for performance using just the Django ORM.
这是通用外键的主要缺点之一。 灵活性带来了巨大的性能成本。 仅使用Django ORM进行性能优化是非常困难的。
Structural Subtyping
结构分型
In the abstract and concrete base class approaches, you used nominal subtyping, which is based on a class hierarchy. Mypy is able to detect this form of relation between two classes and infer types from it.
在抽象和具体的基类方法中,您使用了基于子类的名义子类型化。 Mypy能够检测到两个类之间的这种形式的关系,并从中推断出类型。
In the generic relation approach, you used structural subtyping. Structural subtyping exists when a class implements all the methods and attributes of another class. This form of subtyping is very useful when you wish to avoid direct dependency between modules.
在通用关系方法中,您使用了结构子类型化。 当一个类实现另一个类的所有方法和属性时,存在结构子类型 。 当您希望避免模块之间的直接依赖时,这种子类型化形式非常有用。
Mypy provides a way to utilize structural subtyping using Protocols.
Mypy提供了一种使用协议利用结构子类型的方法。
You already identified a product entity with common methods and attributes. You can define a Protocol
:
您已经确定了具有通用方法和属性的产品实体。 您可以定义一个Protocol
:
from from typing_extensions typing_extensions import import Protocol
Protocol
class class ProductProduct (( ProtocolProtocol ):
):
pkpk : : int
int
namename : : str
str
priceprice : : int
int
def def __str____str__ (( selfself ) ) -> -> strstr :
:
...
...
Note: The use of class attributes and ellipses (...
) in the method definition are new features in Python 3.7. In earlier versions of Python, it isn’t possible to define a Protocol using this syntax. Instead of an ellipsis, methods should have pass
in the body. Class attributes such as pk
and name
can be defined using the @attribute
decorator, but it will not work with Django models.
注意:在方法定义中使用类属性和省略号( ...
)是Python 3.7的新功能。 在Python的早期版本中,无法使用此语法定义协议。 方法应该在体内pass
,而不是省略号。 可以使用@attribute
装饰器来定义pk
和name
类的类属性,但不适用于Django模型。
You can now use the Product
protocol to add type information. For example, in add_item()
, you accept an instance of a product and add it to the cart:
现在,您可以使用Product
协议添加类型信息。 例如,在add_item()
,您接受产品的实例并将其添加到购物车中:
Running mypy
on this function will not yield any warnings. Let’s say you change product.pk
to product.id
, which is not defined in the Product
protocol:
在此函数上运行mypy
不会产生任何警告。 假设您将product.pk
更改为product.id
,这在Product
协议中未定义:
def def add_itemadd_item (
(
selfself ,
,
productproduct : : ProductProduct ,
,
) ) -> -> 'CartItem''CartItem' :
:
product_content_type product_content_type = = ContentTypeContentType .. objectsobjects .. get_for_modelget_for_model (( productproduct )
)
return return CartItemCartItem .. objectsobjects .. createcreate (
(
cartcart == selfself ,
,
product_content_typeproduct_content_type == product_content_typeproduct_content_type ,
,
product_object_idproduct_object_id == productproduct .. idid ,
,
)
)
You will get the following warning from Mypy:
您将从Mypy得到以下警告:
Note: Protocol
is not yet a part of Mypy. It’s part of the complementary package called mypy_extentions
. The package is developed by the Mypy team and includes features that they thought weren’t ready for the main Mypy package yet.
注意: Protocol
还不是Mypy的一部分。 它是名为mypy_extentions
的补充软件包的一部分。 该软件包由Mypy团队开发,并包含他们认为尚未准备好用于Mypy主软件包的功能。
Pros
优点
-
Migrations are not needed to add product types: The generic foreign key can reference any model. Adding a new type of product does not require migrations.
-
Any model can be used as an item: Using generic foreign key, any model can be referenced by the
Item
model. -
Built-in admin support: Django has built-in support for generic foreign keys in the admin. It can inline, for example, information about the referenced models in the detail page.
-
Self-contained module: There is no direct dependency between the products module and the cart module. This makes this approach ideal for existing projects and pluggable modules.
不需要迁移即可添加产品类型:通用外键可以引用任何模型。 添加新型产品不需要迁移。
任何模型都可以用作项目:使用通用外键,
Item
模型可以引用任何模型。内置的管理员支持: Django 在admin中具有对通用外键的内置支持 。 例如,它可以在详细信息页面中内联有关参考模型的信息。
独立模块: products模块和cart模块之间没有直接依赖关系。 这使得该方法非常适合现有项目和可插拔模块。
Cons
缺点
-
Can produce inefficient queries: The ORM cannot determine in advance what models are referenced by the generic foreign key. This makes it very difficult for it to optimize queries that fetch multiple types of products.
-
Harder to understand and maintain: Generic foreign key eliminates some Django ORM features that require access to specific product models. Accessing information from the product models requires writing more code.
-
Typing requires
Protocol
: Mypy is unable to provide type checking for generic models. AProtocol
is required.
可能产生效率低下的查询: ORM无法预先确定通用外键引用的模型。 这使其很难优化获取多种类型产品的查询。
难以理解和维护:通用外键消除了一些需要访问特定产品模型的Django ORM功能。 从产品模型访问信息需要编写更多代码。
键入需要
Protocol
: Mypy无法提供泛型模型的类型检查。 需要一个Protocol
。
Use Case
用例
Generic foreign keys are a great choice for pluggable modules or existing projects. The use of GenericForeignKey
and structural subtyping abstract any direct dependency between the modules.
通用外键是可插拔模块或现有项目的理想选择。 GenericForeignKey
和结构子类型的使用抽象了模块之间的任何直接依赖关系。
In the bookstore example, the book and e-book models can exist in a separate app and new products can be added without changing the cart module. For existing projects, a Cart
module can be added with minimal changes to existing code.
在书店示例中,书籍和电子书模型可以存在于单独的应用中,并且可以在不更改购物车模块的情况下添加新产品。 对于现有项目,可以添加Cart
模块,而对现有代码的更改最少。
The patterns presented in this article play nicely together. Using a mixture of patterns, you can eliminate some of the disadvantages and optimize the schema for your use case.
本文介绍的模式可以很好地配合使用。 使用多种模式,您可以消除一些缺点并针对您的用例优化模式。
For example, in the generic foreign key approach, you were unable to get the price of the entire cart quickly. You had to fetch each item separately and aggregate. You can address this specific concern by inlining the price of the product on the Item
model (the sparse model approach). This will allow you to query only the Item
model to get the total price very quickly.
例如,在通用外键方法中,您无法快速获得整个购物车的价格。 您必须分别获取每个项目并进行汇总。 您可以通过在Item
模型上内联产品的价格(稀疏模型方法)来解决此特定问题。 这将使您仅查询Item
模型即可非常Swift地获得总价。
结论 (Conclusion)
In this article, you started with a small town bookstore and grew it to a big e-commerce website. You tackled different types of problems and adjusted your model to accommodate the changes. You learned that problems such as complex code and difficulty adding new programmers to the team are often symptoms of a larger problem. You learned how to identify these problems and solve them.
在本文中,您从一家小镇的书店开始,然后发展成为一个大型的电子商务网站。 您解决了不同类型的问题,并调整了模型以适应变化。 您了解到诸如复杂代码和难以向团队中添加新程序员之类的问题通常是更大问题的征兆。 您学习了如何识别并解决这些问题。
You now know how to plan and implement a polymorphic model using the Django ORM. You’re familiar with multiple approaches, and you understand their pros and cons. You’re able to analyze your use case and decide on the best course of action.
现在,您知道如何使用Django ORM规划和实现多态模型。 您熟悉多种方法,并且了解它们的优缺点。 您能够分析用例并确定最佳的操作方案。
翻译自: https://www.pybloggers.com/2019/01/modeling-polymorphism-in-django-with-python/