Django model總結(上)

JohnYang819發表於2020-10-27

Django model是django框架中處於比較核心的一個部位,準備分三個部落格從不同的方面分別進行闡述,本文為《上篇》,主要對【a】Model的基本流程,比如它的建立,遷移等;預設行為,以及用定製的行為來覆蓋預設的行為;遷移檔案相關的操作,比如不同的建立遷移檔案的方法,對遷移檔案進行重新命名,將多個遷移檔案壓縮成一個遷移檔案,遷移檔案中的變數的含義,以及遷移檔案的回滾【b】資料型別,不同model之間的關係,以及對model的事務管理【c】一些用於減輕資料庫與model之間互動作業的工具,比如用fixture檔案對model資料進行備份和載入【d】支援軟體observer pattern的signal【e】在預設位置以外的配置以及對多種資料庫的配置及其在model中的應用。

Django model與遷移流程

Django的主要儲存技術是利用關係型資料庫,所以Django的model對應的是資料庫中的table,該表的表名預設是<app_name>_<model_name>,而model的例項則是該表的一個記錄。因為跟資料打交道,所以model經常需要不斷的更改,而管理這樣的變動是通過遷移檔案實現的。

建立Django model

Django model是在每個app中的models.py中,這個models.py檔案是當app建立時候就自動建立的。

#demo
from django.db import models
from datetime import date,timedelta
from django.utils import timezone 
# Create your models here.
class Stores(models.Model):
    name=models.CharField(max_length=30,unique_for_date='date_lastupdate',db_column='my_custom_name')
    #id=models.AutoField(primary_key=True) 
    #objects=models.Manage()
    address=models.CharField(max_length=30,unique=True)
    city=models.CharField(max_length=30)
    state=models.CharField(max_length=2)
    email=models.EmailField(help_text='valide email please')
    date=models.DateField(default=date.today)
    datetime=models.DateTimeField(default=timezone.now)
    date_lastupdate=models.DateField(auto_now=True)
    date_added=models.DateField(auto_now_add=True)
    timestamp_lastupdated=models.DateTimeField(auto_now=True)
    timestamp_added=models.DateTimeField(auto_now_add=True)
    testeditable=models.CharField(max_length=3,editable=False)
    def __str__(self):
        return self.name 

以上程式碼就建立了一個model,其中被註釋掉的id的AutoField被預設自動新增;被註釋掉的objects是model的Manager,負責該Model的增刪查改;還包含了很多欄位如CharField,DateField等;__str__方法方便model例項的顯示;以上具體內容將在下面各個位置進行總結。

最後應注意該app必須被包含在project的settings.py的INSTALLED_APPS列表中。

遷移及基本流程

Django model 資料型別

Django model中的資料型別與在兩個層面有關:底層資料庫層面Django/Python層面

當建立model後,並進行首次遷移,Django產生並執行DDL(Data definition language)來建立資料庫表,這個DDL包含了model中各欄位的定義,比如Django model的IntegerField被轉換為底層資料庫的INTEGER,這就意味著如果後面要修改該欄位的型別,就必須再次進行遷移操作。

model的資料型別也有在Django/Python層面進行操作的,比如

ITEM_SIZES=(
    ('S','Small'),
    ('M','Medium'),
    ('L','Large'),
    ('P','Portion')
)
class Menu(models.Model):
    name=models.CharField(max_length=30)
    def __str__(self):
        return self.name 
class Item(models.Model):
    menu=models.ForeignKey(Menu,on_delete=models.CASCADE,related_name='menus')
    name=models.CharField(max_length=30)
    description=models.CharField(max_length=100)
    size=models.CharField(choices=ITEM_SIZES,max_length=1)
    colories=models.IntegerField(validators=[calorie_watcher])
    def __str__(self):
        return self.name 

這裡,Item的size中有choices,這個限制就是在python層面進行限制的,我們可以通過python manage.py banners 0001來檢視(banner 是app名字,0001是首次遷移的字首):

(env) F:\PycharmProject\webDev\Django project\testBegin>python manage.py sqlmigrate banners 0001
BEGIN;
--
-- Create model Item
--
CREATE TABLE "banners_item" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(30) NOT NULL, "description" varchar(100) NOT NULL, "size" varchar(1) NOT NULL, "menu_id" integer NOT NULL REFERENCES "banners_menu" ("id") DEFERRABLE INITIALLY DEFERRED);
COMMIT;

可以發現底層SQL中關於size並沒有choice的限制,也就是該限制是在python層面中實施的,即如果後續更改choices,可以不再進行資料遷移。

Data type Django model Data type Django model
Binary BinaryField Boolean BooleanField
Date/time DateField Date/time TimeField
Date/time DurationField Number AutoField
Number BigInteger Number DecimalField
Number FloatField Number IntegerField
Number PositiveIntegerField Number PositiveSmallIntegerField
Text CharField Text TextField
Text CommaSeparatedIntegerField Text EmailField
Text FileField Text FilePathField
Text(specialized) ImageField Text(specialized) GenericIPAddress
Text(specialized) SlugField Text(specialized) URLField
限值:max_length,min_value,max_value,max_digits,decimal_places

對於text-based的欄位,max_length限定了最大字元數量;對於IntegerField資料型別,Django提供了min_value來限制其最小值,max_value限制其最大值;對於DecimalField,需要標明max_digitsdecimal_places來限制數值的最多的位數和有效值。

Empty,Null,Not Null: Blank 和Null

所有欄位預設都是資料庫級別的NOT NULL限制(即預設null=False),即如果建立/更新一個NOT NULL欄位,必須為該欄位提供值,否則資料庫拒絕接受該欄位;而有時候,允許一個空欄位是必要的,因為資料庫中可以識別NULL,因此為了允許在資料庫層面支援NULL值,可通過null=True進行設定。

除了 null,Django還支援blank選項,該選項預設為False,被用在python/Django層面的校驗通過在model上的Form。如果一個欄位被宣告為blank=True,Django允許在form中留空,否則Django拒絕接受該值。

例如:

class Person(models.Model):
    first_name=models.CharField(max_length=30)
    middle_name=models.CharField(max_length=30)
    last_name=models.CharField(max_length=30)
blank和null的組合 預設(blank=False,null=False) null=True blank=True null=True,blank=True,
Person.objects.create(first_name='john',middle_name=None,last_name='Batch') None被處理為NULL,資料庫拒絕 資料庫層面可以為NULL None被處理為NULL,資料庫拒絕 資料庫層面可以為NULL,OK
Person.objects.create(first_name='john',last_name='Batch') 未標明的被當作空字元,資料庫層面不拒絕,空字元不是NULL 資料庫層面OK 資料庫層面OK 資料庫層面OK
Person.objects.create(first_name='john',middle_name=‘ ’,last_name='Batch') 資料庫層面OK 資料庫層面OK 資料庫層面OK 資料庫層面OK
Form Validation validation error,因為blank=False validation error,因為blank=False 校驗成功 校驗成功

對於blank,null的引數有四種組合,分別是default(blank=False,null=False),blank=True(null=False),null=True(blank=False),blank=True& null=True,而'middle_name'有三種選擇,分別是賦值為None,'',以及不賦值(使用預設值,等同於賦值為''),所有一共12種情況(如上表所示),但不管哪種情況,只要null=False,賦值為None在資料庫層面不通過,只要blank=False,在form校驗時就不通過。

預設定值(predetermined values):default,auto_now,auto_now_add,choices

可使用default對model欄位進行預設值設定,該defaut引數既可以是一個值,也可以是方法的引用(method reference),該選項是完全由python/Django層面控制的。

儘管default選項對於text 欄位,number欄位以及boolean欄位都是一樣的操作,但對於DateFieldDatetimeField來講,它們有自己獨特的預設值設定選項,該選項為auto_nowauto_now_add

在#demo中,datedatetime欄位均用default使用了method reference(注意到datetime的default為timezone.now,date的default用了date.today,這裡都使用method reference,因為如果帶上(),其實也沒有錯,但是將在編譯階段就呼叫了函式,從而得到一個值,不能動態的返回當前計算值)。

對於auto_nowauto_now_add,這兩個選項與default類似的地方是都自動為欄位提供了預設值,,但也有不同,不同之處在於:(1)僅對欄位DateField,DateTimeField有效,對於DateFieldauto_nowauto_now_add通過date.today來產生值,對於DateTimeField,auto_nowauto_now_add是通過django.utils.timezone.now來產生值(2)auto_nowauto_now_add不能被覆蓋,auto_now選項每當一個record被改變,這個值就會改變,而auto_now_add選項在record存在期間保持不變,這就意味著我們可以用auto_now來追蹤record的最近的一次修改,用auto_now_add來追蹤record的建立時間(3)需要注意的是,在admin中,auto_nowauto_now_add不顯示,因為它們會被自動取值,且不能被覆蓋,所有就不顯示出來。

另一個預設值是choices,它是在python/Django層面控制的。

Unique:unique,unique_for_date,unique_for_month,unique_for_year

比如name=models.CharField(max_length=30,unique=True),它告訴了Django來確保所有記錄都有一個唯一的name

unique是在資料庫層執行的(通過DDL的UNIQUESQL常量),還有Pyhton/Django層面,另外除了ManyToMany,OneToOne,FileField,對於其他的欄位都適用。

對於伴隨著唯一時間的欄位,比如name=models.CharField(max_length=30,unique_for_date='date_lastupdated'),不允許同一個name有相同的date_lasupdated欄位,而這個validation由於其複雜性,在python/Django層面進行控制。

表單值:Editable,help_text,verbose_name,error_messages

當form的欄位由models欄位支撐時,form欄位可能被models欄位的選項所影響。

預設地,所有models欄位都是可編輯的,但是可通過editable=False來告訴Django來忽略它,然後任何與它相關的validation也會被忽略。

例如:將#demo中的city改為city=models.CharField(max_length=30,editable=False),經過makemigrations後,再sqlmigrate,檢視

BEGIN;
--
-- Alter field city on store
--
CREATE TABLE "new__customized store" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "city" varchar(30) NOT NULL, "name" varchar(30) NOT NULL, "address" varchar(30) NOT NULL UNIQUE, "state" varchar(2) NOT NULL, "email" varchar(254) NOT NULL, "date" date NOT NULL, "datetime" datetime NOT NULL, "date_lastupdated" date NOT NULL, "date_added" date NOT NULL, "timestamp_lastupdated" datetime NOT NULL, "timestamp_added" datetime NOT NULL);
INSERT INTO "new__customized store" ("id", "name", "address", "state", "email", "date", "datetime", "date_lastupdated", "date_added", "timestamp_lastupdated", "timestamp_added", "city") SELECT "id", "name", "address", "state", "email", "date", "datetime", "date_lastupdated", "date_added", "timestamp_lastupdated", "timestamp_added", "city" FROM "customized store";
DROP TABLE "customized store";
ALTER TABLE "new__customized store" RENAME TO "customized store";
COMMIT;

可以發現(1)修改欄位是通過新建一個表,將record移植後,刪除舊錶,再重新更改新表為舊錶名字(2)由於設定了city為editable=False,DDL中未出現city,也說明該操作在資料庫層面進行的(3)migrate後,檢視admin,發現city確實不見了。

之前:

Django model總結(上)

之後:

Django model總結(上)

help_text在旁邊給出了關於該欄位的解釋資訊,verbose_name選項使得在表單中輸出為該欄位的label,error_messages可接受字典,該字典的keys代表錯誤程式碼,values是錯誤資訊。

DDL值:db_column,db_index,db_tablespace,primary_key

預設地,Django用model的欄位名作為表的column的名字,如果要改變,可以在欄位裡使用db_column選項,另一個就是db_index選項,但應注意以下兩種情況不需要再設定db_index:

  • unique=True
  • ForeignKey

比如:修改#demo為

city=models.CharField(max_length=30,db_index=True)
state=models.CharField(max_length=2,db_column='customized state JOHNYANG')

經過sqlmigrate:

CREATE TABLE "new__customized store" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(30) NOT NULL, "address" varchar(30) NOT NULL UNIQUE, "city" varchar(30) NOT NULL, "email" varchar(254) NOT NULL, "date" date NOT NULL, "datetime" datetime NOT NULL, "date_lastupdated" date NOT NULL, "date_added" date NOT NULL, "timestamp_lastupdated" datetime NOT NULL, "timestamp_added" datetime NOT NULL, "customized state JOHNYANG" varchar(2) NOT NULL);
INSERT INTO "new__customized store" ("id", "name", "address", "city", "email", "date", "datetime", "date_lastupdated", "date_added", "timestamp_lastupdated", "timestamp_added", "customized state JOHNYANG") SELECT "id", "name", "address", "city", "email", "date", "datetime", "date_lastupdated", "date_added", "timestamp_lastupdated", "timestamp_added", "state" FROM "customized store";  
DROP TABLE "customized store";
ALTER TABLE "new__customized store" RENAME TO "customized store";
CREATE INDEX "customized store_city_5ed81ba0" ON "customized store" ("city");
COMMIT;

可以發現state欄位消失,增加了customized state JOHNYSNG,和最後CREATE INDEX "customized store_city_5ed81ba0" ON "customized store" ("city")

最後,primary_key選項允許對Model定義一個主鍵,如果沒有一個欄位定義primary_key=True,則自動建立一個名為idAutoField欄位來充當主鍵(系統自動執行id=models.AutoField(primary_key=True).

內建和自定義校核器:validators

除了資料型別,長度限制,預設值等之前的選項的約束限制,Django還提供了validator選項(賦值為列表),來允許進行更復雜,高階的邏輯校核。

django.core.validators包中提供了一系列內建的校核方法。比如models.EmailField資料型別依賴django.core.validators.EmailValidator來校核email值,而models.IntegerField使用MinValueValidatorMaxValueValidator來對min_valuemax_value來進行校核。

除了內建validator方法,也可以自定義validator方法,定義該方法很簡單,只需要接受一個欄位為輸入引數,然後如果不滿足某個條件就丟擲django.core.exceptions.ValidatorError就可以。

ITEM_SIZE=(
    ('S','Small'),
    ('M','Medium'),
    ('L','Large'),
    ('P','Portion')
)
from django.core.exceptions import ValidationError
def calorie_watcher(value):
    if value>5000:
        raise ValidationError(('calories are %(value)s? try something less than 5000'),params={'value':value})
    if value<0:
        raise ValidationError(('Strange calories are %(value)s'),params={'value':value})

class Menu(models.Model):
    name=models.CharField(max_length=30)
    def __str__(self):
        return self.name 

class Item(models.Model):
    menu=models.ForeignKey(Menu,on_delete=models.CASCADE)
    name=models.CharField(max_length=30)
    description=models.CharField(max_length=100)
    price=models.FloatField(blank=True,null=True)
    size=models.CharField(choices=ITEM_SIZE,max_length=1)
    calories=models.IntegerField(validators=[calorie_watcher])
    def __str__(self):
        return 'Menu %s name %s' %(self.menu,self.name )
        

在admin中(需要注意的是如果要在admin中管理資料庫,需要在admin.py對需要管理的表進行註冊,如

from django.contrib import admin

# Register your models here.
from .models import Question,Choice,Store,Menu,Item
admin.site.register((Question,Choice,Store,Menu,Item))

)對Item進行新增,如果calorie小於0,可以看到報錯資訊:

Django model總結(上)

Django model預設和定製的行為

Django對Model提供了很多函式,比如基本操作save(),delete(),還有命名慣例以及查詢行為等。

Model方法

所有model都繼承了一系列方法,如儲存,刪除,校核,載入,還有對model 資料施加特定的邏輯等。

save()

save()方法是最常見的一個操作,對一個記錄進行儲存。一旦建立或者獲取了model instance的reference,則可以呼叫save()來進行建立/更新 該instance。

那麼django是如何判定一個save()到底是該建立還是更新一個instance?

它是通過id來進行判斷的,如果reference的id在資料庫中還沒有,那麼建立,如果已經有了則是更新。

save()方法可接受的引數如下:

引數 預設值 描述
force_insert False 顯式的告訴Django對一個記錄進行建立(force_insert=True)。極少這樣用,但是對於不能依靠Django來決定是否建立的情況是有用的。
force_update False 顯式的告訴Django對一個記錄進行更新(force_update=True)。極少這樣用,但是對於不能依靠Django來決定是否更新的情況是有用的。
using DEFAULT_DB_ALLAS 允許save()對settings.py中不是預設值的資料庫進行儲存/更新操作(比如,using='oracle',這裡'oracle'必須是settings.py的'DATABASES'變數的一個key),詳細用法見本文多資料庫部分總結
update_fields None 接受包含多個欄位的list(比如save(update_fields)=['name']),僅更新list中的欄位。當有比較大的model,像進行更有效率更精細的更新,可以使用該選項。
commit True 確保記錄儲存到了資料庫。在特定情況下(比如,models forms或者關係型操作),commit被設定為False,來建立一個instance,而不將其儲存到資料庫。這允許基於它們的輸出來進行額外的操作 。
class Store(models.Model):
    name=models.CharField(max_length=30)
    address=models.CharField(max_length=30)
    city=models.CharField(max_length=30)
    state=models.CharField(max_length=2)
    def save(self,*args,**kwargs):
        #Do custom logic here(e.g. validation,logging,call third party service)
        #Run default save() method
        super().save(*args,**kwargs)
        

也可以對save()method進行重寫,可以在它之前或者之後做一些事情。

delete()

該方法是通過一個reference來從資料庫中消除一條記錄。delete()實際上是依靠id主鍵來移除一個記錄,所以對於一個reference應該有id值來進行delete()操作。

delete()被一個reference呼叫時,它的id主鍵被移除,但它剩餘的值還儲存在記憶體中。另外它用刪除記錄的數量作為回應。比如(1, {'testapp.Item': 1}).

save()類似,delete()也接受兩個引數:using=DEFAULT_DB_ALIAS,keep_parents=False。using允許對其他資料庫進行delete操作,當delete()發生在關係型model,希望保持parent model完整或者移除,keep_parents是很有用的。

最後,也可以對delete()方法進行重寫。

校驗方法:clean_fields(),clean(),validate_unique,full_clean()

理解校驗方法最重要的部分還是從兩方面進行:資料庫層面和Python/Django層面。

對於資料庫層面的校驗,在第一次遷移時就自動完成了,而對於Python/Django層面,必須呼叫校驗函式來進行校驗。

需要注意的是:對於sqlite3,除primary key必須是整數外,其他的資料型別並沒有嚴格的校核,Char(10)的含義是至少佔用10個位元組,所以會出現,如果直接建立一個超過10個位元組的記錄,也是可以保持到資料庫的現象。

比如:

image-20201019103900920

>>> s=Stores.objects.create(name='this is a veryvery long name which exceeds 30 characters let us see if it will raise an error',address='cd',city='cd',state='ca',email='sdfkjs@qq.com')
>>> len('this is a veryvery long name which exceeds 30 characters let us see if it will raise an error')
93
  • clean_fields()

儘管可以通過呼叫save()來讓資料庫校驗欄位的合法性(對sqlite3,資料庫不對欄位進行資料型別檢查,除primary key外),但也可以通過clean_fields()在Python/Django層面進行欄位資料型別的合法性檢驗。

>>> s.clean_fields()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\models\base.py", line 1253, in clean_fields
    raise ValidationError(errors)
django.core.exceptions.ValidationError: {'name': ['Ensure this value has at most 30 characters (it has 93).']}
  • validate_unique()

對於有unique選項的欄位,可通過記錄例項的validate_unique()方法進行unique檢驗。

>>> p=Stores(name='testname',address='科華北路',city='cd',state='ca',email='sjkfd@qq.com')
>>> p.validate_unique()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\models\base.py", line 1013, in validate_unique
    raise ValidationError(errors)
django.core.exceptions.ValidationError: {'address': ['Stores with this Address already exists.']}

除了對欄位的unique選項進行unique檢驗,validate_unique()還對model的Meta class的unique_together屬性(賦值為元組)進行檢驗,即多個欄位組成的唯一性。

例如:

class TestAll(models.Model):
    test0=models.CharField(max_length=3)
    test1=models.CharField(max_length=3)
    class Meta:
        unique_together=('test0','test1')
(env) F:\PycharmProject\webDev\Django project\testBegin>python manage.py sqlmigrate banners 0007
CREATE TABLE "banners_testall" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "test0" varchar(3) NOT NULL, "test1" varchar(3) NOT NULL);
CREATE UNIQUE INDEX "banners_testall_test0_test1_2f0f2688_uniq" ON "banners_testall" ("test0", "test1");
>>> from banners.models import *
>>> s=TestAll(test0='a',test1='b')
>>> ss=TestAll(test0='a',test1='b')
>>> ss.validate_unique()
>>> s.validate_unique()
>>> s.save()
>>> ss.validate_unique()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\models\base.py", line 1013, in validate_unique
    raise ValidationError(errors)
django.core.exceptions.ValidationError: {'__all__': ['Test all with this Test0 and Test1 already exists.']}
>>> s.validate_unique()
>>> ss.save()
Traceback (most recent call last):
  File "F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\backends\utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\backends\sqlite3\base.py", line 413, in execute
    return Database.Cursor.execute(self, query, params)
sqlite3.IntegrityError: UNIQUE constraint failed: banners_testall.test0, banners_testall.test1

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\models\base.py", line 751, in save
    force_update=force_update, update_fields=update_fields)
  File "F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\models\base.py", line 789, in save_base
    force_update, using, update_fields,
  File "F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\models\base.py", line 892, in _save_table
    results = self._do_insert(cls._base_manager, using, fields, returning_fields, raw)
  File "F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\models\base.py", line 932, in _do_insert
    using=using, raw=raw,
  File "F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\models\manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\models\query.py", line 1249, in _insert
    return query.get_compiler(using=using).execute_sql(returning_fields)
  File "F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\models\sql\compiler.py", line 1395, in execute_sql
    cursor.execute(sql, params)
  File "F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\backends\utils.py", line 98, in execute
    return super().execute(sql, params)
  File "F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\backends\utils.py", line 66, in execute
    return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
  File "F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\backends\utils.py", line 75, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\backends\utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\utils.py", line 90, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\backends\utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\backends\sqlite3\base.py", line 413, in execute
    return Database.Cursor.execute(self, query, params)
django.db.utils.IntegrityError: UNIQUE constraint failed: banners_testall.test0, banners_testall.test1
>>> sss=TestAll(test0='a',test1='b')
>>> sss.validate_unique()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\models\base.py", line 1013, in validate_unique
    raise ValidationError(errors)
django.core.exceptions.ValidationError: {'__all__': ['Test all with this Test0 and Test1 already exists.']}
>>>

可見,當s儲存前,sss並不衝突,但是當s儲存後,再呼叫ss.validate_unique()報錯。

  • clean()

除了clean_fields(),還有clean(),用來提供更靈活的校驗方法(比如特定的關係或者值)。

例如:

from django.core.exceptions import ValidationError
class TestAll(models.Model):
    test0=models.CharField(max_length=3)
    test1=models.CharField(max_length=3)
    def clean(self):
        if self.test0==self.test1:
            raise ValidationError('test0 shall not equil to test1')
>>> from banners.models import *
>>> ss=TestAll(test0='c',test1='c')
>>> ss.save()
>>> ss.clean()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "F:\PycharmProject\webDev\Django project\testBegin\banners\models.py", line 99, in clean
    raise ValidationError('test0 shall not equil to test1')
django.core.exceptions.ValidationError: ['test0 shall not equil to test1']
  • full_clean()

最後是full_clean(),它是依次執行clean_fields(),clean(),validate_unique().

資料載入方法:refresh_from_db()

當資料庫被另一個程式更新,或者不小心改變了model instance的值,也就是想用資料庫中的資料來更新model instance的值,refresh_from_db()方法就是用來做這件事的方法.

儘管一般都是不帶引數的使用refresh_from_db(),但它可以接受兩個引數:(1)using,該引數跟save(),delete()方法中的含義一樣(2)fields,用來挑選需要refresh的欄位,如果沒有指定,則refresh model的所有欄位.

from_db(),get_deferred_fields()用來定製載入資料過程,用的情況相對比較少,詳見https://docs.djangoproject.com/en/3.1/ref/models/instances/

定製方法

例:

from django.core.exceptions import ValidationError
class TestAll(models.Model):
    test0=models.CharField(max_length=3)
    test1=models.CharField(max_length=3)
    def clean(self):
        if self.test0==self.test1:
            raise ValidationError('test0 shall not equil to test1')
    def special(self):
        return self.test0,self.test1
>>> from banners.models import *
>>> s=TestAll.objects.all().first()
>>> s.test0
'a'
>>> s.special()
('a', 'b')

Model 管理欄位:Objects

objects欄位是所有Django models的預設管理欄位,用來管理各種query操作(增刪查改).

它是直接用在class上,而不是instance上,比如,要讀取Store的id=1的記錄,可以Store.objects.get(id=1),而刪除所有記錄則可以Store.objects.all().delete().

可以定製管理欄位,比如重新命名為mgr,則只需要新增類屬性mgr=models.Manager().

Model Meta class及選項

Django中的Meta類是用來把model的行為作為一個整體進行定義,與資料型別不同,後者在欄位上定義了更精細的行為.

比如為了避免不斷的顯式的宣告一個model的搜尋順序,可以直接在Meta 類中定義ordering.

比如:

class Stores(models.Model):
    name=models.CharField(max_length=30,unique_for_date='date_lastupdate',db_column='my_custom_name')
    address=models.CharField(max_length=30,unique=True)
    city=models.CharField(max_length=30)
    state=models.CharField(max_length=2)
    email=models.EmailField(help_text='valide email please')
    date=models.DateField(default=date.today)
    datetime=models.DateTimeField(default=timezone.now)
    date_lastupdate=models.DateField(auto_now=True)
    date_added=models.DateField(auto_now_add=True)
    timestamp_lastupdated=models.DateTimeField(auto_now=True)
    timestamp_added=models.DateTimeField(auto_now_add=True)
    testeditable=models.CharField(max_length=3,editable=False)
    def __str__(self):
        return self.name 
    class Meta:
        ordering=['-timestamp_added']
>>>a=Stores.objects.all()
>>> for i in a:
...     print(i.timestamp_added)
...
2020-10-19 02:38:22.767910+00:00
2020-09-28 07:45:12.708000+00:00
2020-09-28 05:05:08.066000+00:00

可以看到是按照timestamp_added降序排列的.

DDL table options: db_table,db_tablespace,managed,unique_together
  • db_table

預設地,建立表的名字為<app_name>_<model_name>,比如,app名字為'banners',model名字為'testall',那麼建立的表名為'banners_testall'.

sqlite> .tables
auth_group                  banners_entry_authors
auth_group_permissions      banners_item
auth_permission             banners_menu
auth_user                   banners_question
auth_user_groups            banners_stores
auth_user_user_permissions  banners_testall
banners_author              django_admin_log
banners_blog                django_content_type
banners_choice              django_migrations
banners_entry               django_session

現在新增meta option:

class TestAll(models.Model):
    test0=models.CharField(max_length=3)
    test1=models.CharField(max_length=3)
    def clean(self):
        if self.test0==self.test1:
            raise ValidationError('test0 shall not equil to test1')
    def special(self):
        return self.test0,self.test1
    class Meta:
        unique_together=('test0','test1')
        db_table='specialTestAll' #新增db_table
(env) F:\PycharmProject\webDev\Django project\testBegin>python manage.py sqlmigrate banners 0008
BEGIN;
--
-- Rename table for testall to specialTestAll
--
ALTER TABLE "banners_testall" RENAME TO "specialTestAll";
COMMIT;

可見,已經將表名進行了更改。

  • db_tablespace

預設地,如果Django的資料庫支援(比如Oracle)tablespace的概念,Django 就用settings.py中的DEFAULT_TABLESPACE變數作為預設的tablespace.也可以通過meta的db_tablespace選項來對model設定其他的tablespace.

Django管理建立/銷燬資料表,如果不需要這樣的管理,可以設定meta的managed=False

DDL index options: indexes,index_together

Index在關係型資料庫記錄的高效查詢中是非常重要的,簡單的說,它是包含了用於確保查詢速度更快的包含了特定記錄的column values。

Django meta類提供了2個選項來對model欄位的index建立相應的DDL (如CREATE INDEX...):indexes,index_together.

對於primary keyunique的欄位,不需要再進行標註為index

index引數可接受包含多個models.Index例項的列表,models.Index例項接受fields列表,其包含了需要被index的欄位名,和name,用於命名Index的名字,預設情況下,自動為其進行命名。

from django.core.exceptions import ValidationError
class TestAll(models.Model):
    test0=models.CharField(max_length=3)
    test1=models.CharField(max_length=3)
    test2=models.CharField(max_length=3)
    test3=models.CharField(max_length=3)
    def clean(self):
        if self.test0==self.test1:
            raise ValidationError('test0 shall not equil to test1')
    def special(self):
        return self.test0,self.test1
    class Meta:
        unique_together=('test0','test1')
        db_table='specialTestAll'
        indexes=[
            models.Index(fields=['test0','test1']),
            models.Index(fields=['test0'],name='test0_idx')
        ]
        index_together=['test2','test3']
(env) F:\PycharmProject\webDev\Django project\testBegin>python manage.py sqlmigrate banners 0009
BEGIN;
CREATE UNIQUE INDEX "specialTestAll_test0_test1_f3575fe2_uniq" ON "specialTestAll" ("test0", "test1");
--
-- Alter index_together for testall (1 constraint(s))
--
CREATE INDEX "specialTestAll_test2_test3_20a904e7_idx" ON "specialTestAll" ("test2", "test3");
--
-- Create index specialTest_test0_eb859d_idx on field(s) test0, test1 of model testall
--
CREATE INDEX "specialTest_test0_eb859d_idx" ON "specialTestAll" ("test0", "test1");
--
-- Create index test0_idx on field(s) test0 of model testall
--
CREATE INDEX "test0_idx" ON "specialTestAll" ("test0");
COMMIT;

index_together允許定義多欄位index,index_together=['test0','test1']等效於indexes=[models.Index(fields=['test0','test1']) ].

命名選項:verbose_name,verbose_name_plural,label,label_lower,app_label

預設地,Django models是通過類名來refer model的,絕大多數都是可以的。但是對於有些情況,比如,做開發的時候,類名使用了縮寫,但如果想要在UI上,或者admin中不這樣顯示,該怎麼辦呢?

這就是verbose_name出現的原因了,verbose_name_plural是對英語的複數,比如class Choice的複數顯示是choices。

class TestAll(models.Model):
    test0=models.CharField(max_length=3)
    test1=models.CharField(max_length=3)
    test2=models.CharField(max_length=3)
    test3=models.CharField(max_length=3)
    class Meta:
        verbose_name='verboseTestAll'
        verbose_name_plural='VerboseTestAllPlural'

在admin中:

image-20201020203055640
>>> from testapp.models import *
>>> TestAll._meta.verbose_name
'verboseTestAll'
>>> TestAll._meta.verbose_name_plural
'VerboseTestAllPlural'
>>> TestAll._meta.label
'testapp.TestAll'
>>> TestAll._meta.label_lower
'testapp.testall'

繼承 Meta option: Abstract and proxy

類似於OOP的繼承,這裡model 的Meta類也有類似的概念,分別是abstractproxy

  • Abstract

對於abstract,它的用法是在model的Meta class中宣告abstract=True,這樣該model就變成了一個類似於base class的model,它不建立表,而繼承它的class自動獲取它定義的所有欄位。

  • Proxy

abstract相反,它的用法是base class就是正常的model,在其子類的Meta class中宣告proxy=True,子類不會建立新的表,子類所定義的行為選項都好像是直接作用在基類上的,但真正的基類的行為選項並沒有實質被改變,可以理解為子類對基類的修修補補,但還是作用在基類的表上的。

簡單總結下就是:在abstract和proxy情況下,帶有meta的沒有表,abstract出現在基類meta中,proxy 出現在子類的meta中。

class TestAbs(models.Model):
    test=models.CharField(max_length=3)
    class Meta:
        abstract=True 
class subAbs(TestAbs):
    pass 

class TestProxy(models.Model):
    test=models.CharField(max_length=3)

class subPro(TestProxy):
    class Meta:
        proxy=True 
(env) E:\programming\DjangoProject\TangleWithTheDjango\testDebug>python manage.py sqlmigrate testapp 0014
BEGIN;
--
-- Create model subAbs
--
CREATE TABLE "testapp_subabs" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "test" varchar(3) NOT NULL);
--
-- Create model TestProxy
--
CREATE TABLE "testapp_testproxy" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "test" varchar(3) NOT NULL);
--
-- Create proxy model subPro
--
COMMIT;

當進行django model query時,有很多預設的行為,如果要用非預設的query行為,則需要顯式的引數顯式的呼叫query的各種方法。然而,一次又一次的這樣做,無聊且易錯,所以我們可以依靠本節的query meta option來改變model query的各種預設行為。

  • ordering

ordering是用來定義預設的排序的,比如Store.objects.all().sort_by('-name')是按照name降序排列,等效為ordering=['-name'],需要注意的是賦值為列表型別。

  • order_with_respect_to

    按照某欄位進行排序,通常是外來鍵,這樣就可以使相互關聯的object允許它的parent object(parent object的id就是子object 的外來鍵)來通過get_RELATED_order,set_RELATED_ORDER來獲得及設定子object的順序。比如:

    class Question(models.Model):
        question_text=models.CharField(max_length=200)
        pub_date=models.DateTimeField('date published')
        def __str__(self):
            return self.question_text
        def was_published_recently(self):
            return self.pub_date>=timezone.now()-timedelta(days=1)
    class Answer(models.Model):
        question=models.ForeignKey(Question,on_delete=models.CASCADE)
    
    ;sqlmigrate來檢視其底層SQL語句
    CREATE TABLE "banners_question" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "question_text" varchar(200) NOT NULL, "pub_date" datetime NOT NULL);
    CREATE TABLE "banners_answer" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "question_id" integer NOT NULL REFERENCES "banners_question" ("id") DEFERRABLE INITIALLY DEFERRED, "_order" integer NOT NULL);
    CREATE INDEX "banners_answer_question_id_b2a3fa7a" ON "banners_answer" ("question_id");
    
    >>> from banners.models import *
    >>> import datetime
    >>> Question.objects.create(question_text='what?',pub_date=datetime.date.today())
    F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\models\fields\__init__.py:1312: RuntimeWarning: DateTimeField Question.pub_date received a naive datetime (2020-10-21 00:00:00) while time zone support
    is active.
      RuntimeWarning)
    <Question: what?>
     >>> s=Question.objects.get(id=1)
    >>> s
    <Question: what?>
    >>> s.pub_date
    datetime.datetime(2020, 10, 20, 16, 0, tzinfo=<UTC>)
    >>> Answer.objects.create(question=s)
    <Answer: Answer object (1)>
    >>> Answer.objects.cretae(question=s)
    Traceback (most recent call last):
      File "<console>", line 1, in <module>
    AttributeError: 'Manager' object has no attribute 'cretae'
    >>> Answer.objects.create(question=s)
    <Answer: Answer object (2)>
    >>> Answer.objects.create(question=s)
    <Answer: Answer object (3)>
    >>> s.get_answer_order()
    <QuerySet [1, 2, 3]>
    >>> s.set_answer_order([3,1,2])
    >>> a=Answer.objects.get(id=2)
    >>> a.get_next_in_order()
    Traceback (most recent call last):
      File "<console>", line 1, in <module>
      File "F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\models\base.py", line 981, in _get_next_or_previous_in_order
        }).order_by(order)[:1].get()
      File "F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\models\query.py", line 431, in get
        self.model._meta.object_name
    banners.models.Answer.DoesNotExist: Answer matching query does not exist.
    >>> a.get_previous_in_order()
    <Answer: Answer object (1)>
    
  • get_latest_by

在Meta中,get_latest_byDateField,DateTimeField,IntegerField的欄位的名字,或包含這種欄位名字的列表。這樣就可以用Managerlatest()earliest()方法。

比如:

class Question(models.Model):
    question_text=models.CharField(max_length=200)
    pub_date=models.DateTimeField('date published')
    def __str__(self):
        return self.question_text
    def was_published_recently(self):
        return self.pub_date>=timezone.now()-timedelta(days=1)
    class Meta:
        get_latest_by='pub_date'
 
;檢視sqlmigrate
(env) F:\PycharmProject\webDev\Django project\testBegin>python manage.py sqlmigrate banners 0011
BEGIN;
--
-- Change Meta options on question
--
--
-- Set order_with_respect_to on answer to None
--
CREATE TABLE "new__banners_answer" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "question_id" integer NOT NULL REFERENCES "banners_question" ("id") DEFERRABLE INITIALLY DEFERRED);
INSERT INTO "new__banners_answer" ("id", "question_id") SELECT "id", "question_id" FROM "banners_answer";
DROP TABLE "banners_answer";
ALTER TABLE "new__banners_answer" RENAME TO "banners_answer";
CREATE INDEX "banners_answer_question_id_b2a3fa7a" ON "banners_answer" ("question_id");
COMMIT;
>>> from banners.models import *
>>> import datetime
>>> datetime.date(2019,8,19)
datetime.date(2019, 8, 19)
>>> Question.objects.create(question_text='how?',pub_date=datetime.date(2019,3,4))
F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\models\fields\__init__.py:1312: RuntimeWarning: DateTimeField Question.pub_date received a naive datetime (2019-03-04 00:00:00) while time zone support
is active.
  RuntimeWarning)
<Question: how?>
>>> Question.objects.create(question_text='when?',pub_date=datetime.date(2017,2,2))
F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\models\fields\__init__.py:1312: RuntimeWarning: DateTimeField Question.pub_date received a naive datetime (2017-02-02 00:00:00) while time zone support
is active.
  RuntimeWarning)
<Question: when?>
>>> Question.objects.create(question_text='where?',pub_date=datetime.day(2018,3,3))
Traceback (most recent call last):
  File "<console>", line 1, in <module>
AttributeError: module 'datetime' has no attribute 'day'
>>> Question.objects.create(question_text='where?',pub_date=datetime.date(2018,3,3))
F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\models\fields\__init__.py:1312: RuntimeWarning: DateTimeField Question.pub_date received a naive datetime (2018-03-03 00:00:00) while time zone support
is active.
  RuntimeWarning)
<Question: where?>
>>> Question.objects.latest()
<Question: what?>
>>> Question.objects.earliest()
<Question: when?>
>>> s=Question.objects.earliest()
>>> s.pub_date
datetime.datetime(2017, 2, 1, 16, 0, tzinfo=<UTC>)
>>> Answer.objects.latest()  #可以看到,沒有設定get_latest_by,就不能用manager的latest方法
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\models\manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\models\query.py", line 674, in latest
    return self.reverse()._earliest(*fields)
  File "F:\PycharmProject\webDev\Django project\env\lib\site-packages\django\db\models\query.py", line 658, in _earliest
    "earliest() and latest() require either fields as positional "
ValueError: earliest() and latest() require either fields as positional arguments or 'get_latest_by' in the model's Meta.
  • default_manager_name

所有的models都將objects作為預設的model manager,但是當存在多個manager時,就必須指定哪個才是預設的manger,這時候,就需要在meta中對default_manager_name進行指定。

  • default_related_name

對於相互有關係的object,可以通過parent object 來反向得到子object:

>>> from banners.models import *
>>> q=Question.objects.get(id=1)
>>> q.answer_set.get(id=1)
<Answer: Answer object (1)>

這裡,預設是<model_name>_set,所以在本例中為answer_set.

因為對於欄位的反向名字應該是唯一的.所以在abstract subclass情況下需要注意不能衝突,可以用包含'%(app_label)s'和'%(model_name)s'來規避這種衝突.

# common/models.py
from django.db import models

class Base(models.Model):
    m2m = models.ManyToManyField(
        OtherModel,
        related_name="%(app_label)s_%(class)s_related",
        related_query_name="%(app_label)s_%(class)ss",
    )

    class Meta:
        abstract = True

class ChildA(Base):
    pass

class ChildB(Base):
    pass
# rare/models.py
from common.models import Base

class ChildB(Base):
    pass
  • Permisssion Meta operation: default_permissions, permission

預設地,default_permissions是('add','change','delete',‘view'),這就允許了對model instance的增刪改的操作.

比如:

class testPermission(models.Model):
    test0=models.TextField()
    class Meta:
        default_permissions=('add',)
        permissions=(('can_do_someting','we can do something'),)
(env) F:\PycharmProject\webDev\Django project\testBegin>python manage.py shell
Python 3.7.1 (v3.7.1:260ec2c36a, Oct 20 2018, 14:05:16) [MSC v.1915 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.contrib.auth.models import User
>>> user=User.objects.create_user('xiaoming','xiaoming@qq.com','3152')
>>> user.last_name
''
>>> user.save()
>>> op=User.objects.get(id=1)
>>> op.first_name
''
>>> op.name
Traceback (most recent call last):
  File "<console>", line 1, in <module>
AttributeError: 'User' object has no attribute 'name'
>>> op.get_all_permissions()
{'banners.delete_menu', 'banners.view_menu', 'banners.add_blog', 'sessions.view_session', 'banners.delete_entry', 'auth.change_user', 'banners.view_testall', 'banners.change_menu', 'sessions.delete_session', 'banners.view_choice', 'banners.view_item', 'banners.change_stores', 'banners.add_item', 'auth.view_user', 'banners.delete_item', 'admin.add_logentry', 'auth.add_permission', 'banners.add_testall', 'banners.view_stores', 'admin.delete_logentry', 'contenttypes.change_contenttype', 'banners.delete_answer', 'banners.change_author', 'banners.view_blog', 'admin.view_logentry', 'banners.change_answer', 'banners.change_blog', 'auth.delete_user', 'auth.add_user', 'banners.add_testpermission', 'banners.view_author', 'sessions.add_session', 'banners.add_menu', 'contenttypes.view_contenttype', 'contenttypes.delete_contenttype', 'banners.view_entry', 'banners.delete_author', 'banners.add_entry', 'auth.view_permission', 'banners.change_testall', 'banners.add_answer', 'auth.change_group', 'auth.view_group', 'banners.change_question', 'banners.add_question', 'banners.add_author', 'banners.view_answer', 'sessions.change_session', 'auth.delete_group', 'contenttypes.add_contenttype', 'banners.delete_blog', 'banners.delete_stores', 'banners.delete_choice', 'banners.change_entry', 'banners.can_do_someting', 'auth.delete_permission', 'auth.change_permission', 'banners.add_stores', 'banners.delete_question', 'banners.change_item', 'banners.add_choice', 'admin.change_logentry', 'banners.view_question', 'banners.delete_testall', 'auth.add_group', 'banners.change_choice'}
>>> op.has_perm('banners.delete_testpermission')
True

更詳細的解答可參考:https://www.cnblogs.com/37Y37/p/11658651.html

Django models 的關係型資料

簡而言之,關係型資料就是用不同資料庫的記錄的id或者pk來連線在一起,從而達到易維護,提高query效能,減少冗雜資料的目的.

Django支援以下三種關係型資料:一對多,多對多,一對一.

  • 一對多

一對多是指,一個model的記錄可聯結另一個model的多個記錄.比如一個Menu的記錄可包含多個Item記錄,而一個Item記錄只能對應一個Menu記錄,為了表達這樣的關係,可在Itemmodel中加入ForeignKey欄位,該欄位指明為Menu

  • 多對多

多對多關係指,一個model的記錄可對應另一個model的多個記錄,而另一個model的一個記錄也可以對應前者的多個記錄.比如Book的一個記錄可對應多個Author的記錄,而Author的一個記錄也可以對應多個Book.為了表達這種關係,可將ManyToManyField用在任意一個Model上.

  • 一對一

一對一有點像物件導向繼承的意思,比如Item如果和Drink進行一對一的聯結,那麼Item之用定義通用的欄位,而Drink定義詳細的,特定的欄位.

關係型資料型別的選型
資料整體性選型(Data integrity options):on_delete

關係型資料將不同的model繫結了起來,當一端被刪除,怎麼處理另一端是非常重要的.on_delete就是用於這個目的.

on_delete適合以上三種關係型資料型別,它的選型為:

  • models.CASCADE(default). 自動刪除相關的記錄.(比如Menubreakfast例項被刪除,所有相關的Item的記錄將被刪除)
Reference option: Self,literal strings
  • 'self'

model 關係有時候有遞迴關係。在一對多的父-子關係中比較常見。比如Categorymodel可以有一個parent,而這個parent就是另一個Category。為了定義這種關係,必須用self來指代相同的model。

但是在實際操作中,往往需要加上null=True,blank=True(在admin操作時,需要),因為如果要定義它,它的parent就必須存在,如果不允許null值,那麼就會報錯。

比如:

class Category(models.Model):
    menumenu=models.ForeignKey('self',on_delete=models.CASCADE,null=True,blank=True)
image-20201022214155077
  • literal string

儘管model關係型資料通常都用reference,但是使用model名字的字元也是合法的,比如models.ForeignKey('menu')。當model定義順序不允許索引還沒有建立的model時,該方法很有用,該方法也被稱為'laze loading'。如果跨app,可以將app_label加在前面,如models.ForeignKey(production.Manufacturer').

反向關係:related_name,related_query_name,symmetrical

對於關係型model,Django自動的用_set在資料間建立了反向關係。

例:

class parent(models.Model):
    name=models.CharField(max_length=5)
    def __str__(self):
        return self.name

class sub(models.Model):
    par=models.ForeignKey(parent,on_delete=models.CASCADE)
    name=models.CharField(max_length=5)
    def __str__(self):
        return self.name 
>>> from testapp.models import *
>>> a=parent.objects.create(name='johnyang')
>>> b=sub.objects.create(par=a,name='johnsub')
>>> c=sub.objects.create(par=a,name='johnsub2')
>>> d=parent.objects.create(name='tom')
>>> e=sub.objects.create(par=d,name='tomsub')
>>> a.sub_set.all()
<QuerySet [<sub: johnsub>, <sub: johnsub2>]>
>>> f=a.sub_set.all()
>>> f[0]
<sub: johnsub>
>>> print(f[0])
johnsub
>>> len(f)
2
>>> parent.objects.filter(sub__name='johnsub')
<QuerySet [<parent: johnyang>]>

從上面可以可看出,反向關係就是通過parent來反向索引/querysub

反向獲取所有其子model,預設通過instance的<submodelname>_set屬性,可通過related_name來進行定製;

通過model class manager的<submodelname>__...來query符合條件的parent instance,該<submodelname>可通過related_query_name來進行定製。

class parent(models.Model):
    name=models.CharField(max_length=5)
    def __str__(self):
        return self.name

class sub(models.Model):
    par=models.ForeignKey(parent,on_delete=models.CASCADE,related_name='subRelated',related_query_name='subQuery')
    name=models.CharField(max_length=5)
    def __str__(self):
        return self.name 
>>> from testapp.models import *
>>> parent.objects.filter(subQuery__id=1)
<QuerySet [<parent: johnyang>]>
>>> a=parent.objects.filter(subQuery__id=1)
>>> a[0]
<parent: johnyang>
>>> a[0].id
1
>>> a[0].subRelated.all()[0].id
1

related_name='+',表示拒絕進行反向索引:

class parent(models.Model):
    name=models.CharField(max_length=5)
    def __str__(self):
        return self.name

class sub(models.Model):
    par=models.ForeignKey(parent,on_delete=models.CASCADE,related_name='+',related_query_name='subQuery')
    name=models.CharField(max_length=5)
    def __str__(self):
        return self.name 
>>> from testapp.models import *
>>> a=parent.objects.get(id=1)
>>> a.sub_set.all()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
AttributeError: 'parent' object has no attribute 'sub_set'
  • symmetrical

ManyToMany='self',那麼Django認為這種多對多關係是對稱的,所以就沒有_set的反向索引,多對多的新增只能通過manytomany鍵的addcreate來進行新增。

class person(models.Model):
    friends=models.ManyToManyField('self')
>>> from testapp.models import *
>>> a=person.objects.create()
>>> b=person.objects.create()
>>> a.friends.create()
<person: person object (7)>
>>> a.friends.add(b)
>>> a.friends.all()
<QuerySet [<person: person object (6)>, <person: person object (7)>]>
>>> e=b.friends.create()
>>> e.friends.all()
<QuerySet [<person: person object (6)>]>
>>> b.friends.all()
<QuerySet [<person: person object (5)>, <person: person object (8)>]>
>>> e.friends_set #無_set
Traceback (most recent call last):
  File "<console>", line 1, in <module>
AttributeError: 'person' object has no attribute 'friends_set'
>>> e.person_set
Traceback (most recent call last):
  File "<console>", line 1, in <module>
AttributeError: 'person' object has no attribute 'person_set'

如果要查詢b的所有朋友,只需要b.friends.all()就可以了,而不能b.friends_set.all()

>>> from testapp.models import *
>>> amenity.objects.get(id=1).storee_set.all()
<QuerySet [<storee: storee object (1)>, <storee: storee object (2)>]>
>>> person.objects.get(id=1).friends.all()
<QuerySet []>
>>> person.objects.get(id=1).friends.create()
<person: person object (10)>
>>> amenity.objects.get(id=1).storee_set.create(name='now1024')
<storee: storee object (4)>

值得注意的是,對於多對多,不能像一對多那樣分別對多個model進行分別賦值,它可以進行反向索引進行createadd操作,對於selfsymmetrical=True,反向索引不能用_set

如果要取消這種對稱,只需要symmetrical=False

>>> from testapp.models import *
>>> a=person1.objects.create()
>>> a.friends_set.all()
Traceback (most recent call last):     
  File "<console>", line 1, in <module>
AttributeError: 'person1' object has no attribute 'friends_set'
>>> a.friends.all()
<QuerySet []>
>>> b=person1.objects.create()
>>> a.friends.add(b)
>>> a.friends.all()
<QuerySet [<person1: person1 object (2)>]>
>>> b.person_set.all()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
AttributeError: 'person1' object has no attribute 'person_set'
>>> b.person1_set.all()
<QuerySet [<person1: person1 object (1)>]>
>>> b.friends.all()
<QuerySet []>

這時候,查詢b的朋友就需要b.person1_set.all()了。

資料庫選項:to_field,db_constraint,swappable,through,through_fields,db_table

  • to_field

一般地,Django的關係型model是通過primary key,(預設是id)來建立的。比如,menu=models.ForeignKey(Menu)儲存Menu例項的id來作為索引。但也可以覆蓋該行為,用to_field,需要注意的是如果要對to_field進行賦值,必須設定unique=True

  • db_constraint

預設地,Django在資料庫層面遵循關係型資料庫的各種慣例。db_constrait選項,預設為True,允許通過設定為False來忽略這種約束,而這種設定也僅僅當你事先知道一個資料庫是破損的,不需要在資料庫層面檢查約束。

  • swappable

如果ManyToManyField指向一個swappable model,它控制資料的遷移行為。一般用在Usermodel,較少應用該選項。

  • through,through_fields,db_table

through,through_fields,db_table影響著junction table,如果要改變交叉表的名字,可以使用db_table

交叉表儲存了多對多的關係型資料的最少資訊:對於關係的id。來指定一個單獨的model來作為交叉表,然後儲存多對多的額外資訊也是可能的(比如,through=MyCustomModel,用MyCustomModel來作為一個多對多的額外表。如果定義了throuth選項,那麼用through_fields來告訴Django在新表中,哪些欄位用來儲存關係索引。

class person2(models.Model):
    name=models.CharField(max_length=128)
    def __str__(self):
        return self.name 
class group2(models.Model):
    name=models.CharField(max_length=128)
    members=models.ManyToManyField(person2,through='membership')
    def __str__(self):
        return self.name 
class membership(models.Model):
    person=models.ForeignKey(person2,on_delete=models.CASCADE)
    group=models.ForeignKey(group2,on_delete=models.CASCADE)
    date_joined=models.DateField(default=timezone.now)
    invite_reason=models.CharField(max_length=64)
    
#直接通過中間表來建立
>>> from testapp.models import *
>>> a=person2.objects.create(name='johnyang')
>>> b=group2.objects.create(name='gp1')
>>> m1=membership.objects.create(person=a,group=b,invite_reason='for test purpose')
>>> a.group2_set.all()
<QuerySet [<group2: gp1>]>
>>> a.group2_set.all().create(name='gp2')
<group2: gp2>
>>> m1.group
<group2: gp1>

如果標明瞭through_defaults,也可以用addcreate,set來新增關係

>>> c=person2.objects.create(name='tom')
>>> from datetime import date                   
>>> b.members.add(c,through_defaults={'date_joined':date(1991,5,5),'invite_reason':'startup'})
>>> b.members.create(name='hanson',through_defaults={'date_joined':date(2000,2,3),'invite_reason':'for development'})
<person2: hanson>
>>> d=person2.objects.create(name='yohn')
>>> e=person2.objects.create(name='porken')
>>> b.members.set([d,e],through_defaults={'date_joined':date(1991,5,5),'invite_reason':'startup'})
>>> m1 
<membership: membership object (1)>
>>> m1.person
<person2: johnyang>
>>> b.members.all()
<QuerySet [<person2: yohn>, <person2: porken>]>

remove用來移除一個記錄,clear用來移除全部的記錄。

>>> n=person2.objects.filter(name='yohn')
>>> b.members.remove(n[0])
>>> b.members.al()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
AttributeError: 'ManyRelatedManager' object has no attribute 'al'
>>> b.members.all()
<QuerySet [<person2: porken>]>
>>> b.members.clear()
>>> b.members.all()
<QuerySet []>

也可以從person2反向查詢:

>>> from testapp.models import *
>>> person2.objects.all()
<QuerySet [<person2: johnyang>, <person2: tom>, <person2: hanson>, <person2: yohn>, <person2: porken>]>
>>> group2.objects.all()
<QuerySet [<group2: gp1>, <group2: gp2>]>
>>> gp1=group2.objects.get(id=1)
>>> gp1
<group2: gp1>
>>> gp1.members.all()
<QuerySet []>
>>> gp2=group2.objects.get(id=2)
>>> gp2
<group2: gp2>
>>> gp2.members.all()
<QuerySet []>
>>> johnyang,tom,hanson,yohn,porken=person2.objects.all()
>>> johnyang
<person2: johnyang>
>>> gp1.members.set([johnyang,tom,hanson],through_defaults={'invite_reason':'for test'})
>>> gp2.members.set([hanson,yohn,porken],through_defaults={'invite_reason':'for interest'})
>>> membership.objects.all()
<QuerySet [<membership: membership object (6)>, <membership: membership object (7)>, <membership: membership object (8)>, <membership: membership object (9)>, <membership: membership object (10)>, <membership: membership object (11)>]>
>>> johnyang.membership_set.all()
<QuerySet [<membership: membership object (6)>]>
>>> johnyang.group2_set.all()
<QuerySet [<group2: gp1>]>
>>> hanson.membership_set.get(group=gp1).invite_reason
'for test'
>>> hanson.membership_set.get(group=gp2).invite_reason
'for interest'
  • Form 值:limit_choices_to

當Django model在forms中使用時,限制顯示關係型資料的數量是很有必要的。比如,對於有幾百乃至幾千個記錄的Item,其對應的model是Menu,那麼全部顯示出Item的記錄就是不切實際的。

limit_choices_to可用來在form中過濾顯示的關係型資料。

Django Model 事務

事務在model資料操作中扮演著非常重要的角色。當在Django中建立了一個資料庫,下面的有關事務的設定預設為:

AUTOCOMMIT=True
ATOMIC_REQUESTS=False

AUTOCOMMIT設定為True,保證了改變資料(Create,Update,Delete)的操作都進行自身的事務,然而也有很多種情況,是有必要將成組的操作包裝成一個事務(all or nothing)。

每個請求(REQUEST)的事務:ATOMIC_REQUESTS,裝飾器

Django支援ATOMIC_REQUESTS,預設是False,該選項是用來對每個對django application的請求進行一個事務操作,通過設定為True,保證了所有的資料操作都在一個請求內完成,如果這個請求成功的話。

當在view method中的邏輯是'all or nothing'的任務,比如,一個view method需要執行5個與資料有關的子任務(驗證,發郵件等),來確保所有子任務都成功,資料操作才結束,如果有一個子任務失敗,所有的子任務都回回滾,就像什麼也沒發生一樣,是非常重要的。

因為ATOMIC_REQUEST=True對每個request都開了事務,這就意味著可能對“高速”應用的效能有一定的影響。也是基於這樣的因素,單獨的對特定的request開啟/關閉”原子請求“(atomic request)也是被django支援的。

from django.db import transaction
# 當ATOMIC_REQUESTS=True ,可以單獨的關閉atomic requests
@transaction.non_atomic_requests
def index(request):
    #事務的commit/rollback
    data_operation_1()
    data_operation_2()
    data_operation_3()
#當ATOMIC_REQUESTS=False ,可以單獨的開啟atomic requests
@transaction.atomic
def detail(request):
    data_operation_1()
    data_operation_2()
    data_operation_3()

環境管理和回撥:atomic(),on_commit()

除了AUTOCOMMIT,ATOMIC_REQUEST配置和view method transaction decorator,也可以將事務運用在一箇中等的規模,意思就是,比單獨的資料操作(比如save())要粗糙,但是比atomic request(比如view method)要精細.

with可以激發context manager,它也是用`django.db.transaction.atomic,但不是裝飾器了,而是用在方法的內部。

from django.db import transaction

def login(request):
    # AUTO_COMMIT=True,ATOMIC_REQUESTS=False
    data_operation_standalone()
    
    with transaction.atomic:;
        # 開始事務
        #失敗後,回滾
        data_operation_1()
    	data_operation_2()
    	data_operation_3()
        #如果成功就commit
     data_operation_standalone2()

callback也是可以被支援的,通過on_commit,它的語法如下:

from django.db import transaction
transaction.on_commit(only_after_success_operation)
transaction.on_committ(lambda:only_affter_success_operation_with_args('success'))

Django model 遷移

遷移檔案實際上是model到資料庫的緩衝,我們可以在建立完遷移檔案後(python manage.py makemigrations,先不寫入資料庫,先提前看下model的變化以及相應的sql語句(使用python manage.py sqlmigrate),當然也可以回滾到models.py的某個時間點。

Migration 檔案建立

python manage.py makemigrations命令就是建立遷移檔案的第一步。如果不帶任何引數的執行該命令,則Django檢查INSTALLED_APP中宣告的所有app的models.py,然後為每個有改變的models.py建立遷移檔案。

下表給出了常用的引數:

Argument Description
<app_name> 專門對某個app的models.py進行建立遷移檔案,如python manage.py makemigrations stores
--dry-run 模擬建立遷移檔案,但不產生實際的遷移檔案,比如:(env) E:\programming\python manage.py makemigrations --dry-run<br/>Migrations for 'testapp':<br/> testapp\migrations\0028_testmig.py<br/> - Create model testmig
--empty 必須提供一個app_name,為其建立一個空遷移檔案。
--name 'my_mig_file' 為其建立一個自定義名字的遷移檔案,需要注意的是字首依然自動帶有序列號。
--merge 為有衝突的兩個遷移檔案進行merge操作。

Migration 檔案重新命名

遷移檔案不是放著不動,對它們進行重新命名也是可以的,或者說有時候甚至是必要的。需要進行的重新命名步驟取決於該遷移檔案是否已經遷移至資料庫。可以執行python manage.py showmigrations來檢視是否已經遷移至資料庫,帶有'x'的就是已經遷移到資料庫了。

比如:

image-20201025214540730

對於那些還沒有遷移到資料庫的,可以直接進行更改名字,因為這時候遷移檔案只是model 改變的一個表示,並沒有將這種變化反映到資料庫中。

對於已經反映到資料庫中的遷移檔案,有兩種辦法:(1)第一種方法是:修改遷移檔名字,更改掌控遷移活動的資料庫表來反映這個新名字,更新其他遷移檔案的dependencies(如果有的話)也來反映這個新名字。一旦更改了新的名字,進入django_migrations表中,檢視舊的記錄,更新為新的名字,如果有更為新一點的遷移檔案,更新一點的遷移檔案會有dependencies宣告,這個也需要更新。(2)第二種方法是:回滾到需要更該名字的遷移檔案之前,這就相當於還沒有將遷移檔案遷移至資料庫,然後直接更改遷移檔案的名字,再執行遷移。

遷移檔案壓縮(Migration file squashing)

一個經歷了很多改變的models.py,它的遷移檔案看可能達到數十,甚至數百個,在這種情況下,對它們進行壓縮也是有可能的。注意是‘壓縮’,而不是‘merge',’merge'往往指解決衝突。

語法是:

python manage.py squashmigrations <app_name> <squash_up_to_migration_file_serial_number>

該命令需要app名字以及需要壓縮到的遷移檔案的序列號,比如:squashmigrations stores 0004就是將遷移檔案0001,0002,0003,0004進行了壓縮;;squashmigrations stores 0002 0004對0002,0003,0004進行了壓縮。

壓縮後的遷移檔案遵從如下慣例:

<initial_serial_number>__squashed__<up_to_serial_number>__<date>.py

image-20201025225101091

壓縮的檔案替代了被壓縮的檔案的功能,也可以選擇保留老的檔案,之後的機理是新的壓縮檔案中用replaces欄位來表明哪個舊檔案被代替。

image-20201025225609594

遷移檔案結構

from django.db import migrations,models
class Migration(migrations.Migration):
    initial=True
    replaces=[
        
    ]
    dependencies=[
        
    ]
    operations=[
        
    ]

initial是布林值,用來表示是否是initial migration file,replaces是壓縮檔案中替換了哪個遷移檔案的列表,dependenciesoperations是用的最為頻繁的兩個欄位。

dependencies是一個包含了元組的('<app_name>','<migration_file>')列表,它告訴了遷移檔案是依賴<app_name>下的<migration_file>的,最常見的是新增跨app間的遷移檔案。比如,如果onlineapp依賴stores0002_data_population遷移檔案,那麼就可以新增一個dependency tuple到onlineapp的第一個遷移檔案,以確保它在stores的遷移檔案之後執行(比如('stores','0002_data_population'))

operations欄位宣告瞭包含遷移操作的列表。對於大部分情況,Django根據models的變化來建立遷移操作,比如,加了一個新的model,下一個遷移檔案就包含了django.db.migrations.operations.CreateModel();當重新命名了一個model,則下一個遷移檔案就包括了django.db.migrations.operations.RenameModel;當改變了一個model欄位就有AlterModel();等等。

最為常見的編輯operation是新增非DDL操作,這個不能被作為是models 的改變。可以通過RunSQL來插入SQL查詢的遷移,或者RunPython來進行python的邏輯遷移。

遷移檔案回滾

遷移檔案回滾非常簡單,只需要python manage.py migrate xxxx就可以回滾到xxxx序列號的地方。

比如:

image-20201026000445955

現在所有表為:

image-20201026000546857

回滾到最後一個:

image-20201026000954772

相應的所有表:

image-20201026001024920

通過比較,發現確實是回滾到了任一指定的位置。

這不是說一定就能回滾成功,當遇到不可逆的操作,就丟擲django.db.migrations.exceptions.IrreversibleError,當然,這並不意味不能進行回滾,而是需要額外的工作來使得檔案變得可回滾。

大多不可逆的操作是在遷移檔案中執行了特定的邏輯(RunSQL,RunPython),為了讓這些操作可回滾,就必須一樣地提供相應的回滾邏輯。下面”Django model初始值設立“部分將詳細講為RunSQLRunPython遷移操作建立可回滾操作。

Django 資料庫任務

Django的manage.py提供了很多命令來執行像備份,載入,刪除資料等任務。

備份:Fixture檔案,dumpdata,loaddata,inspectdb

dumpdataloaddata命令是django的資料備份與載入工具,Django用術語fixture來表示由dumpdata產生的,由loaddata來載入的資料結構。預設使用JSON格式,但是也可以用XML,YAML格式(需要安裝pyYAML).

dumpdata的常見用法如下:

  • manage.py dumpdata > all_model_data.json:將所有app的model的資料庫的資料輸出為json檔案,注意如果沒有>符號,全部列印在命令框中。
  • manage.py dumpdata <app_name> --format xml:將<app_name>下的model的資料以xml輸出。
  • manage.py dumpdata <app_name>.<model_name> --indent 2:對<app_name>下的<model_name>model的資料以縮排2字元的樣式輸出json格式。

manage.py loaddata來載入dumpdata產生的fixture檔案,它的語法就是manage.py loaddata <fixture_file_name>,fixture檔案路徑可以是相對的也可以是絕對的,除了給定的路徑,它還對每個app中的fixtures資料夾進行搜尋。

loaddata可以接受多個引數(比如loaddata <fixture_>,<fixture_2>...),這是有必要的,如果fixtures檔案有內部相互依賴關係。它還可以限制在特定app中進行搜尋/載入fixture檔案(比如,manage.py loaddata menu --app item,僅搜尋/載入在itemapp 下的fixtue 檔案menu`。)

manage.py inspectdb是由現有資料庫而建立model的逆向工程。

image-20201026130845698

刪除資料:flush,sqlflush,sqlsequencereset

manage.py flush將清除資料庫中所有資料,而manage.py sqlflush給出清除全部資料所使用的SQL,而並不實際清除。

image-20201026131912845

與資料直接互動:dbshell

manage.py dbshell將根據預設的資料庫而開啟相應的命令列shell。

Django model初始資料設立

在很多情況下,對Django model進行預先載入一些資料是有必要啊的,Django允許要麼在python中手工新增,或者用sql語句來新增,或者用fixture檔案。

第一步載入一個預先的資料就是先建立一個空的遷移檔案,來處理資料載入過程(manage.py makemigrations --empty <app_name>).

  • RunPython

例如:

建立model後,migrate,形成000_initial.py,但此時不要migrate,否則等建立空遷移檔案後,將不能把資料寫入資料庫.

python manage.py makemigrations --empty testmodel
# Generated by Django 3.1 on 2020-10-26 12:48

from django.db import migrations,models

def load_stores(apps,schema_editor):
    store=apps.get_model('testmodel','store')#<model_name>,<app_name>
    store_corporate=store(name='Corporate',address='623 borradway',city='San diego',state='CA')
    store_corporate.save()
    store_downtown=store(name='Downtown',address='Horton plazza',city='San diego',state='CA')
    store_downtown.save()
    store_uptown=store(name='Uptown',address='1240 university',city='San diego',state='CA')
    store_uptown.save()
    store_midtown=store(name='Midtown',address='784 washhington',city='San diego',state='CA')
    store_midtown.save()

def delete_stores(apps,schema_editor):
    """
    回滾.
    """
    store=apps.get_model('testmodel','store')
    store.objects.all().delete()

class Migration(migrations.Migration):

    dependencies = [
        ('testmodel', '0001_initial'),
    ]

    operations = [
        migrations.RunPython(load_stores,delete_stores),
    ]

python manage.py migrate testmodel
image-20201026215649357

可見,已經寫入了資料庫。

  • RunSQL

刪除剛才的RunPython的遷移檔案,新建一個空的0002號遷移檔案,在app下的sql資料夾下寫入以下store.sql檔案:

INSERT INTO testmodel_store (id,name,address,city,state) VALUES (0,'hongqi','tianrenlu','chengdu','CD');
INSERT INTO testmodel_store (id,name,address,city,state) VALUES (1,'haoyouduo','kehuabeilu','chengdu','CD');
INSERT INTO testmodel_store (id,name,address,city,state) VALUES (2,'jailefu','shuangdianxilu','chengdu','CD');
INSERT INTO testmodel_store (id,name,address,city,state) VALUES (3,'hongqi','wenwenglu','chengdu','CD');
# Generated by Django 3.1 on 2020-10-26 14:08

from django.db import migrations,models

def load_store_from_sql():
    from testDebug.settings import BASE_DIR
    import os
    sql_statement=open(os.path.join(BASE_DIR,'testmodel/sql/store.sql'),'r').read()
    return sql_statement

def delte_stores_from_sql():
    return 'DELETE from testmodel_store'


class Migration(migrations.Migration):

    dependencies = [
        ('testmodel', '0001_initial'),
    ]

    operations = [
        migrations.RunSQL(load_store_from_sql(),delte_stores_from_sql())#需要注意的是傳入的不是函式索引,而是函式返回值。
    ]

執行遷移命令後(有必要的話,可以刪除db.sqltie3檔案)

image-20201026222915557

  • fixture 檔案

在app下設fixtures資料夾,資料夾下寫入store.jsonfixture檔案(非常重要),如下:

[
    {
        "fields":{
            "city":"beijing",
            "state":"BJ",
            "name":"beijingkaoya",
            "address":"wenhuadadao"
        },
        "model":"testmodel.store",
        "pk":0
    },
    {
        "fields":{
            "city":"nanjing",
            "name":"tianshuimian",
            "state":"NJ",
            "address":"dongfenglu"
        },
        "model":"testmodel.store",
        "pk":1
    }
]

空遷移檔案

# Generated by Django 3.1 on 2020-10-26 14:51

from django.db import migrations,models
def load_stores_from_fixture(apps,schema_editor):
    from django.core.management import call_command
    call_command('loaddata','store.json')
def delete_stores(apps,schema_editor):
    Store=apps.get_model('testmodel','store')
    Store.objects.all().delete()


class Migration(migrations.Migration):

    dependencies = [
        ('testmodel', '0001_initial'),
    ]

    operations = [
        migrations.RunPython(load_stores_from_fixture,delete_stores)
    ]

執行遷移後:

image-20201026232651547

Django model 訊號

Django models有很多可以覆蓋的方法,如save,__init__等,這樣的話,就可以在裡面嵌入我們特定的邏輯就可以達到model A的某個方法可以觸發model B的另一個方法,但是這樣卻不是比較好的方法,因為model A中有了其他模型的一些資訊,不符合低耦合的原則,所以在專案變得逐漸龐大後,這樣會變得很亂,怎麼辦呢?

這樣的問題在軟體工程中非常常見,軟體工程也提供了一個非常棒的方法,那就是signal-slot,每個控制元件都往環境裡發射訊號,然後有專門的監聽器,監測到某個訊號,就呼叫與該訊號相應的slot函式,類似的,Django也借鑑了這種思路,所有的model都有自己的signal,只需要定義相應的接受函式即可,一般的模式為:

from django.dispatch import receiver
@receiver(<signal_to_listen>,sender=<model_class_to_listen>)
def method_with_logic(sender,**kwargs):
    #logic when signal is emitted
    #Access sender and kwargs to get info on model that emitted signal

需要注意的是sender不一定非得是model索引,也可以是索引的字元,這種情況下是lazy-loading模式。

內建的model signal如下表:

Signal Signal class Description
pre_init,post_init django.db.models.signal.pre_init/post_init 在model的__init__的開始與結束時發射訊號
pre_save,post_save django.db.models.signal.pre_save/post_save 在model的save開始與結束時發射訊號
pre_delete,post_delete django.db.models.signal.pre_delete/post_delete 在model的delete開始與結束時發射訊號
m2m_changed django.db.models.signal.m2m_changed 當一個多對多模型被改變發射訊號
class_prepared django.db.models.signals.class_prepared 當一個模型被定義和註冊時候發射訊號,一般是django內部機制,較少的使用於其他地方

建議專門把一個app的signal全部寫入signals.py,便於管理。

# app內的signals.py
from django.dispatch import receiver
from django.db.models.signals import pre_save,post_save
from datetime import datetime 
import logging

# logging=logging.getLogger(__name__)
logging.basicConfig(filename='testP.log',level=logging.DEBUG)
@receiver(pre_save,sender='banners.testP')
def run_before_save(sender,**kwargs):
    now=datetime.now().strftime('%Y-%m-%d %H:%m:%S')
    logging.info('new pre save'.center(30,'*'))
    logging.info('Start pre_save testP at %s' % now)
    logging.info('Sender is %s' % sender)
    logging.info('kwargs is %s' % str(kwargs))
@receiver(post_save,sender='banners.testP')
def run_after_save(sender,**kwargs):
    now=datetime.now().strftime('%Y-%m-%d %H:%m:%S')
    logging.info('new post save'.center(30,'*'))
    logging.info('Post_save testP at %s' % now)
    logging.info('Sender is %s' % sender)
    logging.info('kwargs is %s' % str(kwargs))

需要注意的是,寫完signals.py後,需要在app.py中定義ready方法來引入該signal檔案

#app.py
from django.apps import AppConfig
class BannersConfig(AppConfig):
    name = 'banners'
    def ready(self):  #定義ready方法,引入signals.py
        import banners.signals

最後還需要在app的__init__.py中定義如下:

# __init__.py
default_app_config='banners.apps.BannersConfig'
>>> from banners.models import *
>>> testP.objects.create(name='johnyang3')
<testP: johnyang3>
>>> testP.objects.create(name='johnyang4')
<testP: johnyang4>
>>> a=testP(name='johnyang5')
>>> a.save()

log檔案如下:

image-20201026155750199

定製signal

#models.py引入Signal類
from django.dispatch import Signal
testPClosed=Signal(providing_args=['employee']) #自定義signal,但必須提供providing_args,該引數為sender,receiver都使用
class testP(models.Model):
    name=models.CharField(max_length=5)
    def closing(self,employee):
        testPClosed.send(sender=self.__class__,employee=employee)
    def __str__(self):
        return self.name 
#在signal.py中加入
import logging
from .models import testPClosed
# logging=logging.getLogger(__name__)
logging.basicConfig(filename='testP.log',level=logging.DEBUG)
@receiver(testPClosed)
def run_when_testP_close(sender,**kwargs):
    now=datetime.now().strftime('%Y-%m-%d %H:%m:%S')
    logging.info('new closing'.center(30,'*'))
    logging.info('closing testP at %s' % now)
    logging.info('Sender is %s' % sender)
    logging.info('kwargs is %s' % str(kwargs))

>>> from banners.models import *
>>> a=testP(name='johnyang')
>>> a.closing(2)

image-20201027102155222

除models.py外的其他model放置方法

預設地,Django 的model 是放置在app裡的models.py。然而隨著定義的model的數量的增長,儲存在一個檔案的方法就越來越不便於管理,對此,有3種方法,將model存放在除models.py之外的位置。

Apps內的models資料夾

model可以存放在apps的models的資料夾中的檔案中,首先刪去models.py,然後在models資料夾中新增__init__使之成為一個包,並在__init__中引入models資料夾下各種.py檔案中的model,注意,不建議使用from xxx import *,這樣會導致名稱空間紊亂。

image-20201027123259552

#__init__.py
from .testModelFolder import storeFolder
#models/testModelFolder.py
from django.db import models
class storeFolder(models.Model):
    name=models.CharField(max_length=3)

image-20201027123604350

Apps內的自定義資料夾

可以在apps內建立多個資料夾,只要每個資料夾下都有一個__init__.py檔案,然後就可以在每個資料夾下任意定義.py來儲存各種model,只需要在models.py中引入進來就行from .<sub_folder>.<file> import <model_class>

image-20201027131848894

#testfolds/cutomFile.py
from django.db import models
class storeFolder(models.Model):
    name=models.CharField(max_length=3)
#models.py
from .testfolds.customFile import storeFolder

經過makemigration,migrate後,

image-20201027132043399

Apps外及model“賦值”到apps

這種方法一般不推薦。

它是將apps外任意一個.py的model的meta的app_label賦值為給定的app,然後該model就屬於該app了。

例如:

#about/models.py
from django.db import models

# Create your models here.
class testAcrossApp(models.Model):
    name=models.CharField(max_length=3)
    class Meta:
        app_label='banners'

image-20201027133913543

Django 多資料庫

settings.py中,DATABASES用來定義與app相關的資料庫。既然DATABASES是複數,也就意味著該變數可以有多個值:

#settings.py
DATABASES={
    'default':{
        ...
    },
    'devops':{
        ...
    },
    'analytics':{
        ...
    }
}

...表示資料庫連線引數(比如ENGINE,NAME)。最重要的一點就是必須要有default鍵,除非宣告瞭DEFAULT_DB_ALIAS值,那麼該值不叫'default'了,但承擔的就是預設資料庫的任務。

model的多資料庫選擇:using

using來使用非預設資料庫,有兩種用法,一種是引數選項,一種是objects的方法。

  • 比如save(using=...),delete(using=...).
  • 比如Item.objects.using('somedb').all().

多資料庫工具:--database

對於manage.py的用法中,可以加入--database,用以對特定資料庫進行操作(migrate,dumpdata等)。比如:python manage.py migrate --database devops就是隻對該資料庫執行操作。

相關文章