本文档描述了 Model
API 的细节。它建立在 模型 和 数据库查询 指南中所介绍的材料基础上,因此,在阅读本文档之前,你可能需要阅读并理解这些文档。
在整篇参考中,我们将使用 数据库查询指南 中的 Weblog 示例模型 。
要创建一个新的模型实例,像其他 Python 类一样实例化它。
Model
(**kwargs)¶关键字参数是你在模型上定义的字段名。请注意,实例化一个模型不会触及你的数据库;为此,你需要 save()
。
注解
你可能会想通过覆盖 __init__
方法来定制模型。但是,如果你这样做,请注意不要更改调用签名,因为任何更改都可能阻止模型实例被保存。与其覆盖 __init__
,不如尝试使用以下方法之一:
在模型类上增加一个类方法:
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=100)
@classmethod
def create(cls, title):
book = cls(title=title)
# do something with the book
return book
book = Book.create("Pride and Prejudice")
在自定义管理器上添加一个方法(通常首选):
class BookManager(models.Manager):
def create_book(self, title):
book = self.create(title=title)
# do something with the book
return book
class Book(models.Model):
title = models.CharField(max_length=100)
objects = BookManager()
book = Book.objects.create_book("Pride and Prejudice")
Model.
from_db
(db, field_names, values)¶from_db()
方法可以在数据库加载时用于自定义模型实例创建。
db
参数包含模型从数据库加载的数据库别名,field_names
包含所有加载字段的名称,values
包含 field_names
中每个字段的加载值。field_names
和 values
的顺序相同。如果模型的所有字段都存在,那么 values
就必须按照 __init__()
预期的顺序。也就是说,实例可以通过 cls(*values)
来创建。如果有任何字段被推迟,它们将不会出现在 field_names
中。在这种情况下,给每个缺失的字段分配一个 django.db.models.DEFERRED
的值。
除了创建新的模型外,from_db()
方法必须在新实例的 _state
属性中设置 adding
和 db
标志。
下面是一个例子,说明如何记录从数据库中加载字段的初始值:
from django.db.models import DEFERRED
@classmethod
def from_db(cls, db, field_names, values):
# Default implementation of from_db() (subject to change and could
# be replaced with super()).
if len(values) != len(cls._meta.concrete_fields):
values = list(values)
values.reverse()
values = [
values.pop() if f.attname in field_names else DEFERRED
for f in cls._meta.concrete_fields
]
instance = cls(*values)
instance._state.adding = False
instance._state.db = db
# customization to store the original field values on the instance
instance._loaded_values = dict(zip(field_names, values))
return instance
def save(self, *args, **kwargs):
# Check how the current values differ from ._loaded_values. For example,
# prevent changing the creator_id of the model. (This example doesn't
# support cases where 'creator_id' is deferred).
if not self._state.adding and (
self.creator_id != self._loaded_values['creator_id']):
raise ValueError("Updating the value of creator isn't allowed")
super().save(*args, **kwargs)
上面的例子显示了一个完整的 from_db()
实现,以说明如何做到这一点。在这种情况下,可以在 from_db()
方法中使用 super()
调用。
如果你从模型实例中删除了一个字段,再次访问它就会从数据库中重新加载该值:
>>> obj = MyModel.objects.first()
>>> del obj.field
>>> obj.field # Loads the field from the database
Model.
refresh_from_db
(using=None, fields=None)¶如果你需要从数据库中重新加载一个模型的值,你可以使用 refresh_from_db()
方法。当这个方法被调用时,没有参数时,会做以下工作:
只有模型的字段会从数据库中重载。其他依赖于数据库的值,如注释,不会被重载。任何 @cached_property
属性也不会被清除。
重载发生在实例被加载的数据库中,如果实例不是从数据库中加载的,则从默认数据库中加载。using
参数可以用来强制使用数据库进行重载。
可以通过使用 fields
参数强制加载一组字段。
例如,为了测试 update()
的调用是否导致了预期的更新,你可以写一个类似这样的测试:
def test_update_result(self):
obj = MyModel.objects.create(val=1)
MyModel.objects.filter(pk=obj.pk).update(val=F('val') + 1)
# At this point obj.val is still 1, but the value in the database
# was updated to 2. The object's updated value needs to be reloaded
# from the database.
obj.refresh_from_db()
self.assertEqual(obj.val, 2)
请注意,当访问递延字段时,递延字段的值的加载是通过这个方法发生的。因此,可以自定义递延加载的发生方式。下面的例子显示了当一个递延字段被重载时,如何重载实例的所有字段:
class ExampleModel(models.Model):
def refresh_from_db(self, using=None, fields=None, **kwargs):
# fields contains the name of the deferred field to be
# loaded.
if fields is not None:
fields = set(fields)
deferred_fields = self.get_deferred_fields()
# If any deferred field is going to be loaded
if fields.intersection(deferred_fields):
# then load all of them
fields = fields.union(deferred_fields)
super().refresh_from_db(using, fields, **kwargs)
Model.
get_deferred_fields
()¶一个辅助方法,返回一个包含当前这个模型的所有这些字段的属性名的集合。
验证一个模型有三个步骤:
Model.clean_fields()
Model.clean()
Model.validate_unique()
当你调用一个模型的 full_clean()
方法时,这三个步骤都会被执行。
当你使用一个 ModelForm
时,调用 is_valid()
将对表单中包含的所有字段执行这些验证步骤。更多信息请参见 模型表单文档。只有当你打算自己处理验证错误,或者你从 ModelForm
中排除了需要验证的字段时,才需要调用模型的 full_clean()
方法。
Model.
full_clean
(exclude=None, validate_unique=True)¶这个方法依次调用 Model.clean_fields()
、 Model.clean()
和 Model.validate_unique()
(如果 validate_unique
为 True
),并引发一个 ValidationError
,该方法的 message_dict
属性包含了所有三个阶段的错误。
可选的 exclude
参数可以用来提供一个可以从验证和清理中排除的字段名列表。 ModelForm
使用这个参数来排除那些不存在于你的表单中的字段进行验证,因为任何引发的错误都无法被用户纠正。
请注意,当您调用模型的 save()
方法时,full_clean()
不会 自动调用。当你想为自己手动创建的模型运行一步模型验证时,你需要手动调用它。例如:
from django.core.exceptions import ValidationError
try:
article.full_clean()
except ValidationError as e:
# Do something based on the errors contained in e.message_dict.
# Display them to a user, or handle them programmatically.
pass
full_clean()
执行的第一步是清理每个单独的字段。
Model.
clean_fields
(exclude=None)¶这个方法将验证你模型上的所有字段。可选的 exclude
参数让你提供一个要从验证中排除的字段名列表。如果有任何字段没有通过验证,它将引发一个 ValidationError
。
full_clean()
执行的第二步是调用 Model.clean()
。这个方法应该被重写,以便对你的模型进行自定义验证。
Model.
clean
()¶这个方法应该用来提供自定义模型验证,如果需要的话,还可以修改模型上的属性。例如,你可以使用它来自动为一个字段提供一个值,或进行需要访问多个字段的验证:
import datetime
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _
class Article(models.Model):
...
def clean(self):
# Don't allow draft entries to have a pub_date.
if self.status == 'draft' and self.pub_date is not None:
raise ValidationError(_('Draft entries may not have a publication date.'))
# Set the pub_date for published items if it hasn't been set already.
if self.status == 'published' and self.pub_date is None:
self.pub_date = datetime.date.today()
但请注意,像 Model.full_clean()
一样,当你调用你的模型的 save()
方法时,模型的 clean()
方法不会被调用。
在上面的例子中,由 Model.clean()
引发的 ValidationError
异常是用字符串实例化的,所以它将被存储在一个特殊的错误字典键中, NON_FIELD_ERRORS
。这个键用于与整个模型相关的错误,而不是与某个特定字段相关的错误:
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
try:
article.full_clean()
except ValidationError as e:
non_field_errors = e.message_dict[NON_FIELD_ERRORS]
要将异常分配给一个特定的字段,用一个字典实例化 ValidationError
,其中键是字段名。我们可以更新前面的例子,将错误分配给 pub_date
字段:
class Article(models.Model):
...
def clean(self):
# Don't allow draft entries to have a pub_date.
if self.status == 'draft' and self.pub_date is not None:
raise ValidationError({'pub_date': _('Draft entries may not have a publication date.')})
...
如果你在 Model.clean()
期间检测到多个字段的错误,你也可以传递一个字段名与错误映射的字典:
raise ValidationError({
'title': ValidationError(_('Missing title.'), code='required'),
'pub_date': ValidationError(_('Invalid date.'), code='invalid'),
})
最后,full_clean()
将检查你模型上的任何唯一约束。
如果字段没有出现在 ModelForm
中,如何引发特定字段的验证错误。
你不能在 Model.clean()
中对没有出现在模型表单中的字段提出验证错误(一个表单可以使用 Meta.field
或 Meta.exclude
来限制它的字段)。这样做会引发一个 ValueError
,因为验证错误将无法与被排除的字段相关联。
为了解决这个难题,可以覆盖 Model.clean_fields()
,因为它接收的是被排除在验证之外的字段列表。例如:
class Article(models.Model):
...
def clean_fields(self, exclude=None):
super().clean_fields(exclude=exclude)
if self.status == 'draft' and self.pub_date is not None:
if exclude and 'status' in exclude:
raise ValidationError(
_('Draft entries may not have a publication date.')
)
else:
raise ValidationError({
'status': _(
'Set status to draft if there is not a '
'publication date.'
),
})
Model.
validate_unique
(exclude=None)¶这个方法类似于 clean_fields()
,但验证的是模型上所有的唯一性约束,而不是单个字段值。可选的 exclude
参数允许你提供一个要从验证中排除的字段名列表。如果任何字段验证失败,它将引发一个 ValidationError
。
请注意,如果你为 validate_unique()
提供了一个 exclude
参数,任何涉及你提供的一个字段的 unique_together
约束将不会被检查。
要将对象保存回数据库,调用 save()
:
Model.
save
(force_insert=False, force_update=False, using=DEFAULT_DB_ALIAS, update_fields=None)¶For details on using the force_insert
and force_update
arguments, see
强制执行 INSERT 或 UPDATE. Details about the update_fields
argument
can be found in the 指定要保存的字段 section.
如果你想自定义保存行为,你可以覆盖这个 save()
方法。更多细节请参见 重写之前定义的模型方法。
模型保存过程也有一些微妙的地方,请看下面的章节。
如果一个模型有一个 AutoField
——一个自动递增的主键,那么当你第一次调用 save()
时,这个自动递增的值就会被计算出来并保存为你的对象的一个属性:
>>> b2 = Blog(name='Cheddar Talk', tagline='Thoughts on cheese.')
>>> b2.id # Returns None, because b2 doesn't have an ID yet.
>>> b2.save()
>>> b2.id # Returns the ID of your new object.
在你调用 save()
之前,没有办法知道一个 ID 的值是多少,因为这个值是由你的数据库计算出来的,而不是由 Django 计算出来的。
为了方便起见,每个模型都有一个 AutoField
默认命名为 id
,除非你在模型中的字段上明确指定 primary_key=True
。更多细节请参见 AutoField
的文档。
pk
属性¶Model.
pk
¶无论你是自己定义一个主键字段,还是让 Django 为你提供一个主键字段,每个模型都会有一个叫做 pk
的属性。它的行为就像模型上的一个普通属性,但实际上是模型主键字段属性的别名。您可以像读取和设置任何其他属性一样读取和设置这个值,它将更新模型中的正确字段。
如果一个模型有一个 AutoField
,但你想在保存时显式地定义一个新对象的 ID,就在保存前显式地定义它,而不是依赖 ID 的自动分配:
>>> b3 = Blog(id=3, name='Cheddar Talk', tagline='Thoughts on cheese.')
>>> b3.id # Returns 3.
>>> b3.save()
>>> b3.id # Returns 3.
如果你手动分配自动主键值,请确保不要使用一个已经存在的主键值!如果你创建一个新的对象,并使用一个已经存在于数据库中的显式主键值,Django 会认为你是在改变现有的记录,而不是创建一个新的记录。
考虑到上面的 'Cheddar Talk'
博客的例子,这个例子将覆盖数据库中以前的记录:
b4 = Blog(id=3, name='Not Cheddar', tagline='Anything but cheese.')
b4.save() # Overrides the previous blog with ID=3!
发生这种情况的原因,请看下面 How Django knows to UPDATE vs. INSERT 。
明确指定自动主键值主要用于批量保存对象,当你确信不会发生主键碰撞时。
如果你使用的是 PostgreSQL,与主键相关的序列可能需要更新;参见 手动指定自增主键的值。。
当你保存一个对象时,Django 会执行以下步骤:
发送一个预保存信号。 pre_save
信号被发送,允许任何监听该信号的函数做一些事情。
预处理数据。 每个字段的 pre_save()
方法被调用来执行任何需要的自动数据修改。例如,日期/时间字段重写了 pre_save()
来实现 auto_now_add
和 auto_now
。
为数据库准备数据。 要求每个字段的 get_db_prep_save()
方法提供其当前的值,数据类型可以写入数据库。
大多数字段不需要数据准备。简单的数据类型,如整数和字符串,作为一个 Python 对象是“可以写入”的。然而,更复杂的数据类型通常需要一些修改。
例如,DateField
字段使用 Python datetime
对象来存储数据。数据库不存储 datetime
对象,所以字段值必须转换成符合 ISO 标准的日期字符串才能插入数据库。
将数据插入数据库。 将预先处理、准备好的数据组成 SQL 语句,以便插入数据库。
发送一个保存后的信号。 post_save
信号被发送,允许任何监听该信号的函数做一些事情。
你可能已经注意到 Django 数据库对象使用相同的 save()
方法来创建和更改对象。Django 抽象了需要使用 INSERT
或 UPDATE
的 SQL 语句。具体来说,当你调用 save()
,而对象的主键属性 没有 定义一个 default
时,Django 会遵循这个算法。
True
(即,一个不是 None
或空字符串的值),Django 会执行 UPDATE
。UPDATE
没有更新任何东西(例如主键被设置为数据库中不存在的值),Django 会执行 INSERT
。如果对象的主键属性定义了一个 default
,那么如果它是一个现有的模型实例,并且主键被设置为数据库中存在的值,Django 就会执行一个 UPDATE
。否则,Django 会执行一个 INSERT
。
这里的一个问题是,如果你不能保证主键值未被使用,那么在保存新对象时,你应该注意不要显式地指定一个主键值。关于这个细微的差别,请看上面的 Explicitly specifying auto-primary-key values 和下面的 Forcing an INSERT or UPDATE 。
在 Django 1.5 和更早的版本中,当主键属性被设置时,Django 执行 SELECT
。如果 SELECT
找到了一条记录,那么 Django 就会进行 UPDATE
,否则就会进行 INSERT
。老算法的结果是在 UPDATE
的情况下多了一个查询。在一些罕见的情况下,即使数据库中包含了一条对象主键值的记录,数据库也不会报告某行被更新。一个例子是 PostgreSQL 的 ON UPDATE
触发器,它返回 NULL
。在这种情况下,可以通过将 select_on_save
选项设置为 True
来恢复到旧算法。
有时你需要在一个字段上执行一个简单的算术任务,比如递增或递减当前值。一种方法是在 Python 中进行运算,比如:
>>> product = Product.objects.get(name='Venezuelan Beaver Cheese')
>>> product.number_sold += 1
>>> product.save()
如果从数据库中检索到的 number_sold
旧值是 10,那么 11 的值将被写回数据库。
这个过程可以变得更健壮, 避免竞争条件,以及通过表达相对于原始字段值的更新,而不是作为一个新值的显式赋值来稍微加快。Django 提供了 F 表达式
来执行这种相对更新。使用 F 表达式
,前面的例子表示为:
>>> from django.db.models import F
>>> product = Product.objects.get(name='Venezuelan Beaver Cheese')
>>> product.number_sold = F('number_sold') + 1
>>> product.save()
如果 save()
在关键字参数 update_fields
中传递了一个字段名列表,那么只有列表中命名的字段才会被更新。如果你只想更新一个对象上的一个或几个字段,这可能是可取的。防止数据库中所有的模型字段被更新会有轻微的性能优势。例如:
product.name = 'Name changed again'
product.save(update_fields=['name'])
update_fields
参数可以是任何包含字符串的可迭代对象。一个空的 update_fields
可迭代对象将跳过保存。值为 None
将对所有字段进行更新。
指定 update_fields
将强制更新。
当保存一个通过延迟模型加载获取的模型时(only()
或 defer()
),只有从数据库加载的字段会被更新。实际上,在这种情况下有一个自动的 update_fields
。如果你分配或改变任何延迟字段的值,该字段将被添加到更新的字段中。
Model.
delete
(using=DEFAULT_DB_ALIAS, keep_parents=False)¶为该对象发出一个 SQL DELETE
。这只是删除数据库中的对象;Python 实例仍然存在,并且在其字段中仍然有数据。这个方法返回被删除的对象的数量和一个包含每个对象类型删除数量的字典。
更多细节,包括如何批量删除对象,请参见 删除对象。
如果你想自定义删除行为,你可以覆盖 delete()
方法。更多细节请参见 重写之前定义的模型方法。
有时,在 多表继承 中,你可能只想删除子模型的数据,指定 keep_parents=True
将保留父模型的数据。指定 keep_parents=True
将保留父模型的数据。
有几个对象方法有特殊用途。
__str__()
¶Model.
__str__
()¶每当你对一个对象调用 str()
时,就会调用 __str__()
方法。Django 在很多地方使用了 str(obj)
方法。最主要的是,在 Django 管理站点中显示一个对象,以及作为模板显示对象时插入的值。因此,你应该总是从 __str__()
方法中返回一个漂亮的、人类可读的模型表示。
例子:
from django.db import models
class Person(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
def __str__(self):
return '%s %s' % (self.first_name, self.last_name)
__eq__()
¶Model.
__eq__
()¶相等方法的定义是,具有相同主键值和相同具体类的实例被认为是相等的,但主键值为 None
的实例除自身外对任何事物都不相等。对于代理模型,具体类被定义为模型的第一个非代理父类;对于所有其他模型,它只是模型的类。
例子:
from django.db import models
class MyModel(models.Model):
id = models.AutoField(primary_key=True)
class MyProxyModel(MyModel):
class Meta:
proxy = True
class MultitableInherited(MyModel):
pass
# Primary keys compared
MyModel(id=1) == MyModel(id=1)
MyModel(id=1) != MyModel(id=2)
# Primary keys are None
MyModel(id=None) != MyModel(id=None)
# Same instance
instance = MyModel(id=None)
instance == instance
# Proxy model
MyModel(id=1) == MyProxyModel(id=1)
# Multi-table inheritance
MyModel(id=1) != MultitableInherited(id=1)
__hash__()
¶Model.
__hash__
()¶__hash__()
方法是基于实例的主键值。它实际上是 hash(obj.pk)
。如果实例没有主键值,那么将引发一个 TypeError
(否则 __hash__()
方法会在保存实例之前和之后返回不同的值,但是在 Python 中禁止改变实例的 __hash__()
值。
get_absolute_url()
¶Model.
get_absolute_url
()¶定义一个 get_absolute_url()
方法来告诉 Django 如何计算一个对象的标准 URL。对于调用者来说,这个方法应该返回一个字符串,可以通过 HTTP 引用对象。
例子:
def get_absolute_url(self):
return "/people/%i/" % self.id
虽然这段代码正确且简单,但它可能不是写这种方法的最可移植的方式。reverse()
函数通常是最好的方法。
例子:
def get_absolute_url(self):
from django.urls import reverse
return reverse('people-detail', kwargs={'pk' : self.pk})
Django 使用 get_absolute_url()
的一个地方就是在管理应用中。如果一个对象定义了这个方法,那么对象编辑页面会有一个“View on site”的链接,直接跳转到对象的公开视图,就像 get_absolute_url()
给出的那样。
类似的,Django 的其他几个部分,比如 联合供稿框架,当定义了 get_absolute_url()
时,也会使用 get_absolute_url()
。如果你的模型的每个实例都有一个唯一的 URL,你应该定义 get_absolute_url()
。
警告
你应该避免从未经验证的用户输入中建立 URL,以减少链接或重定向中毒的可能性:
def get_absolute_url(self):
return '/%s/' % self.name
如果 self.name
是 '/example.com'
,这将返回 '//example.com/'
,这反过来又是一个有效的协议相对 URL,但不是预期的 '/%2Fexample.com/'
。
在模板中使用 get_absolute_url()
,而不是硬编码你的对象的 URL,这是一个很好的做法。例如,这个模板代码就很糟糕:
<!-- BAD template code. Avoid! -->
<a href="/people/{{ object.id }}/">{{ object.name }}</a>
这个模板代码就好多了:
<a href="{{ object.get_absolute_url }}">{{ object.name }}</a>
这里的逻辑是,如果你改变了你的对象的 URL 结构,即使是为了纠正拼写错误这样的小事,你也不想追踪 URL 可能被创建的每个地方。在 get_absolute_url()
中指定一次,然后让你的其他代码调用那个地方。
注解
从 get_absolute_url()
返回的字符串 必须 只包含 ASCII 字符(URI 规范要求,RFC 2396#section-2),并在必要时进行 URL 编码。
调用 get_absolute_url()
的代码和模板应该可以直接使用结果,而不需要任何进一步的处理。如果你使用的字符串包含 ASCII 码范围以外的字符,你可能希望使用 django.utils.encoding.iri_to_uri()
函数来帮助解决这个问题。
除了 save()
、 delete()
之外,一个模型对象还可能有以下一些方法:
Model.
get_FOO_display
()¶对于每一个设置了 choice
的字段,该对象将有一个 get_FOO_display()
方法,其中 FOO
是字段的名称。该方法返回字段的“人类可读”值。
例子:
from django.db import models
class Person(models.Model):
SHIRT_SIZES = (
('S', 'Small'),
('M', 'Medium'),
('L', 'Large'),
)
name = models.CharField(max_length=60)
shirt_size = models.CharField(max_length=2, choices=SHIRT_SIZES)
>>> p = Person(name="Fred Flintstone", shirt_size="L")
>>> p.save()
>>> p.shirt_size
'L'
>>> p.get_shirt_size_display()
'Large'
增加了对 ArrayField
和 RangeField
的支持。
Model.
get_next_by_FOO
(**kwargs)¶Model.
get_previous_by_FOO
(**kwargs)¶对于每一个 DateField
和 DateTimeField
没有 null=True
,该对象将有 get_next_by_FOO()
和 get_previous_by_FOO()
方法,其中 FOO
是字段名。这将返回与日期字段相关的下一个和上一个对象,适当时引发一个 DoesNotExist
异常。
这两种方法都将使用模型的默认管理器执行查询。如果你需要模拟自定义管理器使用的过滤,或者想要执行一次性的自定义过滤,这两种方法也都接受可选的关键字参数,其格式应该是 字段查找 中描述的格式。
请注意,在日期值相同的情况下,这些方法将使用主键作为比较。这保证了没有记录被跳过或重复。这也意味着你不能对未保存的对象使用这些方法。
覆盖额外的实例方法
在大多数情况下,覆盖或继承 get_FOO_display()
、get_next_by_FOO()
和 get_previous_by_FOO()
应按预期工作。然而,由于它们是由元类添加的,所以要考虑所有可能的继承结构是不实际的。在更复杂的情况下,你应该覆盖 Field.contribution_to_class()
来设置你需要的方法。
12月 07, 2021