本文完整目录请见 Django 3网页开发指南 - 第4版
本章包含如下主题:
- 使用模型mixin
- 通过URL相关的方法创建模型mixin
- 创建模型mixin来处理创建和变更日期
- 创建模型mixin来处理元标签
- 创建模型mixin来处理通用关系
- 处理多语言字段
- 操作模型翻译数据表
- 规避循环依赖
- 添加数据库约束
- 使用迁移
- 修改外键为many-to-many字段
引言
在启动新应用时,首先是创建能代表数据库结构的模型。假设你已经创建过Django应用或者至少是阅读并掌握了Django官方课程。本章中,你将学习到一些技术,能让项目中的不同应用保持数据库结构的连续性。然后,你将学习到如何处理数据库中数据的国际化。再后,你将学习到如何在模型中避免循环依赖以及如何设置数据库约束。在本章的最后,你将学习到如何在开发的过程中使用迁移来修改数据库结构。
技术要求
使用本书中的代码,需要有稳定版本的Python、MySQL或PostgreSQL数据库,以及位于虚拟环境中的Django项目。
可以在GitHub仓库的Chapter02目录中查看本章的代码。
使用模型mixin
在面向对象语言如Python中,mixin类可看作带有实现功能的接口。在模型继承mixin时,它继承该接口并包含其所有的字段、属性(attributes)、私有属性(property)和方法。Django模型中的mixin可在不同模型中多次复用通用功能。Django中的模型mixin是抽象模型基类。我们将在下面的几个小节中进行探讨。
准备工作
首先需要创建可复用的mixin。把模型mixin放在myproject.apps.core中会是一个不错的选择。如果创建一个会与他人共享的可复用应用,将模型mixin放在可复用应用本身当中,可以是在base.py文件中。
如何实现...
打开任意希望使用mixin的Django应用的models.py文件,并输入如下代码:
# myproject/apps/ideas/models.py
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from myproject.apps.core.models import (
CreationModificationDateBase,
MetaTagsBase,
UrlBase,
)
class Idea(CreationModificationDateBase, MetaTagsBase, UrlBase):
title = models.CharField(
_("Title"),
max_length=200,
)
content = models.TextField(
_("Content"),
)
# other fields...
class Meta:
verbose_name = _("Idea")
verbose_name_plural = _("Ideas")
def __str__(self):
return self.title
def get_url_path(self):
return reverse("idea_details", kwargs={
"idea_id": str(self.pk),
})
实现原理...
Django的模型继承支持3种类型的继承:抽象基类、多表继承和代码模型。模型mixin是抽象模型类,在其中我们通过抽象Meta类以指定的字段、属性和方法进行定义。在创建像前例中所示的Idea这样的模型时,它会继承CreationModificationDateMixin、MetaTagsMixin和UrlMixin的所有功能。这些抽象类的所有字段在数据表中以继承模型的字段进行保存。下面的小节中,我们将学习如何定义模型mixin。
扩展知识...
在常规的Python类继承中,如果有一个以上的基类,它们都会实现具体的方法,可以调用子类 实例上的方法,仅第一个父类的方法会被调用,如下例所示:
>>> class A(object):
... def test(self):
... print("A.test() called")
...
>>> class B(object):
... def test(self):
... print("B.test() called")
...
>>> class C(object):
... def test(self):
... print("C.test() called")
...
>>> class D(A, B, C):
... def test(self):
... super().test()
... print("D.test() called")
>>> d = D()
>>> d.test()
A.test() called
D.test() called
这与Django模型基类相同;但有一个特例。
ℹ️Django框架通过元类调用每个基类的save()和delete()方法实现了一些特殊功能。
这表示你可以放心地通过重写save()和delete()方法对在mixin中特别定义的指定字段进行保存前、保存后、删除前和删除后的操作。
要学习有关模型继承不同类型的更多知识,参数Django官方文档中的模型继承部分。
相关内容
- 通过URL相关的方法创建模型mixin一节
- 创建模型mixin来处理创建和变更日期一节
- 创建模型mixin来处理元标签一节
通过URL相关的方法创建模型mixin
对于每个有自己独立详情页的模型,定义一个get_absolute_url()是一种好做法。这一方法可在模板中使用并用于在Django后台站点中预览已保存对象。但get_absolute_url()本身有些歧义,它返回的是URL路径而非完整URL。
在这一小节中,我们将学习如何创建为模型具体的URL提供简化支持的模型mixin。这一mixin让我们可以做如下事情:
- 允许我们在模型中定义URL路径或完整URL
- 自动根据我们的定义生成其它URL
- 在后台定义 get_absolute_url() 方法
准备工作
如尚未创建,请创建myproject.apps.core应用,在其中存储模型mixin。然后在core包中创建models.py文件。此外,如果你创建的是可复用应用,可以将mixin放在应用的base.py文件中。
如何实现...
按照如下步骤逐一执行:
- 将如下内容添加到core应用的models.py文件中:
# myproject/apps/core/models.py
from urllib.parse import urlparse, urlunparse
from django.conf import settings
from django.db import models
class UrlBase(models.Model):
"""
A replacement for get_absolute_url()
Models extending this mixin should have either get_url or
get_url_path implemented.
"""
class Meta:
abstract = True
def get_url(self):
if hasattr(self.get_url_path, "dont_recurse"):
raise NotImplementedError
try:
path = self.get_url_path()
except NotImplementedError:
raise
return settings.WEBSITE_URL + path
get_url.dont_recurse = True
def get_url_path(self):
if hasattr(self.get_url, "dont_recurse"):
raise NotImplementedError
try:
url = self.get_url()
except NotImplementedError:
raise
bits = urlparse(url)
return urlunparse(("", "") + bits[2:])
get_url_path.dont_recurse = True
def get_absolute_url(self):
return self.get_url()
- 向开发、测试、预发布和生产设置添加WEBSITE_URL设置,后面不带斜线。例如,开发环境的设置如下:
# myproject/settings/dev.py
from ._base import *
DEBUG = True
WEBSITE_URL = "http://127.0.0.1:8000" # 最后面不带斜线
- 要在应用中使用mixin,通过core应用导入mixin,在模型类中继承该mixin,并定义get_url_path()方法如下:
# myproject/apps/ideas/models.py
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from myproject.apps.core.models import UrlBase
class Idea(UrlBase):
# 字段、属性、私有属性和方法...
def get_url_path(self):
return reverse("idea_details", kwargs={
"idea_id": str(self.pk),
})
实现原理...
UrlBase类是拥有3个方法的抽象模型,如下:
- get_url()获取对象的完整URL
- get_url_path()获取对象的绝对路径
- get_absolute_url()与get_url_path()方法类似
get_url()和get_url_path()方法会在继承后的模型类中进行重写,如Idea。可以定义get_url(),get_url_path()会将其变为路径。相应地也可以定义get_url_path(),而get_url() 会在其路径前添加网站URL。
ℹ️一条黄金法则是保持重写 get_url_path()方法。
在模板中,在需要链接到同一网站上的对象时使用get_url_path(),如下:
{{ idea.title }}
对于外部通讯的链接,如email、RSS feed或API,使用get_url(),示例如下:
{{ idea.title }}
默认的get_absolute_url()方法会在Django模型管理后台中使用,用于“在网站中浏览”的功能,也可以由第三方Django应用所使用。
扩展知识...
通常来说,不要在URL中使用递增主键,因为这样不太安全,会将主键暴露给终端用户:数据总条数会对用户可见,也太易于通过修改URL路径来访问不同数据了。
在详情页URL中使用主键时,主键应用为UUID(通用唯一识别码)或随机生成的字符串。否则请像下面这样创建并使用slug字段:
class Idea(UrlBase):
slug = models.SlugField(_("Slug for URLs"), max_length=50)
相关内容
- 使用模型mixin
- 创建模型mixin来处理创建和变更日期
- 创建模型mixin来处理元标签
- 创建模型mixin来处理通用关系
- 第1章 Django 3.0入门中的为开发、测试、预发布和生产环境配置设置一节
创建模型mixin来处理创建和变更日期
在模型中包含时间戳供创建和修改模型实例时使用非常普遍。本小节中,我们将学习如何创建一个对模型保存创建和修改日期的简单模型mixin。使用这类mixin可以确保每个模型对时间戳使用相同的字段名,并具有同样的行为。
准备工作
如尚未创建,请创建myproject.apps.core包来保存mixin。然后,在core包中创建models.py文件。
如何实现...
打开myprojects.apps.core包中的models.py文件,并插入如下内容:
# myproject/apps/core/models.py
from django.db import models
from django.utils.translation import gettext_lazy as _
class CreationModificationDateBase(models.Model):
"""
Abstract base class with a creation and modification date and time
"""
created = models.DateTimeField(
_("Creation Date and Time"),
auto_now_add=True,
)
modified = models.DateTimeField(
_("Modification Date and Time"),
auto_now=True,
)
class Meta:
abstract = True
实现原理...
CreationModificationDateMixin类是一个抽象模型,这表示继承模型类会在同一张数据表中创建所有字段,即这将会是一对一的关系,让数据表更难以处理。
这一mixin有两个日期时间字段,created和modified。借助auto_now_add和auto_now属性,在保存模型时会自动保存时间戳。这些字段会自动获取editable=False属性,因此会在后台管理表单中隐藏。如果在设置中USE_TZ设置为True(默认值且是推荐值),会使用适配时区的时间戳。否则,会使用原时区时间戳。适配时区的时间戳在数据库中以世界标准时间(UTC)进行保存,在读取或写入时会转换为项目的默认时间。原时区时间戳在数据库中以项目的本地时区进行存储,通常不太适用,因为这样会让跨时区的管理变得更为复杂。
要使用这一mixin,我们只需要导入并扩展模型即可,如下:
# myproject/apps/ideas/models.py
from django.db import models
from myproject.apps.core.models import CreationModificationDateBase
class Idea(CreationModificationDateBase):
# other fields, attributes, properties, and methods...
相关内容
- 使用模型mixin一节
- 创建模型mixin来处理元标签一节
- 创建模型mixin来处理通用关系一节
创建模型mixin来处理元标签
在针对搜索引擎优化站点时,不仅需要在每个页面使用语法标记,还需要包含一些适当的元标签。为保持最大化的灵活性,如果对在网站中具有自己详情页面的具体对象有定义通用元标签内容的方式会非常有帮助。本小节中,我们将学习如何创建包含关键词、描述、作者和版权元标签相关字段和方法的模型mixin。
准备工作
和前面小节一样,请确保有用于mixin的myproject.apps.core包。同时,在包下创建一个目录结构templates/utils/includes/,并在其中创建一个meta.html文件用于存储基础元标签标记。
如何实现...
下面就开始创建模型mixin:
- 在设置文件的INSTALLED_APPS中添加myproject.apps.core,因为我们希望让模型中的templates目录生效。
- 在meta.html文件中添加如下基础meta标签:
{# templates/core/includes/meta.html #}
- 使用你常用的编辑器打开core包中的 models.py文件,并添加如下内容:
# myproject/apps/core/models.py
from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.utils.safestring import mark_safe
from django.template.loader import render_to_string
class MetaTagsBase(models.Model):
"""
Abstract base class for generating meta tags
"""
meta_keywords = models.CharField(
_("Keywords"),
max_length=255,
blank=True,
help_text=_("Separate keywords with commas."),
)
meta_description = models.CharField(
_("Description"),
max_length=255,
blank=True,
)
meta_author = models.CharField(
_("Author"),
max_length=255,
blank=True,
)
meta_copyright = models.CharField(
_("Copyright"),
max_length=255,
blank=True,
)
class Meta:
abstract = True
def get_meta_field(self, name, content):
tag = ""
if name and content:
tag = render_to_string("core/includes/meta.html", {
"name": name,
"content": content,
})
return mark_safe(tag)
def get_meta_keywords(self):
return self.get_meta_field("keywords", self.meta_keywords)
def get_meta_description(self):
return self.get_meta_field("description", self.meta_description)
def get_meta_author(self):
return self.get_meta_field("author", self.meta_author)
def get_meta_copyright(self):
return self.get_meta_field("copyright", self.meta_copyright)
def get_meta_tags(self):
return mark_safe("\n".join((
self.get_meta_keywords(),
self.get_meta_description(),
self.get_meta_author(),
self.get_meta_copyright(),
)))
实现原理...
这一mixin向模型添加4个进行扩展的字段:meta_keywords、meta_description、meta_author和meta_copyright。还添加了对应的get_*()方法用于渲染相关联的元标签。其中每一个都向核心的get_meta_field()方法传递了名称及相应的字面,该方法用于返回根据meta.html模板所渲染的标记。最后提供了一个get_meta_tags()快捷方法来对所有可用的元标签一次性地生成组合标记。
如果在模型中使用这一mixin,如Idea模型,即在本章一开始使用模型mixin一节中所使用的,可以在详情页模板中在HEAD中添加如下内容来渲染所有的meta标签:
{% block meta_tags %}
{{ block.super }}
{{ idea.get_meta_tags }}
{% endblock %}
这里meta_tags块在父模板中已进行了定义,并且这一代码块展示了子模板中如何重定义该代码块,首先以block.super包含父级中的内容,然后使用idea对象中的其它标签来扩展它。也可以通过使用{{ idea.get_meta_description }}这样在仅对具体meta标签进行渲染。
读者可能注意到了models.py的代码中,所渲染的元标签标记为safe,即不进行转义,我们不一定要使用safe模板过滤器。仅来自于数据库的值会进行转义,用于确保最终的HTML格式正确。meta_keywords及其它字段的数据库数据会在调用render_to_string() 或meta.html 模型时自动进行转义,因为该模板没有在内容中指定{% autoescape off %}。
相关内容
- 使用模型mixin一节
- 创建模型mixin来处理创建和变更日期一节
- 创建模型mixin来处理通用关系一节
- 第4章 模板和JavaScript
中的安排base.html模板一节
创建模型mixin来处理通用关系
除常规的数据库关联如外联关联或多对多关联外,Django有一种关联一个模型到任何其它模型的实例。这一概念称为通用关系(generic relation)。对于通用关联,我们会保存所关联模型的内容类型以及模型实例的ID。
本小节中,我们将学习如何在模型mixin中对通用关联进行抽象。
准备工作
要正常的学习本小节,需要安装一个contenttypes应用。默认它应当已经添加到了INSTALLED_APPS列表中,如以下代码所示:
# myproject/settings/_base.py
INSTALLED_APPS = [
# contributed
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# third-party
# ...
# local
"myproject.apps.core",
"myproject.apps.categories",
"myproject.apps.ideas",
]
同样,请确保已创建了供模型mixin使用的myproject.apps.core应用。
如何实现...
按照如下步骤来创建和使用通用关系的mixin:
- 在文本编辑器中打开core包中的models.py 文件,并添加如下内容:
# myproject/apps/core/models.py
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import FieldError
def object_relation_base_factory(
prefix=None,
prefix_verbose=None,
add_related_name=False,
limit_content_type_choices_to=None,
is_required=False):
"""
Returns a mixin class for generic foreign keys using "Content type - object ID" with dynamic field names. This function is just a class generator.
Parameters:
prefix: a prefix, which is added in front of the fields
prefix_verbose: a verbose name of the prefix, used to generate a title for the field column of the content object in the Admin
add_related_name: a boolean value indicating, that a related name for the generated content type foreign key should be added. This value should be true, if you use more than one ObjectRelationBase in your model.
The model fields are created using this naming scheme:
<>_content_type
<>_object_id
<>_content_object
"""
p = ""
if prefix:
p = f"{prefix}_"
prefix_verbose = prefix_verbose or _("Related object")
limit_content_type_choices_to = limit_content_type_choices_to or {}
content_type_field = f"{p}content_type"
object_id_field = f"{p}object_id" content_object_field = f"{p}content_object"
class TheClass(models.Model):
class Meta:
abstract = True
if add_related_name:
if not prefix:
raise FieldError("if add_related_name is set to "
"True, a prefix must be given")
related_name = prefix
else:
related_name = None
optional = not is_required
ct_verbose_name = _(f"{prefix_verbose}'s type (model)")
content_type = models.ForeignKey(
ContentType,
verbose_name=ct_verbose_name,
related_name=related_name,
blank=optional,
null=optional,
help_text=_("Please select the type (model) "
"for the relation, you want to build."),
limit_choices_to=limit_content_type_choices_to,
on_delete=models.CASCADE)
fk_verbose_name = prefix_verbose
object_id = models.CharField(
fk_verbose_name,
blank=optional,
null=False,
help_text=_("Please enter the ID of the related object."),
max_length=255,
default="") # for migrations
content_object = GenericForeignKey(
ct_field=content_type_field,
fk_field=object_id_field)
TheClass.add_to_class(content_type_field, content_type)
TheClass.add_to_class(object_id_field, object_id)
TheClass.add_to_class(content_object_field, content_object)
return TheClass
- 以下是如何在应用中使用两个通用关联的示例代码片断(放在ideas/models.py中):
# myproject/apps/ideas/models.py
from django.db import models
from django.utils.translation import gettext_lazy as _
from myproject.apps.core.models import (
object_relation_base_factory as generic_relation,
)
FavoriteObjectBase = generic_relation(
is_required=True,
)
OwnerBase = generic_relation(
prefix="owner",
prefix_verbose=_("Owner"),
is_required=True,
add_related_name=True,
limit_content_type_choices_to={
"model__in": (
"user",
"group",
)
}
)
class Like(FavoriteObjectBase, OwnerBase):
class Meta:
verbose_name = _("Like")
verbose_name_plural = _("Likes")
def __str__(self):
return _("{owner} likes {object}").format(
owner=self.owner_content_object,
object=self.content_object
)
实现原理...
可以看到,这段代码比前面的要复杂。
object_relation_base_factory函数,我们使用简短别名进行导入为generic_relation,并不是mixin本身;它是生成模型mixin的函数,即一个用于继承的抽象模型类。动态创建的mixin添加了content_type和object_id字段以及指定所关联实例的content_object通用外键。
为什么我们不直接定义带有这个三个属性的模型mixin呢?动态生成的抽象类让我们可以对每个字段名拥有前缀;因此,我们可以在同一个模型中拥有多个通用关联。例如,前面所展示的Like模型,会对所喜欢的对象拥有content_type、object_id和content_object字段,以及发送喜欢的对象(用户或组)的owner_content_type、owner_object_id和owner_content_object字段。
被我们简化为generic_relation的object_relation_base_factory函数,增加了由limit_content_type_choices_to参数限定内容类型选项的可能性。前面的示例限制定了owner_content_type选项为仅为User和Group模型的内容类型。
相关内容
- 通过URL相关的方法创建模型mixin一节
- 创建模型mixin来处理创建和变更日期一节
- 创建模型mixin来处理元标签一节
- 第4章 模板和JavaScript
中的实现Like组件一节
处理多语言字段
Django使用国际化机制来来在代码和模板中翻译可视化字符串。但如何在模型中实现多语言内容由开发者决定。我们将展示一些在项目中直接实现多语言模型的方式。第一种方法是在模型中使用特定语言的字段。
该方法有如下特点:
- 在模型中定义多语言字段一目了然
- 在数据库查询中使用多语言字段非常简单
- 可以使用已有的管理后台来编辑多语言字段模型,而无需做额外的修改
- 如若需要,可以轻松地在同一个模板中展示一个对象的所有翻译
- 在设置中修改语言数量后,需要在多语言中模型中创建字段并进行迁移
准备工作
有没有创建在本章前面小节中使用到的myproject.apps.core包?现在需要在core应用中新增model_fields.py文件,用于自定义模型字段。
如何实现...
执行如下步骤在定义多语言字符字符段及多语言文本字段:
- 打开model_fields.py文件,并创建如下的多语言基础字段:
# myproject/apps/core/model_fields.py
from django.conf import settings
from django.db import models
from django.utils.translation import get_language
from django.utils import translation
class MultilingualField(models.Field):
SUPPORTED_FIELD_TYPES = [models.CharField, models.TextField]
def __init__(self, verbose_name=None, **kwargs):
self.localized_field_model = None
for model in MultilingualField.SUPPORTED_FIELD_TYPES:
if issubclass(self.__class__, model):
self.localized_field_model = model
self._blank = kwargs.get("blank", False)
self._editable = kwargs.get("editable", True)
super().__init__(verbose_name, **kwargs)
@staticmethod
def localized_field_name(name, lang_code):
lang_code_safe = lang_code.replace("-", "_")
return f"{name}_{lang_code_safe}"
def get_localized_field(self, lang_code, lang_name):
_blank = (self._blank
if lang_code == settings.LANGUAGE_CODE
else True)
localized_field = self.localized_field_model(
f"{self.verbose_name} ({lang_name})",
name=self.name,
primary_key=self.primary_key,
max_length=self.max_length,
unique=self.unique,
blank=_blank,
null=False, # we ignore the null argument!
db_index=self.db_index,
default=self.default or "",
editable=self._editable,
serialize=self.serialize,
choices=self.choices,
help_text=self.help_text,
db_column=None,
db_tablespace=self.db_tablespace)
return localized_field
def contribute_to_class(self, cls, name,
private_only=False,
virtual_only=False):
def translated_value(self):
language = get_language()
val = self.__dict__.get(
MultilingualField.localized_field_name(
name, language))
if not val:
val = self.__dict__.get(
MultilingualField.localized_field_name(
name, settings.LANGUAGE_CODE))
return val
# generate language-specific fields dynamically
if not cls._meta.abstract:
if self.localized_field_model:
for lang_code, lang_name in settings.LANGUAGES:
localized_field = self.get_localized_field(
lang_code, lang_name)
localized_field.contribute_to_class(
cls, MultilingualField.localized_field_name(
name, lang_code))
setattr(cls, name, property(translated_value))
else:
super().contribute_to_class(
cls, name, private_only, virtual_only)
- 在同一个文件中,对字符和文本字段表单继承子类,如下:
class MultilingualCharField(models.CharField, MultilingualField):
pass
class MultilingualTextField(models.TextField, MultilingualField):
pass
- 在core应用中创建admin.py文件,并添加如下内容:
# myproject/apps/core/admin.py
from django.conf import settings
def get_multilingual_field_names(field_name):
lang_code_underscored = settings.LANGUAGE_CODE.replace("-", "_")
field_names = [f"{field_name}_{lang_code_underscored}"]
for lang_code, lang_name in settings.LANGUAGES:
if lang_code != settings.LANGUAGE_CODE:
lang_code_underscored = lang_code.replace("-", "_")
field_names.append(
f"{field_name}_{lang_code_underscored}"
)
return field_names
下面我们将研究如何在应用中使用多语言字段的示例,如下:
- 首先,在项目的设置中设置多语言。比如,我们的网站将支持欧盟的所有官方语言,英语作为其默认语言:
# myproject/settings/_base.py
LANGUAGE_CODE = "en"
# All official languages of European Union
LANGUAGES = [
("bg", "Bulgarian"), ("hr", "Croatian"),
("cs", "Czech"), ("da", "Danish"),
("nl", "Dutch"), ("en", "English"),
("et", "Estonian"), ("fi", "Finnish"),
("fr", "French"), ("de", "German"),
("el", "Greek"), ("hu", "Hungarian"),
("ga", "Irish"), ("it", "Italian"),
("lv", "Latvian"), ("lt", "Lithuanian"),
("mt", "Maltese"), ("pl", "Polish"),
("pt", "Portuguese"), ("ro", "Romanian"),
("sk", "Slovak"), ("sl", "Slovene"),
("es", "Spanish"), ("sv", "Swedish"),
]
- 然后,打开myproject.apps.ideas应用中的models.py文件,并针对Idea模型创建多语言字段,如下:
# myproject/apps/ideas/models.py
from django.db import models
from django.utils.translation import gettext_lazy as _
from myproject.apps.core.model_fields import (
MultilingualCharField,
MultilingualTextField,
)
class Idea(models.Model):
title = MultilingualCharField(
_("Title"),
max_length=200,
)
content = MultilingualTextField(
_("Content"),
)
class Meta:
verbose_name = _("Idea")
verbose_name_plural = _("Ideas")
def __str__(self):
return self.title
- 为ideas项目创建一个admin.py文件:
# myproject/apps/ideas/admin.py
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from myproject.apps.core.admin import get_multilingual_field_names
from .models import Idea
@admin.register(Idea)
class IdeaAdmin(admin.ModelAdmin):
fieldsets = [
(_("Title and Content"), {
"fields": get_multilingual_field_names("title") +
get_multilingual_field_names("content")
}),
]
实现原理...
Idea的示例会生成类似下面这样的模型:
class Idea(models.Model):
title_bg = models.CharField(
_("Title (Bulgarian)"),
max_length=200,
)
title_hr = models.CharField(
_("Title (Croatian)"),
max_length=200,
)
# titles for other languages...
title_sv = models.CharField(
_("Title (Swedish)"),
max_length=200,
)
content_bg = MultilingualTextField(
_("Content (Bulgarian)"),
)
content_hr = MultilingualTextField(
_("Content (Croatian)"),
)
# content for other languages...
content_sv = MultilingualTextField(
_("Content (Swedish)"),
)
class Meta:
verbose_name = _("Idea")
verbose_name_plural = _("Ideas")
def __str__(self):
return self.title
如果语言代码中在横杠,如de-ch表示瑞士德语,这种语言的字段会被替换为下划线,如title_de_ch和content_de_ch。
除所生成的具体语言字段外,还会有两个属性:标题和内容,将在当前使用语言中返回相应字段。如果没有本地化字段内容就会使用默认语言。
MultilingualCharField和MultilingualTextField字段会根据LANGUAGES设置动态兼容模型字段。它们会重写在Django框架创建模型类时使用的contribute_to_class()方法。多语言字段动态为项目的每种语言添加字符或文本字段。需要创建一个数据库迁移来在数据库中增加相应的字段。同时,默认创建这些属性来返回当前使用语言或主语言的翻译值。
在后台中,get_multilingual_field_names()会返回一个具体语言字段名列表,以一种默认语言开头,然后接LANGUAGES设置中的其它语言。
以下是一些在模型及视图中使用多语言字段的示例:
如果模板中有如下代码,它会显示当前使用语言中的文本,比如立陶宛语,在翻译不存在时就依然使用英语:
{{ idea.title }}
{{ idea.content|urlize|linebreaks }}
如果想让QuerySet按照翻译后的标题进行排序,可以进行如下的定义:
>>> lang_code = input("Enter language code: ")
>>> lang_code_underscored = lang_code.replace("-", "_")
>>> qs = Idea.objects.order_by(f"title_{lang_code_underscored}")
相关内容
- 操作模型翻译数据表一节
- 使用迁移一节
- 第6章 模型管理
操作模型翻译数据表
第二种在数据库中处理多语言内容的方法为对每个多语言模型使用模型翻译表。
这种方法的特点如下:
- 可以使用现有后台来在行内编辑翻译
- 在修改设置中的语言量后,无需做出迁移或其它操作
- 可以毫不费力地显示当前语言的翻译,但对于在同一页面中显示多种指定语言的翻译时会更为复杂
- 需要了解并使用本小节中创建模型翻译的具体套路
- 本方法进行数据库查询时会比较复杂,但通过学习会发现仍可以实现
准备工作
同样,我们会使用myprojects.apps.core应用。
如何实现...
执行如下步骤来准备多语言模型:
- 在core应用中,创建model_fields.py并添加如下内容:
# myproject/apps/core/model_fields.py
from django.conf import settings
from django.utils.translation import get_language
from django.utils import translation
class TranslatedField(object):
def __init__(self, field_name):
self.field_name = field_name
def __get__(self, instance, owner):
lang_code = translation.get_language()
if lang_code == settings.LANGUAGE_CODE:
# The fields of the default language are in the main model
return getattr(instance, self.field_name)
else:
# The fields of the other languages are in the translation
# model, but falls back to the main model
translations = instance.translations.filter(
language=lang_code,
).first() or instance
return getattr(translations, self.field_name)
- 在core 应用中增加admin.py文件并添加如下内容:
# myproject/apps/core/admin.py
from django import forms
from django.conf import settings
from django.utils.translation import gettext_lazy as _
class LanguageChoicesForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
LANGUAGES_EXCEPT_THE_DEFAULT = [
(lang_code, lang_name)
for lang_code, lang_name in settings.LANGUAGES
if lang_code != settings.LANGUAGE_CODE
]
super().__init__(*args, **kwargs)
self.fields["language"] = forms.ChoiceField(
label=_("Language"),
choices=LANGUAGES_EXCEPT_THE_DEFAULT,
required=True,
)
下面我们来实现多语言模型:
- 首先在项目的设置中设置多语言。比如我们的网站支持欧盟的所有官方语言,英语是默认语言:
# myproject/settings/_base.py
LANGUAGE_CODE = "en"
# All official languages of European Union
LANGUAGES = [
("bg", "Bulgarian"), ("hr", "Croatian"),
("cs", "Czech"), ("da", "Danish"),
("nl", "Dutch"), ("en", "English"),
("et", "Estonian"), ("fi", "Finnish"),
("fr", "French"), ("de", "German"),
("el", "Greek"), ("hu", "Hungarian"),
("ga", "Irish"), ("it", "Italian"),
("lv", "Latvian"), ("lt", "Lithuanian"),
("mt", "Maltese"), ("pl", "Polish"),
("pt", "Portuguese"), ("ro", "Romanian"),
("sk", "Slovak"), ("sl", "Slovene"),
("es", "Spanish"), ("sv", "Swedish"),
]
- 然后我们来创建Idea和IdeaTranslations模型:
# myproject/apps/ideas/models.py
from django.db import models
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from myproject.apps.core.model_fields import TranslatedField
class Idea(models.Model):
title = models.CharField(
_("Title"),
max_length=200,
)
content = models.TextField(
_("Content"),
)
translated_title = TranslatedField("title")
translated_content = TranslatedField("content")
class Meta:
verbose_name = _("Idea")
verbose_name_plural = _("Ideas")
def __str__(self):
return self.title
class IdeaTranslations(models.Model):
idea = models.ForeignKey(
Idea,
verbose_name=_("Idea"),
on_delete=models.CASCADE,
related_name="translations",
)
language = models.CharField(_("Language"), max_length=7)
title = models.CharField(
_("Title"),
max_length=200,
)
content = models.TextField(
_("Content"),
)
class Meta:
verbose_name = _("Idea Translations")
verbose_name_plural = _("Idea Translations")
ordering = ["language"]
unique_together = [["idea", "language"]]
def __str__(self):
return self.title
- 最后,对ideas应用创建admin.py并添加如下内容:
# myproject/apps/ideas/admin.py
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from myproject.apps.core.admin import LanguageChoicesForm
from .models import Idea, IdeaTranslations
class IdeaTranslationsForm(LanguageChoicesForm):
class Meta:
model = IdeaTranslations
fields = "__all__"
class IdeaTranslationsInline(admin.StackedInline):
form = IdeaTranslationsForm
model = IdeaTranslations
extra = 0
@admin.register(Idea)
class IdeaAdmin(admin.ModelAdmin):
inlines = [IdeaTranslationsInline]
fieldsets = [
(_("Title and Content"), {
"fields": ["title", "content"]
}),
]
实现原理...
我们将默认语言的具体语言字段放到Idea模型本身当中。每种语言的翻译位于IdeaTranslations模型中,在后台中以内联翻译的形式列出。IdeaTranslations在模型层没有语言选项是有原因的,我们不希望在每次添加新语言或删除语言时都进行迁移。取而代之的是把语言选项放到后台表单中,同时确保跳过默认语言或在选项的列表不可用。语言选项通过LanguageChoicesForm类进行的限制。
要获取当前语言的指定字段,使用定义为TranslatedField的这些字段。在模板中像下面这样:
{{ idea.translated_title }}
{{ idea.translated_content|urlize|linebreaks }}
要通过指定语言的翻译后标题进行排序,需要像下面这样使用annotate()方法:
>>> from django.conf import settings
>>> from django.db import models
>>> lang_code = input("Enter language code: ")
>>> if lang_code == settings.LANGUAGE_CODE:
... qs = Idea.objects.annotate(
... title_translation=models.F("title"),
... content_translation=models.F("content"),
... )
... else:
... qs = Idea.objects.filter(
... translations__language=lang_code,
... ).annotate(
... title_translation=models.F("translations__title"),
... content_translation=models.F("translations__content"),
... )
>>> qs = qs.order_by("title_translation")
>>> for idea in qs:
... print(idea.title_translation)
本例中,我们在Django shell中弹出要求输入语言代码。如果为默认语言,我们将标题和内容存储为Idea模型中的title_translation和content_translation。如果选中的是其它语言,我们从IdeaTranslations模型中按照所选中语言将标题和内容读取为title_translation和content_translation。
之后,我们就可以通过title_translation或content_translation对查询集合(QuerySet)进行过滤或排序。
相关内容
- 处理多语言字段一节
- 第6章 模型管理
规避循环依赖
在开发Django模型时,避免循环依赖尤其是在models.py中规避这一问题尤为重要。循环依赖是指在Python模块对彼此进行导入操作。应绝对避免在不同的models.py文件之间进行交叉导入,因为这样会导致严重的稳定性问题。如果有相互依赖,请使用本小节中所介绍的方法。
准备工作
我们使用categories和ideas应用来讲解如何处理交叉依赖的问题。
如何实现...
按照如下方法处理使用到其它应用中模型的模型:
- 对于外键和与其它应用中模型的多对多关联,使用
. 的声明方式来代替模型的导入。在Django中这对ForeignKey、OneToOneField和ManyToManyField都有效,如:
# myproject/apps/ideas/models.py
from django.db import models
from django.conf import settings
from django.utils.translation import gettext_lazy as _
class Idea(models.Model):
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_("Author"),
on_delete=models.SET_NULL,
blank=True,
null=True,
)
category = models.ForeignKey(
"categories.Category",
verbose_name=_("Category"),
blank=True,
null=True,
on_delete=models.SET_NULL,
)
# other fields, attributes, properties and methods...
这里settings.AUTH_USER_MODEL是一个设置,带有auth.User这样的值
- 如果你需要在谢谢学姐中访问其它应用的模型的话,在方法内部而不要在模型级别导入模型,如下:
# myproject/apps/categories/models.py
from django.db import models
from django.utils.translation import gettext_lazy as _
class Category(models.Model):
# fields, attributes, properties, and methods...
def get_ideas_without_this_category(self):
from myproject.apps.ideas.models import Idea
return Idea.objects.exclude(category=self)
- 如何使用模型继承,如针对模型mixin,将基类放到单独的应用中并在INSTALLED_APPS中将它们放到其它应用之前,如下:
# myproject/settings/_base.py
INSTALLED_APPS = [
# contributed
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# third-party
# ...
# local
"myproject.apps.core",
"myproject.apps.categories",
"myproject.apps.ideas",
]
这里ideas应用将像下面这样使用core应用中的模型mixin:
# myproject/apps/ideas/models.py
from django.db import models
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from myproject.apps.core.models import (
CreationModificationDateBase,
MetaTagsBase,
UrlBase,
)
class Idea(CreationModificationDateBase, MetaTagsBase, UrlBase):
# fields, attributes, properties, and methods...
相关内容
- 第1章 Django 3.0入门中的为开发、测试、预发布和生产环境配置设置一节
- 第1章 Django 3.0入门中的在Python文件中重视导入顺序一节
- 使用模型mixin一节
- 修改外键为many-to-many字段一节
添加数据库约束
为更好地保证数据库一致性,经常会定义数据库约束,表明一些字段与其它数据表相绑定,让这些字段保持唯一或不为null。更高级的数据库约束,如让字段根据条件保持唯一或为一些字段值设置指定条件,Django提供了一些特殊类:UniqueConstraint和CheckConstraint。本小节中,我们将学习如何使用它们的实例。
准备工作
我们使用ideas应用并且Idea模型到少要包含title和author字段。
如何实现...
设置Idea模型Meta类中的数据库约束如下:
# myproject/apps/ideas/models.py
from django.db import models
from django.utils.translation import gettext_lazy as _
class Idea(models.Model):
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_("Author"),
on_delete=models.SET_NULL,
blank=True,
null=True,
related_name="authored_ideas",
)
title = models.CharField(
_("Title"),
max_length=200,
)
class Meta:
verbose_name = _("Idea")
verbose_name_plural = _("Ideas")
constraints = [
models.UniqueConstraint(
fields=["title"],
condition=~models.Q(author=None),
name="unique_titles_for_each_author",
),
models.CheckConstraint(
check=models.Q(
title__iregex=r"^\S.*\S$"
# starts with non-whitespace,
# ends with non-whitespace,
# anything in the middle
),
name="title_has_no_leading_and_trailing_whitespaces",
)
]
实现原理...
我们在数据库中定义两个约束。
第一个是UniqueConstraint,限定每个作者的标题要唯一。如果未设定作者,则标题允许重复。要检查是否设置了作者我们使用否定查询:models.Q(author=None)。注意在Django,查找中的 运算符等价于QuerySet中的exclude()方法,因此以下的查询集是等价的:
ideas_with_authors = Idea.objects.exclude(author=None)
ideas_with_authors2 = Idea.objects.filter(~models.Q(author=None))
第二个约束CheckConstraint,查看标题是否以空白开始及结束。对于这类,我们使用正则表达式查找。
扩展知识...
数据库约束不影响表单验证。如将内容保存到数据库时,如果数据不满足条件则抛出django.db.utils.IntegrityError。
如果希望在表单中进行数据验证,需要自己单独进行实现,例如,在模型的clean()方法添加逻辑。Idea会变成类似下面这样:
# myproject/apps/ideas/models.py
from django.db import models
from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
class Idea(models.Model):
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_("Author"),
on_delete=models.SET_NULL,
blank=True,
null=True,
related_name="authored_ideas2",
)
title = models.CharField(
_("Title"),
max_length=200,
)
# other fields and attributes...
class Meta:
verbose_name = _("Idea")
verbose_name_plural = _("Ideas")
constraints = [
models.UniqueConstraint(
fields=["title"],
condition=~models.Q(author=None),
name="unique_titles_for_each_author2",
),
models.CheckConstraint(
check=models.Q(
title__iregex=r"^\S.*\S$"
# starts with non-whitespace,
# ends with non-whitespace,
# anything in the middle
),
name="title_has_no_leading_and_trailing_whitespaces2",
)
]
def clean(self):
import re
if self.author and Idea.objects.exclude(pk=self.pk).filter(
author=self.author,
title=self.title,
).exists():
raise ValidationError(
_("Each idea of the same user should have a unique title.")
)
if not re.match(r"^\S.*\S$", self.title):
raise ValidationError(
_("The title cannot start or end with a whitespace.")
)
# other properties and methods...
相关内容
第3章 表单和视图
第10章 锦上添花
中的使用数据库查询表达式一节
使用迁移
在敏捷软件开发中,要求项目不断演进并在开发过程中保持更新。因为开发是迭代进行的,需要在过程中进行数据库模式的更改。借助于Django的迁移命令,我们无需手动更改数据表及字段,这大多数都是通过命令行自动完成的。
准备工作
在命令行工具中启用虚拟环境,并将当前目录切换为项目目录。
如何实现...
要实现数据库迁移,参见如下步骤:
- 在新的categories或ideas应用创建模型时,需要先生成用于创建应用对应数据表的初始迁移代码。可通过如下命令实现:
(env)$ python manage.py makemigrations ideas
- 首次希望对项目创建所有数据表时,运行如下命令:
(env)$ python manage.py migrate
在希望对所有应用执行新的迁移时运行该命令。
- 如果希望对具体应用执行迁移,则运行如下命令:
(env)$ python manage.py migrate ideas
- 如若想对数据库模式进行修改,需要对该模式创建迁移。例如,如果我们对idea模型添加一个副标题字段,则可以通过如下命令来添加至迁移文件:
(env)$ python manage.py makemigrations --name=subtitle_added ideas
但是,可以跳过 --name=subtitle_added字段的指定,因为大多数情况下Django会生成可说明含义的默认名称。
- 有时,可以批量在已有模式中添加或修改数据,这可以通过数据迁移来实现,而无需进行模式迁移。要创建在数据表中修改数据的迁移,可以使用如下命令:
(env)$ python manage.py makemigrations --name=populate_subtitle \
> --empty ideas
--empty参数告诉Django要创建一个框架数据迁移,在应用前需要修改以执行必要的数据操作。对于数据迁移,推荐设置名称。
- 可运行如下命令来列举所有可用的已应用和未应用迁移:
(env)$ python manage.py showmigrations
已应用迁移列举时的前缀为[X] 。未应用的迁移列举时前缀为[ ] 。
- 可以通过运行相同的命令,但传递应用名来列出指定应用的可用迁移,如下:
(env)$ python manage.py showmigrations ideas
实现原理...
Django的迁移是针对数据库迁移机制的指令文件。这个指令文件告诉我们要创建或删除哪些数据表、添加或删除哪些字段段,以及插入、更新或删除哪些数据。同时也定义了哪些迁移依赖于另外的哪些迁移。
Django中有两种类型的迁移。一种是模式(schema)迁移,另一种是数据迁移。模式迁移应当在新增模型、添加或删除字段时创建。数据迁移应在希望在数据库中填充一些值或批量从数据库中删除值时使用。数据迁移在命令行工具使用命令创建,然后在迁移文件中添加代码。
各应用的迁移保存在各自的migrations目录中。第一次迁移名称通常为0001_initial.py,我们的示例应用的其它迁移名称为0002_subtitle_added.py及0003_populate_subtitle.py。每次迁移会获取一个自动递增的数字前缀。每执行一次迁移,都会在django_migrations数据表中保存一条记录。
可以通过指定迁移编号来回迁移,如以下命令所示:
(env)$ python manage.py migrate ideas 0002
要取消包含初始迁移在内的所有应用迁移,运行如下命令:
(env)$ python manage.py migrate ideas zero
取消迁移要求每次迁移存在前向和后向操作。理想情况下,后台操作将精确地取消由前向操作所做的修改。但在某些情况下修改不可恢复,比如在前台操作从模式中删除了某列的情况下,因为它销毁了数据。在这种情况下,后台操作可以恢复模式,但数据则无法恢复,另一种可能是完全没有后向操作存在。
在测试了前向和后台迁移流程并确保在其它开发环境和对公网站环境中良好运行之前,请不要提交迁移到版本控制中。
扩展知识...
编写数据库迁移的更多内容参见官方 How To指南:https://docs.djangoproject.com/en/3.0/howto/writing-migrations/。
相关内容
- 第1章 Django 3.0入门中的使用虚拟环境一节
- 第1章 Django 3.0入门中的使用Docker容器处理Django, Gunicorn, Nginx和PostgreSQL一节
- 第1章 Django 3.0入门中的通过pip处理项目依赖一节
- 第1章 Django 3.0入门中的在项目中包含外部依赖一节
- 修改外键为many-to-many字段一节
修改外键为many-to-many字段
本小节的实例为如何将多对一关联修改为多对多关联,同时保留已有数据。我们针对此情况将同时使用模式和数据迁移。
准备工作
假定有Idea模型,包含一个指向Category模型的外键。
- 我们在categories应用中定义Category模型如下:
# myproject/apps/categories/models.py
from django.db import models
from django.utils.translation import gettext_lazy as _
from myproject.apps.core.model_fields import MultilingualCharField
class Category(models.Model):
title = MultilingualCharField(
_("Title"),
max_length=200,
)
class Meta:
verbose_name = _("Category")
verbose_name_plural = _("Categories")
def __str__(self):
return self.title
- 在ideas应用中定义Idea模型如下:
# myproject/apps/ideas/models.py
from django.db import models
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from myproject.apps.core.model_fields
import (
MultilingualCharField,
MultilingualTextField,
)
class Idea(models.Model):
title = MultilingualCharField(
_("Title"),
max_length=200,
)
content = MultilingualTextField(
_("Content"),
)
category = models.ForeignKey(
"categories.Category",
verbose_name=_("Category"),
blank=True,
null=True,
on_delete=models.SET_NULL,
related_name="category_ideas",
)
class Meta:
verbose_name = _("Idea")
verbose_name_plural = _("Ideas")
def __str__(self):
return self.title
- 使用如下命令创建并执行初始化迁移:
(env)$ python manage.py makemigrations categories
(env)$ python manage.py makemigrations ideas
(env)$ python manage.py migrate
如何实现...
以下步骤展示如何从外键切换为多对多关联,同时保留已有数据:
- 新增多对多字段categories如下:
# myproject/apps/ideas/models.py
from django.db import models
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from myproject.apps.core.model_fields import (
MultilingualCharField,
MultilingualTextField,
)
class Idea(models.Model):
title = MultilingualCharField(
_("Title"),
max_length=200,
)
content = MultilingualTextField(
_("Content"),
)
category = models.ForeignKey(
"categories.Category",
verbose_name=_("Category"),
blank=True,
null=True,
on_delete=models.SET_NULL,
related_name="category_ideas",
)
categories = models.ManyToManyField(
"categories.Category",
verbose_name=_("Categories"),
blank=True,
related_name="ideas",
)
class Meta:
verbose_name = _("Idea")
verbose_name_plural = _("Ideas")
def __str__(self):
return self.title
- 创建并运行模式迁移来对数据库添加新关联,如以下代码所示
(env)$ python manage.py makemigrations ideas
(env)$ python manage.py migrate ideas
- 创建一个数据迁移来将categories从外键拷贝到多对多字段,如下:
(env)$ python manage.py makemigrations --empty \
> --name=copy_categories ideas
- 打开新创建的迁移文件(0003_copy_categories.py),并定义前台迁移指令,如以下代码所示:
# myproject/apps/ideas/migrations/0003_copy_categories.py
from django.db import migrations
def copy_categories(apps, schema_editor):
Idea = apps.get_model("ideas", "Idea")
for idea in Idea.objects.all():
if idea.category:
idea.categories.add(idea.category)
class Migration(migrations.Migration):
dependencies = [
('ideas', '0002_idea_categories'),
]
operations = [
migrations.RunPython(copy_categories),
]
- 运行新的数据迁移如下:
(env)$ python manage.py migrate ideas
- 删除models.py文件中的外键字段category,仅保留新的多对多字段categories,如下:
# myproject/apps/ideas/models.py
from django.db import models
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from myproject.apps.core.model_fields import (
MultilingualCharField,
MultilingualTextField,
)
class Idea(models.Model):
title = MultilingualCharField(
_("Title"),
max_length=200,
)
content = MultilingualTextField(
_("Content"),
)
categories = models.ManyToManyField(
"categories.Category",
verbose_name=_("Categories"),
blank=True,
related_name="ideas",
)
class Meta:
verbose_name = _("Idea")
verbose_name_plural = _("Ideas")
def __str__(self):
return self.title
- 创建并运行模式迁移,来删除数据表中的Categories字段,如下:
(env)$ python manage.py makemigrations ideas
(env)$ python manage.py migrate ideas
实现原理...
首先,我们在Idea模型中添加了一个多对多字段,并生成了一个数据库中相应地进行更新的迁移文件。然后,我们创建了一个会从外键category将已有关联拷贝到新的多对多字段categories的数据迁移。最后,我们从模型中删除了外键,并再次更新了数据库。
扩展知识...
我们的数据迁移当前仅包含前台操作、将外键category拷贝为新的categories关联的准备个关联项。虽然这里没有详述,在真实场景中最好还应包含反向操作。这可通过将关联项拷贝回到category外键来实现。可惜地是,什么问题带有多个分类的Idea对象会丢失额外数据。
相关内容
- 使用迁移一节
- 处理多语言字段一节
- 操作模型翻译数据表一节
- 规避循环依赖一节
本文首发地址: Alan Hou 的人个博客