目錄
序言
最近在做公司的一個項目,我是產品經理,本不用自己碼代碼,但是實在是手癢。。。。所以就做做。主要遇到一個問題,自定義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對象的字符串形式,您可以節省大量的工作。