目录
序言
最近在做公司的一个项目,我是产品经理,本不用自己码代码,但是实在是手痒。。。。所以就做做。主要遇到一个问题,自定义Filed的问题。总有一些奇葩需求没法通过默认的Filed满足,这里有一个自定义的方式进行生成Filed和django动态生成数据表并注册。
创建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
#创建实例方法
def create_book(self, title):
book = self.create(title=title)
# do something with the book
return book
book = Book.create("Pride and Prejudice")
baseModel参考:https://docs.djangoproject.com/zh-hans/2.1/_modules/django/db/models/base/#Model.from_db
创建项目
class Book(models.Model):
from_db()
自定义模型从数据库加载数据的方法
@classmethod
Model.from_db(db,field_names,values)
from_db()
从数据库加载时,该方法可用于自定义模型实例创建。
- db:参数包含加载模型的数据库的数据库别名,
- field_names:包含所有已加载字段的名称,包含每个field_names字段的加载值values 。field_names与values顺序相同。
- values:保证按__init__()预期顺序排列 。
如果延迟任何字段,它们将不会出现 field_names。在这种情况下,django.db.models.DEFERRED 为每个缺少的字段分配值。
除了创建新模型之外,该from_db()方法还必须在新实例的属性中设置 adding和db标志_state。
下面是一个示例,说明如何记录从数据库加载的字段的初始值:
from django.db.models import DEFERRED
#cls是models.Model
@classmethod
def from_db(cls, db, field_names, values):
#from_db()的默认实现(可能会更改,可以用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
]
#除了创建新模型之外,该from_db()方法还必须在新实例的属性中设置 adding和db标志_state。
instance = cls(*values)
instance._state.adding = False
instance._state.db = db
# 自定义以在实例上存储原始字段值
instance._loaded_values = dict(zip(field_names, values))
return instance
def save(self, *args, **kwargs):
# 检查当前值与._loaded_values的区别。例如,防止更改模型的creator_id。(本例不支持'creator_id'被延迟的情况)。
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)
refresh_from_db()
刷新数据库中的对象
如果从模型实例中删除字段,则再次访问该字段会重新加载数据库中的值:
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)
#在这一点上obj.val仍然是1,但是数据库中的值被更新为2。对象的更新值需要从数据库中重新加载。
obj.refresh_from_db()
self.assertEqual(obj.val, 2)
请注意,访问延迟字段时,通过此方法加载延迟字段的值。因此,可以自定义延迟加载的方式。下面的示例显示了在重新加载延迟字段时如何重新加载所有实例的字段:
class ExampleModel(models.Model):
def refresh_from_db(self, using=None, fields=None, **kwargs):
# 字段包含要加载的延迟字段的名称。
if fields is not None:
fields = set(fields)
deferred_fields = self.get_deferred_fields()
# 如果要加载任何延迟字段
if fields.intersection(deferred_fields):
# 然后把它们都装上
fields = fields.union(deferred_fields)
super().refresh_from_db(using, fields, **kwargs)
get_deferred_fields()
Model.get_deferred_fields()
一个辅助方法,它返回一个集合,其中包含当前为此模型延迟的所有字段的属性名称。
clean及相关
验证对象
验证模型涉及三个步骤:
- 验证模型字段 - Model.clean_fields()
- 整体验证模型 - Model.clean()
- 验证字段唯一性 - Model.validate_unique()
调用模型的full_clean()方法时,将执行所有这三个步骤 。
保存
save()
编写自定义模型字段
该模型参考文档介绍了如何使用Django的标准字段类- CharField, DateField等多种用途,这些类是所有你需要的。但有时候,Django版本无法满足您的精确要求,对于更加模糊的列类型,例如地理多边形或甚至用户创建的类型(如 PostgreSQL自定义类型),您可以定义自己的Django Field子类。或者,您可能有一个复杂的Python对象,可以某种方式序列化以适应标准数据库列类型。这是另一种情况,其中Field子类将帮助您将对象与模型一起使用。
理论
存储数据库
考虑模型字段的最简单方法是它提供了一种方法来获取普通的Python对象 - 字符串,布尔值datetime,或类似的更复杂的东西Hand- 并将其转换为处理和处理时有用的格式。
必须以某种方式转换模型中的字段以适合现有的数据库列类型。不同的数据库提供不同的有效列类型集,但规则仍然相同:这些是您必须使用的唯一类型。您要存储在数据库中的任何内容都必须适合其中一种类型。
通常,您要么编写Django字段以匹配特定的数据库列类型,要么将数据转换为字符串,这是一种相当简单的方法。
一个字段类做了什么?
所有Django的字段是django.db.models.Field
的子类。Django记录的关于字段的大多数信息对于所有字段都是通用的 - 名称,帮助文本,唯一性等等。存储所有信息由Field处理。
Django字段类不是存储在模型属性中的字段。
模型属性包含普通的Python对象。您在模型中定义的字段类实际上在Meta创建模型类时存储在类中。当您只是创建和修改属性时,不需要字段类。相反,它们提供了在属性值和存储在数据库中或发送到序列化器的内容之间进行转换的机制。
当您需要自定义字段时,通常最终会创建两个类:
第一个类是用户将操作的Python对象。他们将它分配给模型属性,他们将从中读取它以用于显示目的。
第二个类是Field子类。这个类知道如何在永久存储形式和Python表单之间来回转换第一个类。
编写一个字段子类
在规划Field子类时,首先要考虑Field新字段与哪个现有类最相似。如果没有相似的,你应该继承Field 类,从中产生一切。
在我们的例子中,我们将调用我们的字段HandField。(调用Field子类是个好主意Field,因此很容易将其识别为Field子类。)它的行为与现有字段不同,因此我们将直接从子类中进行子类化 Field:
from django.db import models
class HandField(models.Field):
description = "A hand of cards (bridge style)"
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 104
super().__init__(*args, **kwargs)
我们HandField接受大多数标准字段选项(请参阅下面的列表),但我们确保它具有固定长度,因为它只需要保存52个卡值加上它们的套装; 共104个字符。
Field.init() 方法接收以下参数:
- verbose_name
- name
- primary_key
- max_length
- unique:如果True,该字段在整个表格中必须是唯一的。
- blank 如果True,该字段允许为空。默认是False。null纯粹与数据库相关,而blank与验证相关。如果字段有blank=True,则表单验证将允许输入空值。如果字段有blank=False,则需要该字段。
- null
- db_index:如果True,将为此字段创建数据库索引。
- rel:用于相关字段(如ForeignKey)。仅供高级使用。
- default
- editable:如果False,该字段将不会显示在管理员或任何其他字段中 ModelForm。在模型验证期间也会跳过它们。默认是True。
- serialize:如果False,当模型传递给Django的序列化程序时,该字段将不会被序列化。默认为 True。
- unique_for_date
- unique_for_month
- unique_for_year
- choices 每个元组中的第一个元素是要在模型上设置的实际值,第二个元素是人类可读的名称。例如:((a,b),)
- help_text:使用表单小部件显示的额外“帮助”文本。即使您的字段未在表单上使用,它也对文档很有用。请注意,此值不会在自动生成的表单中进行HTML转义。help_text如果您愿意,这可以让您包含HTML 。例如:
help_text="Please use the following format: <em>YYYY-MM-DD</em>."
- db_column: 用于此字段的数据库列的名称。如果没有给出,Django将使用该字段的名称。
- db_tablespace:仅用于索引创建,如果后端支持表空间。您通常可以忽略此选项。
- auto_created:True如果字段是自动创建的,则为OneToOneField 模型继承所使用的字段。仅供高级使用。
上面列表中没有解释的所有选项与普通Django字段的含义相同。有关示例和详细信息,请参阅现场文档。
场解构
编写__init__()方法的对应方法是编写 deconstruct()方法。这个方法告诉Django如何获取新字段的实例并将其减少为序列化形式 - 特别是传递__init__()给重新创建它的参数。
如果您没有在继承的字段之上添加任何额外选项,则无需编写新deconstruct()方法。但是,如果您正在更改传入的参数__init__()则需要补充传递的值。
deconstruct()很简单; 它返回一个由四个项组成的元组:
- 字段的属性名称,
- 字段类的完整导入路径,
- 位置参数(作为列表)
- 关键字参数(作为dict)。
请注意,这与返回三个元组的自定义类的deconstruct()方法不同。
作为自定义字段作者,您无需关心前两个值; 基Field类具有计算字段的属性名称和导入路径的所有代码。但是,您必须关注位置和关键字参数,因为这些可能是您正在更改的内容。
例如,在我们的HandField课堂上,我们总是强行对 init()设置max_length。在deconstruct()对基方法Field 类将看到这一点,并试图在关键字参数返回它; 因此,为了便于阅读,我们可以从关键字参数中删除它:
from django.db import models
class HandField(models.Field):
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 104
super().__init__(*args, **kwargs)
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
del kwargs["max_length"]
return name, path, args, kwargs
如果添加新的关键字参数,则需要编写代码以将其值放入kwargs:
from django.db import models
class CommaSepField(models.Field):
"Implements comma-separated storage of lists"
def __init__(self, separator=",", *args, **kwargs):
self.separator = separator
super().__init__(*args, **kwargs)
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
# Only include kwarg if it's not the default
if self.separator != ",":
kwargs['separator'] = self.separator
return name, path, args, kwargs
更改自定义字段的基类¶
您无法更改自定义字段的基类,因为Django不会检测更改并为其进行迁移。例如,如果您从以下开始:
class CustomCharField(models.CharField):
...
然后决定你要使用TextField,你不能像这样改变子类:
class CustomCharField(models.TextField):
...
相反,您必须创建一个新的自定义字段类并更新模型以引用它:
class CustomCharField(models.CharField):
...
class CustomTextField(models.TextField):
...
如删除字段中所述,CustomCharField只要您具有引用它的迁移,就必须保留原始类。
记录您的自定义字段
与往常一样,您应记录您的字段类型,以便用户知道它是什么。除了为开发人员提供文档字符串之外,您还可以允许管理员应用程序的用户通过django.contrib.admindocs应用程序查看字段类型的简短描述。为此,只需在description自定义字段的类属性中提供描述性文本即可。在上面的例子中,admindocs 应用程序显示的描述HandField将是“A hand of cards(bridge style)”。
在django.contrib.admindocs显示中,插入字段描述field.dict,允许描述包含字段的参数。例如,描述为 CharField:
description = _("String (up to %(max_length)s)")
可能需要覆盖的方法
有用的方法
一旦创建了Field子类,您可以考虑覆盖一些标准方法,具体取决于您的字段的行为。下面的方法列表大致按重要性递减顺序,因此从顶部开始。
自定义数据库类型
假设您已经创建了一个名为的PostgreSQL自定义类型mytype。您可以子类化Field并实现该db_type()方法,如下所示:
from django.db import models
class MytypeField(models.Field):
def db_type(self, connection):
return 'mytype'
有了MytypeField,你就可以在任何模型中使用它,就像任何其他 Field类型一样:
class Person(models.Model):
name = models.CharField(max_length=80)
something_else = MytypeField()
如果您的目标是构建与数据库无关的应用程序,则应考虑数据库列类型的差异。例如,调用PostgreSQL中的日期/时间列类型timestamp,同时调用MySQL中的相同列 datetime。在方法中处理此问题的最简单db_type() 方法是检查connection.settings_dict[‘ENGINE’]属性。
例子:
class MyDateField(models.Field):
def db_type(self, connection):
if connection.settings_dict['ENGINE'] == 'django.db.backends.mysql':
return 'datetime'
else:
return 'timestamp'
该db_type()和rel_db_type()方法由Django的调用时框架构建的应用程序的语句-也就是说,当你第一次创建表。该方法是构建时也称,包括模型字段条款-那就是,当你检索使用类似的QuerySet方法的数据, 以及和有示范田作为参数。它们在任何其他时间都不会被调用,因此它可以执行稍微复杂的代码,例如上面示例中的检查。CREATE TABLEWHEREget()filter()exclude()connection.settings_dict
某些数据库列类型接受参数,例如CHAR(25),参数25表示最大列长度。在这些情况下,如果在模型中指定参数而不是在db_type()方法中进行硬编码,则会更灵活。例如,拥有一个没有多大意义CharMaxlength25Field,如下所示:
# This is a silly example of hard-coded parameters.
class CharMaxlength25Field(models.Field):
def db_type(self, connection):
return 'char(25)'
# In the model:
class MyModel(models.Model):
# ...
my_field = CharMaxlength25Field()
执行此操作的更好方法是在运行时使参数可指定 - 即,在实例化类时。要做到这一点,只需实现 Field.__init__(),如下:
# This is a much more flexible example.
class BetterCharField(models.Field):
def __init__(self, max_length, *args, **kwargs):
self.max_length = max_length
super().__init__(*args, **kwargs)
def db_type(self, connection):
return 'char(%s)' % self.max_length
# In the model:
class MyModel(models.Model):
# ...
my_field = BetterCharField(25)
最后,如果您的列需要真正复杂的SQL设置,请None从中 返回db_type()。这将导致Django的SQL创建代码跳过此字段。当然,您负责以其他方式在右表中创建列,但这为您提供了一种方法来告诉Django。
该rel_db_type()方法由诸如ForeignKey 和之类的字段调用,并OneToOneField指向另一个字段以确定其数据库列数据类型。例如,如果您有UnsignedAutoField,则还需要指向该字段的外键使用相同的数据类型:
# MySQL unsigned integer (range 0 to 4294967295).
class UnsignedAutoField(models.AutoField):
def db_type(self, connection):
return 'integer UNSIGNED AUTO_INCREMENT'
def rel_db_type(self, connection):
return 'integer UNSIGNED'
将值转换为Python对象
如果您的自定义Field类处理比字符串,日期,整数或浮点数更复杂的数据结构,那么您可能需要覆盖 from_db_value()和to_python()。
如果存在于字段子类中,from_db_value()则在从数据库加载数据时(包括聚合和values()调用)将在所有情况下调用。
to_python()通过反序列化和clean()从表单中使用的方法调用 。
作为一般规则,to_python()应优雅地处理以下任何参数:
- 正确类型的实例(例如,Hand在我们正在进行的示例中)。
- 一个字符串
- None(如果该字段允许null=True)
在我们的HandField类中,我们将数据存储为数据库中的VARCHAR字段,因此我们需要能够处理字符串并None在数据库中 from_db_value()。在to_python(),我们还需要处理Hand 实例:
import re
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _
def parse_hand(hand_string):
"""拿起一串牌,劈开成一手牌。"""
p1 = re.compile('.{26}')
p2 = re.compile('..')
args = [p2.findall(x) for x in p1.findall(hand_string)]
if len(args) != 4:
raise ValidationError(_("Invalid input for a Hand instance"))
return Hand(*args)
class HandField(models.Field):
# ...
def from_db_value(self, value, expression, connection):
if value is None:
return value
return parse_hand(value)
def to_python(self, value):
if isinstance(value, Hand):
return value
if value is None:
return value
return parse_hand(value)
请注意,我们总是Hand从这些方法返回一个实例。这是我们想要存储在模型属性中的Python对象类型。
因为to_python(),如果在值转换期间出现任何问题,则应引发ValidationError异常。
将Python对象转换为查询值:get_prep_value()
由于使用数据库需要以两种方式进行转换,因此如果覆盖,则 to_python()还必须重写get_prep_value() 以将Python对象转换回查询值。
例子:
class HandField(models.Field):
def get_prep_value(self, value):
return ''.join([''.join(l) for l in (value.north,
value.east, value.south, value.west)])
注意:如果您的自定义字段使用CHAR,VARCHAR或TEXT 类型为MySQL,你必须确保get_prep_value() 总是返回一个字符串类型。当对这些类型执行查询并且提供的值是整数时,MySQL执行灵活和意外匹配,这可能导致查询在其结果中包含意外对象。如果始终从中返回字符串类型,则不会发生此问题get_prep_value()。
将查询值转换为数据库值:get_db_prep_save()
某些数据类型(例如,日期)需要采用特定格式,然后才能被数据库后端使用。 get_db_prep_value()是应该进行这些转换的方法。将用于查询的特定连接作为connection参数传递。这允许您在需要时使用特定于后端的转换逻辑。
例如,Django使用以下方法 BinaryField:
def get_db_prep_value(self, value, connection, prepared=False):
value = super().get_db_prep_value(value, connection, prepared)
if value is not None:
return connection.Database.Binary(value)
return value
如果您的自定义字段在保存时需要特殊转换,这与用于普通查询参数的转换不同,您可以覆盖get_db_prep_save()。
保存在前预处理数值
如果要在保存之前预处理该值,则可以使用 pre_save()。例如,Django DateTimeField使用此方法在auto_now或 的情况下正确设置属性auto_now_add。
如果覆盖此方法,则必须在结尾处返回属性的值。如果对值进行任何更改,则还应更新模型的属性,以便保持对模型的引用的代码始终能够看到正确的值。
一些一般建议
编写自定义字段可能是一个棘手的过程,特别是如果您在Python类型与数据库和序列化格式之间进行复杂的转换。以下是一些使事情变得更顺利的提示:
查看现有的Django字段(in django/db/models/fields/__init__.py)
以获取灵感。尝试找到一个类似于你想要的字段并稍微扩展它,而不是从头开始创建一个全新的字段。
将一个__str__()方法放在您要作为字段包装的类上。在很多地方,字段代码的默认行为是调用 str()值。(在本文档的示例中,value将是一个Hand实例,而不是a HandField)。因此,如果您的__str__() 方法自动转换为Python对象的字符串形式,您可以节省大量的工作。