Django 模型層

雲崖先生發表於2020-09-19

Django ORM

   Django模型層的功能就是與資料庫打交道,其中最主要的框架為ORM

   ORM為物件關係對映,說白了就是將Python中的類對映成資料表,將表中每一條記錄對映成類的例項物件,將物件屬性對映成表中的欄位。

   如果用原生的SQL語句結合pymysql來對資料庫進行操作無疑是非常繁瑣的,但是Django提供了非常強大的ORM框架來對資料庫進行操作,在增刪改查方面都有非常大的提升,學會使用ORM十分的必要。

   注意:儘管ORM十分方便,但是也請不要過分依賴它從而忘記原生SQL命令。

   ORM作為Django中最難的一章基礎知識點,應該是很多初學者的第一道門檻。

   那麼在學習ORM之前,我想寫一些我的心得體會,ORM的操作很方便,但是有些設計比較反人類,你可以使用原生SQL進行代替。

   條條大路通羅馬,不一定非要在一棵樹上吊死。

準備工作

原生語句

   如果想在操作ORM時還能看到原生的SQL語句,請在settings.py中新增上以下程式碼

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console':{
            'level':'DEBUG',
            'class':'logging.StreamHandler',
        },
    },
    'loggers': {
        'django.db.backends': {
            'handlers': ['console'],
            'propagate': True,
            'level':'DEBUG',
        },
    }
}

測試指令碼

   Django中允許對py檔案做單獨的測試,而不用啟動Django專案,這是非常方便的。

   注意:所有測試中的程式碼都必須在if "__name__" == "__main__":下進行,這意味著你的from xx import xx不能放在頂行

from django.test import TestCase

# Create your tests here.
import os
import sys
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))  # 如果在pycharm中,這兩句可以省略

if __name__ == "__main__":
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project01.settings")

    import django
    django.setup()

    # 測試程式碼均在下面進行
    from app01 import models

連結MYSQL

   Django中預設使用的資料庫是sqlit3,所以我們需要在配置檔案中對其實行修改。

   大體分為兩個步驟

   1.修改預設連結為MySQL

   2.連結宣告,即宣告連結MySQL的模組為pymysql(預設是MySQLdb

修改連結

   開啟專案全域性資料夾下的settings.py,找到以下程式碼進行註釋

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',  # 預設連結sqlite3
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}

   現在我們就要進行手動配置了,參照如下程式碼

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',  # 預設連結sqlite3
        'NAME': 'db1', 			# 你的資料庫名稱
        'USER': 'root', 		# 你的登入使用者名稱稱
        'PASSWORD': '123',		# 你的登入密碼,如果沒有留空即可
        'HOST':	'localhost', 	# 連結地址
        'PORT': '3306',			# MySQL服務端埠號
        'CHARSET': 'utf8mb4',	# 預設字元編碼
    }
}

   現在,你的Django會丟擲一個異常,不管他,直接進入下一個步驟

django.core.exceptions.ImproperlyConfigured: Error loading MySQLdb module: No module named 'MySQLdb'.
Did you install mysqlclient or MySQL-python?

連結宣告

   由於我們要將預設連結MySQL的模組從MySQLdb修改為pymysql,所以你要先安裝pymysql模組

pip install pymysql

   安裝完成後開啟專案全域性資料夾下的__init__(實際上任意APP下的__init__都可以),新增程式碼如下:

import pymysql
pymysql.install_as_MySQLdb()

   現在我們的Django就可以使用MySQL進行連線了。

資料庫操作

   ORM為物件關係對映,類就相當於資料表,屬性就相當於欄位。

   因此我們有兩條很常見的命令用於操作:

python manage.py makemigrations  # 建立模型對映表
python manage.py migrate # 同步模型表至資料庫中

   這兩條命令在對資料庫欄位、資料表結構進行修改時都需要重新進行。

單表建立

   在專案的APP中,開啟models.py,開始建立表(該檔案下可以建立一個表,也可以建立多個表)。

   注意:ORM建立表時如果不指定主鍵欄位,將會預設建立一個名為id的主鍵欄位。並且我們在使用時可以使用pk來代指主鍵

from django.db import models

class User(models.Model): # 必須繼承
    # 自動建立 id 的主鍵
    username = models.CharField(max_length=16,verbose_name="使用者名稱",db_index=True)
    age = models.IntegerField()
    gender = models.BooleanField(choices=([0,"male"],[1,"famale"]),default=0)

   接下來要執行建立模型對映表的命令,以及執行資料庫同步命令。

python manage.py makemigrations  # 建立模型對映表
python manage.py migrate # 同步模型表至資料庫中

   當建立模型對映表命令執行完成後,會發現APP下的migrations資料夾中多一一個檔案,檔案中就存放的剛剛建立好的模型表。

   當資料庫同步命令執行完成後,MySQL中才會真正的建立出一張真實的物理表。

   模型表資訊:

# project01專案/app01/migrations

# -*- coding: utf-8 -*-
# Generated by Django 1.11.11 on 2020-09-12 18:18
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='User',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('username', models.CharField(db_index=True, max_length=16, verbose_name='使用者名稱')),
                ('age', models.IntegerField()),
                ('gender', models.BooleanField(choices=[[0, 'male'], [1, 'famale']], default=0)),
            ],
        ),
    ]

   物理表(注意:物理表的命名會以APP名稱開始):

desc app01_user;

+----------+-------------+------+-----+---------+----------------+
| Field    | Type        | Null | Key | Default | Extra          |
+----------+-------------+------+-----+---------+----------------+
| id       | int(11)     | NO   | PRI | NULL    | auto_increment |
| username | varchar(16) | NO   | MUL | NULL    |                |
| age      | int(11)     | NO   |     | NULL    |                |
| gender   | tinyint(1)  | NO   |     | NULL    |                |
+----------+-------------+------+-----+---------+----------------+

欄位操作

   所有關於欄位的增刪改查都直接操縱物件屬性即可,這裡要提一下增加欄位。

   如果需要新增一個欄位,而資料表中又有一些資料,此時Django會提醒你為原本的老資料設定一個預設值。

   它會提供給你2個選項,選項1:立即為所有老資料設定預設值。選項2:放棄本次新增欄位的操作,重新新增欄位並設定預設值(也可以設定null=True

   示例如下:我們現在表中建立一條資訊。

from app01 import models

res = models.User.objects.create(
	username= "雲崖",
	age=18,
	gender=0,
)

print(res)

   然後修改其欄位,新增一個註冊時間。

from django.db import models

class User(models.Model): # 必須繼承
    # 自動建立 id 的主鍵
    username = models.CharField(max_length=16,verbose_name="使用者名稱",db_index=True)
    age = models.IntegerField()
    gender = models.BooleanField(choices=([0,"male"],[1,"famale"]),default=0)
    register_time = models.DateField(auto_now_add=True)

   在執行修改模型對映表的結構命令時,會讓你做出選擇。此時我們選擇1,並且給定一個預設值就好。

PS D:\project\project01> python manage.py makemigrations
You are trying to add the field 'register_time' with 'auto_now_add=True' to user without a default; the database needs something to populate existing rows.

 1) Provide a one-off default now (will be set on all existing rows)  # 1.新增一個預設值
 2) Quit, and let me add a default in models.py # 2.退出,自己手動新增default引數設定預設值
Select an option: 1
Please enter the default value now, as valid Python
You can accept the default 'timezone.now' by pressing 'Enter' or you can provide another value.
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now
Type 'exit' to exit this prompt
[default: timezone.now] >>> "2020-01-28"  # 選擇新增,給定一個時間
Migrations for 'app01':
  app01\migrations\0002_user_register_time.py
    - Add field register_time to user
PS D:\project\project01>

   記得最後執行資料庫同步

python manage.py migrate

返回資訊

   其實在建立資料表時,我們都會為其加上__str__方法。

   這是為了方便查詢操作時看到那一條資料。

   如下,如果不加的話返回結果是QuerySet中套上列表+物件,十分不利於檢視資訊。

from app01 import models

res = models.User.objects.all()

print(res)  # <QuerySet [<User: User object>]>

   如果我們加上__str__方法,那麼就方便我們進行檢視,到底查詢的有那些記錄。

   (只要不對模型表本身結構做出修改,都不用重新執行模型表和物理表的兩條命令)

from django.db import models

class User(models.Model): # 必須繼承
    # 自動建立 id 的主鍵
    username = models.CharField(max_length=16,verbose_name="使用者名稱",db_index=True)
    age = models.IntegerField()
    gender = models.BooleanField(choices=([0,"male"],[1,"famale"]),default=0)
    register_time = models.DateField(auto_now_add=True)

    def __str__(self) -> str:
        return "物件-%s"%self.username 
        # 每一個記錄物件都是User的例項化物件,所以我們返回一下self的username即可

   現在再來進行查詢

from app01 import models

res = models.User.objects.all()

print(res)  # <QuerySet [<User: 物件-雲崖>]>

記錄操作

新增記錄

   新增記錄語法有兩種,均可使用。

models.表名.objects.create(col='xxx', col='xxx')  # 注意,返回值是建立好的物件記錄本身

obj = models.表名(col='xxx', col='xxx')
obj.save()

   每次的建立,都會返回一個建立成功的新物件。一個例項物件中包含欄位及欄位資料,換而言之就是一條記錄。

   示例演示:

from app01 import models

# 方式一:推薦使用
obj1 = models.User.objects.create(
    # 主鍵自動新增,AUTOFILED
    username="及時雨",
    age=21,
    gender=0,
    # register_time 建立時自動新增
)

# 方式二:
obj2 = models.User(
    # 主鍵自動新增,AUTOFILED
    username="玉麒麟",
    age=22,
    gender=0,
    # register_time 建立時自動新增
)

obj2.save()

print(obj1, obj2)  # 物件-及時雨 物件-玉麒麟

修改記錄

   修改記錄有兩種方式,推薦使用第一種,因為第二種修改會將該條記錄的所有欄位都進行修改,無論資料是否有更新。

models.表名.objects.filter(col='舊資料').update(col='新資料') 
# filter相當於where,不可以使用get,因為單一物件沒有update方法,只有QuerySet物件才有。

obj = models.表名.objects.get(col='舊資料') # get() 只獲取單個物件
obj.col = '新資料'
obj.save() 

   對於方式1而言,它可能修改多條記錄。所以每次修改後,都會返回一個int型別的數字,這代表受到影響的行數

   示例演示:

from app01 import models

# 方式一:推薦使用
num1 = models.User.objects.filter(pk=1).update(username="宋公明")

# 方式二:
obj = models.User.objects.get(pk=2)
obj.username="盧俊義"
obj.save()

print(num1)  # 1

刪除記錄

   語法介紹:

models.表名.objects.filter(col='xxx').delete()
# 刪除指定條件的資料 filter相當於where,不可以使用get,因為單一物件沒有delete方法,只有QuerySet物件才有。

   返回值是一個元組,包含刪除成功的行數。

   示例如下

from app01 import models

num_obj = models.User.objects.filter(pk=2).delete()

print(num_obj)  # (1, {'app01.User': 1})

批量增加

   如果要插入的資料量很大,則可以使用bulk_create來進行批量插入。

   它比單純的create效率更高。

book_list = []
for i in range(100000):
	book_obj = models.Book(title="第%s本書"%i)
	book_list.append(book_obj)
models.Book.objects.bulk_create(book_list)

單表查詢

truncate app01_user;

INSERT INTO app01_user(username, age, gender,register_time) VALUES
    ("雲崖",18,0,now()),
    ("及時雨",21,0,now()),
    ("玉麒麟",22,0,now()),
    ("智多星",21,0,now()),
    ("入雲龍",23,0,now()),
    ("大刀",22,0,now());

QuerySET

   在查詢時候,一定要區分開QuerySet物件與單條記錄物件的區別。

   比如:我們的單條記錄物件中儲存有欄位名等資料,均可進行.出來。它的格式應該是這樣的:obj[col1,col2,col3]

   而QuerySet物件是一個物件集合體,中間包含很多單條記錄物件,它的格式是:QuerySet<[obj1,obj2,obj3]>

   不同的物件有不同的方法,單條記錄物件中能.出欄位,而QuerySet物件中能點出filter()/values()等方法。所以一定要區分開。

   QuerySet集合物件:暫時可以理解為一個列表,裡面可以包含很多記錄物件,但是不支援負向的索引取值,並且Django不太希望你使用index進行取值,而應該使用first()以及last()進行取出單條記錄物件。

基本查詢

   語法介紹:

models.表名.objects.all() # 拿到當前模型類對映表中的所有記錄,返回QuerySet集合物件
models.表名.objects.filter(col='xxx')	# 相當於whlie條件過濾,可拿到多條,返回QuerySet集合物件
models.表名.objects.get(col='xxx')	# 返回單個記錄物件。注意區分與QuerySet的區別

   示例演示:(仔細看結果,區分QuerySet與單個物件的區別)

from app01 import models

all_queryset = models.User.objects.all()

filter_queryset = models.User.objects.filter(pk__gt=3)

obj = models.User.objects.get(pk=1)

print(all_queryset)
# <QuerySet [<User: 物件-雲崖>, <User: 物件-及時雨>, <User: 物件-玉麒麟>, <User: 物件-智多星>, <User: 物件-入雲龍>, <User: 物件-大刀>]>
print(filter_queryset)
# <QuerySet [<User: 物件-智多星>, <User: 物件-入雲龍>, <User: 物件-大刀>]>
print(obj)
# 物件-雲崖

filter過濾

   filter()相當於where條件,那麼在其中可以進行過濾操作

   過濾使用__雙下劃線進行操作。

   注意:在filter()中,所有的條件都是AND關係

條件說明
filter(col1=xxx,col2=xxx) 查詢符合多個條件的記錄
col__lt = xxx 欄位資料小於xxx的記錄
col__lte = xxx 欄位資料小於等於xxx的記錄
col_gt = xxx 欄位資料大於xxx的記錄
col_gte = xxx 欄位資料大於xxx的記錄
col_in = [x,y,z] 欄位資料在[x,y,z]中的記錄
col_range = [1,5] 欄位資料在1-5範圍內的記錄
col_startswith = "xxx" 欄位資料以"xxx"開頭的記錄,區分大小寫
col_istartswith = "xxx" 欄位資料以"xxx"開頭的記錄,不區分大小寫
col_endswith = "xxx" 欄位資料以"xxx"結尾的記錄,區分大小寫
col_iendswith = "xxx" 欄位資料以"xxx"結尾的記錄,不區分大小寫
col__contains = ”xxx" 欄位資料包含"xxx"的記錄,區分大小寫
col__icontains = ”xxx" 欄位資料包含"xxx"的記錄,不區分大小寫
col__year = 2020 日期欄位在2020年中的記錄
col__year__lt = 2020 日期欄位在2020年之後的記錄
col__date__gte = datetime.date(2017, 1, 1) 日期欄位在2017年1月1日之後的記錄
col__month = 3 日期欄位在3月的記錄
col__day = 3 日期欄位在3天的記錄
col__hour = 3 日期欄位在3小時的記錄
col__minute = 3 日期欄位在3分鐘的記錄
col__second = 3 日期欄位在3秒鐘的記錄
User.objects.filter(pk__lt=2)  # 查詢id小於2的物件

User.objects.filter(pk__lte=2) # 查詢id小於等於2的物件

User.objects.filter(pk__gt=2)  # 查詢id大於2的物件

User.objects.filter(pk__gte=2) # 查詢id大於等於2的物件

User.objects.filter(pk__in=[1,2,5]) # 查詢id為1,2,5的物件。

User.objects.filter(pk__range=[1,5])  # 查詢id為1-5之間的物件。(左右都為閉區間)

User.objects.filter(name__contains="shawn")  # 查詢name中包含shawn的物件。類似於正則/%shawn%/

User.objects.filter(name__icontains="shawn")  # 查詢name中包含shawn的物件。(忽略大小寫)

User.objects.filter(name__startswith="shawn")  # 查詢name以shawn開頭的物件 

User.objects.filter(name__istartswith="shawn")  # 查詢name以shawn開頭的物件(忽略大小寫)

User.objects.filter(name__endswith="shawn")  # 查詢name以shawn結尾的物件 

User.objects.filter(name__iendswith="shawn")  # 查詢name以shawn結尾的物件 (忽略大小寫)

User.objects.exclude(name__contains="shawn")  # 查詢name不包含shawn的所有物件。
xx__date型別欄位可以根據年月日進行過濾

User.objects.filter(birthday__year=2012)   # 查詢出生在2012年的物件

User.objects.filter(birthday__year__gte=2012)  # 查詢出生在2012年之後物件

User.objects.filter(birthday__date__gte=datetime.date(2017, 1, 1))  # 查詢在2017,1,1之後出生的物件

User.objects.filter(birthday__month=3)  查詢出生在3月份的物件

User.objects.filter(birthday__week_day=3) 

User.objects.filter(birthday__day=3)

User.objects.filter(birthday__hour=3)

User.objects.filter(birthday__minute=3)

User.objects.filter(birthday__second=3)

常用操作

   下面將例舉一些常用操作。

單詞查詢操作描述
all() models.表名.objects.all() <QuerySet [obj obj obj]>,相當於查詢該表所有記錄。返回值相當於列表套物件
filter() models.表名.objects.filter(col=xxx) <QuerySet [obj obj objJ]>,過濾條件可通過逗號分割,等同於where查詢。返回值相當於列表套物件
get() models.表名.objects.get(col=xxx) obj,單條記錄,即一個類的例項化物件,直接返回物件
以下方法多為配合第一個或第二個方法使用    
values() models.表名.objects.values("col") <QerySet [{col,x1},{col,x2}]>,返回記錄中所有欄位的值。返回值相當於列表套字典
values_list() models.表名.objects.values_list("col") <QerySet [(x1),(x2)]>,返回記錄中所有欄位的值。返回值相當於列表套元組
distinct() models.表名.objects.values("col").distinct() <QuerySet [{col,x1},{col,x2}]>,對指定欄位去重。返回記錄中所有欄位的值。返回值相當於列表套字典
order_by() models.類名.objects.order_by("-col") <QuerySet [obj obj objJ]>,對指定欄位排序,如直接寫col是升序,-col則是降序。
reverse() models.類名.objects.order_by("-col").reverse() <QuerySet [obj obj obj]>,前置條件必須有order_by(col),對其進行反轉操作
exclude() models.表名.objects.exclude(col="xxx") <QuerySet [obj obj obj]>,除開col=xxx的記錄,其他記錄都拿
count() models.表名.objects.values("col").count() int,返回該欄位記錄的個數
exists() models.表名.objects.filter(col=“data”).exists() bool,返回該記錄是否存在
first() models.表名.objects.first() obj,單條記錄,取QuerySet中第一條記錄
last() models.表名.objects.last() obj,單條記錄,取QuerySet中最後一條記錄
注意:獲取all()後想配合其他的一些方法,可直接使用models.型別.object.其他方法()。這等同於all()    

   示例演示如下:

、
from app01 import models

values_queryset_dict = models.User.objects.values("username")
print(values_queryset_dict)
# <QuerySet [{'username': '雲崖'}, {'username': '入雲龍'}, {'username': '及時雨'}, {'username': '大刀'}, {'username': '智多星'}, {'username': '玉麒麟'}]>

values_list_queryset_tuple = models.User.objects.values_list("username")
print(values_list_queryset_tuple)
# <QuerySet [('雲崖',), ('入雲龍',), ('及時雨',), ('大刀',), ('智多星',), ('玉麒麟',)]>

distict_queryset = models.User.objects.values("age").distinct()
print(distict_queryset)
# <QuerySet [{'age': 18}, {'age': 21}, {'age': 22}, {'age': 23}]>

order_by_queryset = models.User.objects.order_by("-age")
print(order_by_queryset)
# <QuerySet [<User: 物件-入雲龍>, <User: 物件-玉麒麟>, <User: 物件-大刀>, <User: 物件-及時雨>, <User: 物件-智多星>, <User: 物件-雲崖>]>

reverse_queryset = models.User.objects.order_by("-age").reverse()
print(reverse_queryset)
# <QuerySet [<User: 物件-雲崖>, <User: 物件-及時雨>, <User: 物件-智多星>, <User: 物件-玉麒麟>, <User: 物件-大刀>, <User: 物件-入雲龍>]>

exclude_queryset = models.User.objects.exclude(pk__gt=3)
print(exclude_queryset)
# <QuerySet [<User: 物件-雲崖>, <User: 物件-及時雨>, <User: 物件-玉麒麟>]>

num_pk = models.User.objects.values("pk").count()
print(num_pk)
# 6

bool_res = models.User.objects.filter(username="雲崖").exists()
print(bool_res)
# True

first_obj = models.User.objects.first()
print(first_obj)
# 物件-雲崖

last_obj = models.User.objects.last()
print(last_obj)
# 物件-大刀

多表關係

   Django中如果要實現多表查詢,必須要建立外來鍵,因為Django的多表查詢是建立在外來鍵關係基礎之上的。

   但是如果使用原生的SQL命令時我並不推薦使用外來鍵做關聯,因為這樣會使得表與表之間的耦合度增加。

   值得一提的是Django中多對多關係建立則只需要兩張表即可,因為它會自動建立第三張表(當然也可以手動建立,有兩種手動建立的方式,下面也會進行介紹)。

一對一

   作者與作者詳情是一對一

   關鍵字:OneToOneField

   注意事項如下:

   1.如不指定關聯欄位,自動關聯主鍵id欄位

   2.關聯過後從表的關聯欄位會自動加上 _id字尾

   3.一對一關係建立在任何一方均可,但是建議建立在查詢使用多的一方,如作者一方

class Author(models.Model):
    name = models.CharField(max_length=16, default="0")
    age = models.IntegerField()
    # 作者與作者詳情一對一
    author_detail = models.OneToOneField(
        to="Authordetail", on_delete=models.CASCADE)

    def __str__(self) -> str:
        return "物件-%s" % self.name


class Authordetail(models.Model):
    phone = models.BigIntegerField()
    # 手機號,用BigIntegerField,

    def __str__(self) -> str:
        return "物件-%s" % self.phone

   檢視一對一關係(注意,外來鍵上加了一個UNIQUE約束):

desc app01_author;

+------------------+-------------+------+-----+---------+----------------+
| Field            | Type        | Null | Key | Default | Extra          |
+------------------+-------------+------+-----+---------+----------------+
| id               | int(11)     | NO   | PRI | NULL    | auto_increment |
| age              | int(11)     | NO   |     | NULL    |                |
| author_detail_id | int(11)     | NO   | UNI | NULL    |                |
| name             | varchar(16) | NO   |     | NULL    |                |
+------------------+-------------+------+-----+---------+----------------+

一對多

   書籍與出版社是一對多

   關鍵字:ForeignKey

   注意事項如下:

   1.如不指定關聯欄位,自動關聯主鍵id欄位

   2.關聯過後從表的關聯欄位會自動加上 _id字尾

   3.一對多關係的fk應該建立在多的一方

class Publish(models.Model):
    name = models.CharField(max_length=16)
    addr = models.CharField(max_length=64)
    email = models.EmailField()

    def __str__(self) -> str:
        return "物件-%s" % self.name


class Book(models.Model):
    title = models.CharField(max_length=16)
    price = models.DecimalField(max_digits=5, decimal_places=2)
    publish_date = models.DateField(auto_now=False, auto_now_add=True)
    publish = models.ForeignKey(to="Publish", on_delete=models.CASCADE)
    
    def __str__(self) -> str:
        return "物件-%s" % self.title

   檢視一對多關係:

decs app01_book;

+--------------+--------------+------+-----+---------+----------------+
| Field        | Type         | Null | Key | Default | Extra          |
+--------------+--------------+------+-----+---------+----------------+
| id           | int(11)      | NO   | PRI | NULL    | auto_increment |
| title        | varchar(16)  | NO   |     | NULL    |                |
| price        | decimal(5,2) | NO   |     | NULL    |                |
| publish_date | date         | NO   |     | NULL    |                |
| publish_id   | int(11)      | NO   | MUL | NULL    |                |
+--------------+--------------+------+-----+---------+----------------+

多對多

   書籍與作者是多對多

   關鍵字:ManyToManyFiled

   1.Django使用ManyToManyFiled會自動建立第三張表,而該欄位就相當於指向第三張表,並且第三張表預設關聯其他兩張表的pk欄位

   2.多對多關係建立在任何一方均可,但是建議建立在查詢使用多的一方

   3.多對多沒有級聯操作

class Book(models.Model):
    title = models.CharField(max_length=16)
    price = models.DecimalField(max_digits=5, decimal_places=2)
    publish_date = models.DateField(auto_now=False, auto_now_add=True)
    publish = models.ForeignKey(to="Publish", on_delete=models.CASCADE)
    # 書籍和出版社是一對多/多對一
    authors = models.ManyToManyField("Author")
    # 書籍與作者是多對多關係

    def __str__(self) -> str:
        return "物件-%s" % self.title

   檢視多對多關係,即第三張表:

desc app01_book_authors;

+-----------+---------+------+-----+---------+----------------+
| Field     | Type    | Null | Key | Default | Extra          |
+-----------+---------+------+-----+---------+----------------+
| id        | int(11) | NO   | PRI | NULL    | auto_increment |
| book_id   | int(11) | NO   | MUL | NULL    |                |
| author_id | int(11) | NO   | MUL | NULL    |                |
+-----------+---------+------+-----+---------+----------------+

自關聯

   自關聯是一個很特殊的關係,如評論表。

   一個文章下可以有很多評論,這些評論又分為根評論和子評論,那麼這個時候就可以使用自關聯建立關係。

#評論表
class Comment(models.Model):
    #評論的內容欄位
    content=models.CharField(max_length=255)
    #評論的釋出時間
    push_time=models.DateTimeField(auto_now_add=True)
    #關聯父評論的id,可以為空,一定要設定null=True
    pcomment = models.ForeignKey(to='self',null=True) 
    def __str__(self):
        return self.content

級聯操作

   一對一,一對多關係均可使用級聯操作。

   在Django中,預設會開啟級聯更新,我們可自行指定級聯刪除on_delete的操作方式。

級聯刪除選項描述
models.CASCADE 刪除主表資料時,從表關聯資料也將進行刪除
models.SET 刪除主表資料時,與之關聯的值設定為指定值,設定:models.SET(值),或者執行一個可執行物件(函式)。
models.SET_NUL 刪除主表資料時,從表與之關聯的值設定為null(前提FK欄位需要設定為可空)
models.SET_DEFAULT 刪除主表資料時,從表與之關聯的值設定為預設值(前提FK欄位需要設定預設值)
models.DO_NOTHING 刪除主表資料時,如果從表中有與之關聯的資料,引發錯誤IntegrityError
models.PROTECT 刪除主表資料時,如果從表中有與之關聯的資料,引發錯誤ProtectedError

   操作演示:

 publish = models.ForeignKey(to="Publish",on_delete=models.CASCADE)
    # 書籍和出版社是一對多/多對一

全部程式碼

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=16,default="0")
    age = models.IntegerField()
    # 作者與作者詳情一對一
    author_detail = models.OneToOneField(to="Authordetail",on_delete=models.CASCADE)

    def __str__(self) -> str:
        return "物件-%s" % self.name


class Authordetail(models.Model):
    phone = models.BigIntegerField()
    # 手機號,用BigIntegerField,

    def __str__(self) -> str:
        return "物件-%s" % self.phone


class Publish(models.Model):
    name = models.CharField(max_length=16)
    addr = models.CharField(max_length=64)
    email = models.EmailField()

    def __str__(self) -> str:
        return "物件-%s" % self.name


class Book(models.Model):
    title = models.CharField(max_length=16)
    price = models.DecimalField(max_digits=5, decimal_places=2)
    publish_date = models.DateField(auto_now=False, auto_now_add=True)
    publish = models.ForeignKey(to="Publish",on_delete=models.CASCADE)
    # 書籍和出版社是一對多/多對一
    authors = models.ManyToManyField("Author")
    # 書籍與作者是多對多關係

    def __str__(self) -> str:
        return "物件-%s" % self.title

多表操作

一對多

新增記錄

   對於增加操作而言有兩種

   1.使用fk_id進行增加,新增另一張表的id號(因為外來鍵會自動新增_id字首,所以我們可以直接使用)

   2.使用fk進行增加,新增另一張表的具體記錄物件

第一種:
	models.表名.objects.create(
		col = xxx,
		col = xxx,
		外來鍵_id = int,
	)
第二種:
	# 先獲取物件
	obj = models.表名.objects.get(col=xxx)
	models.表名.objects.create(
		col = xxx,
		col = xxx,
		外來鍵 = obj,
	)

   示例操作:

from app01 import models

# 新增出版社
models.Publish.objects.create(
    # id自動為1
    name="北京出版社",
    addr="北京市海淀區",
    email="BeiJingPublish@gmail.com",
)

models.Publish.objects.create(
    # id自動為2
    name="上海出版社",
    addr="上海市蒲東區",
    email="ShangHaiPublish@gmail.com",
)

# 第一種:直接使用fk_id,新增id號
models.Book.objects.create(
    title="Django入門",
    price=99.50,
    # auto_now_add,不用填
    publish_id=1,  # 填入出版設id號
)

# 第二種,使用fk,新增物件
# 2.1 先獲取出版社物件
pub_obj = models.Publish.objects.get(pk=1)
    # 2.2 新增物件
    models.Book.objects.create(
    title="CSS攻略",
    price=81.50,
    # auto_now_add,不用填
    publish=pub_obj, # 填入出版社物件
) 

修改記錄

   修改記錄和增加記錄一樣,都是有兩種

   1.使用fk_id進行修改,改為另一張表的id號(因為外來鍵會自動新增_id字首,所以我們可以直接使用)

   2.使用fk進行修改,改為另一張表的具體記錄物件

第一種:
	models.表名.objects.filter(col=xxx).update(外來鍵_id=int)
第二種:
	# 先獲取物件
	obj = models.表名.objects.get(col=xxx)
	models.表名.objects.filter(col=xxx).update(外來鍵=obj)

   示例操作:

from app01 import models

# 第一種,直接使用fk_id  # 將第一本書改為上海出版社 # 注意,這裡只能使用filter,因為QuerySet物件才具有update方法
models.Book.objects.filter(pk=1).update(publish_id=2,)

# 第二種,獲取出版設物件,使用fk進行修改 # 將第二本書改為北京出版社
# 2.1獲取出版設物件
pub_obj = models.Publish.objects.get(pk=2)
# 2.2使用fk進行修改
models.Book.objects.filter(pk=2).update(publish=pub_obj)  #  注意,這裡只能使用filter,因為QuerySet物件才具有update方法

刪除記錄

   如果刪除一個出版社物件,與其關聯的所有書籍都將會被級聯刪除。

models.Publish.objects.filter(pk=1).delete()

多對多

   多對多的外來鍵,相當於第三張表,必須要拿到第三張表才能進行操作。

   這一點只要能理解,那麼對多對多的操作就會十分便捷。

增加記錄

   增加記錄也是兩種操作,拿到第三張表後可以增加物件,也可以增加id

   注意:可以一次新增一個物件或fk_id,也可以新增多個

第一種:
   obj = models.表名.objects.get(col=xxx)
   obj.外來鍵.add(id_1, id_2) # 可以增加一個,也可以增加多個
第二種:
	# 先獲取物件
	obj1 = models.表名.objects.get(col=xxx)
	obj2 = models.表名.objects.get(col=xxx)
	# 拿到第三張表
	obj = models.表名.objects.get(col=xxx)
	obj.外來鍵.add(obj1, obj2) # 可以增加一個,也可以增加多個

# obj.外來鍵 就是第三張表

   示例操作:

from app01 import models

# 建立作者詳細
a1 = models.Authordetail.objects.create(
	phone=12345678911
)

a2 = models.Authordetail.objects.create(
	phone=15196317676
)

# 建立作者
models.Author.objects.create(
    name="雲崖",
    age=18,
    author_detail=a1,  # a1是建立的記錄物件本身
)

models.Author.objects.create(
    name="傑克",
    age=18,
    author_detail=a2,  # a2是建立的記錄物件本身
)

# 為書籍新增作者
# 方式1 先拿到具體物件,通過外來鍵欄位拿到第三張表,新增作者的id
# 拿到書籍,通過書籍外來鍵欄位拿到第三張表(必須是具體物件,不是QuerySet)
book_obj = models.Book.objects.get(pk=1)
book_obj.authors.add(1, 2)  # 為第一部書新增兩個作者

# 方式2 先拿到作者的物件,再拿到第三張表,新增作者物件
author_1 = models.Author.objects.get(name="雲崖")
author_2 = models.Author.objects.get(name="傑克")

# 拿到書籍,通過書籍外來鍵欄位拿到第三張表(必須是具體物件,不是QuerySet)
book_obj = models.Book.objects.get(pk=2)
book_obj.authors.add(author_1, author_2)  # 為第一部書新增兩個作者

修改記錄

   修改記錄也是拿到第三張表進行修改,可以修改物件,也可以修改id

   注意:可以一次設定一個物件或id,也可以新增多個,但是要使用[]進行包裹

第一種:
    obj = models.表名.objects.get(col=xxx)
    book_obj.authors.set([1]) # 注意[]包裹,可以設定一個,也可以設定多個
第二種:
	# 先獲取物件
	obj1 = models.表名.objects.get(col=xxx)
	obj2 = models.表名.objects.get(col=xxx)
	# 拿到第三張表
	obj = models.表名.objects.get(col=xxx)
	obj.表名.set([obj1, obj2]) # 注意[]包裹,可以設定一個,也可以設定多個
	
# obj.外來鍵 就是第三張表

   示例操作:

from app01 import models

# 用id進行設定
book_obj = models.Book.objects.get(pk=1)
book_obj.authors.set([1])  # 拿到第三張表,放入作者的id進行設定。注意[]包裹,可以設定一個,也可以設定多個

# 用物件進行設定
author_obj = models.Author.objects.first()
# 先獲取作者物件
book_obj = models.Book.objects.get(pk=2)
book_obj.authors.set([author_obj])  # 拿出第三張表,放入作者物件,注意[]包裹,可以設定一個,也可以設定多個

刪除記錄

   直接拿到第三張表進行刪除即可。可以刪除物件,也可以刪除id

   注意:可以一次性刪除單個,也可以一次性刪除多個

第一種:
	obj = models.表名.objects.get(col=xxx)
    obj.外來鍵.remove(id_1,id_2) # 可以刪除一個,也可以設定多個
第二種:
	# 先獲取物件
	obj1 = models.表名.objects.get(col=xxx)
	obj2 = models.表名.objects.get(col=xxx)
	# 拿到第三張表
	obj = models.表名.objects.get(col=xxx)
	obj.外來鍵.remove(obj1, obj2) # 可以刪除一個,也可以設定多個
	
# obj.外來鍵 就是第三張表

   示例操作:

from app01 import models

# 用id進行刪除
book_obj = models.Book.objects.get(pk=1)
book_obj.authors.remove(1) # 拿到第三張表,放入作者的id進行刪除。可以刪除一個,也可以刪除多個

# 用物件進行刪除
author_obj = models.Author.objects.last()
# 先獲取作者物件
book_obj = models.Book.objects.get(pk=2)
book_obj.authors.remove(author_obj) # 拿出第三張表,放入作者物件,可以刪除一個,也可以刪除多個

清空記錄

   拿到第三張表,執行clear()則是清空操作。

book_obj = models.Book.objects.get(pk=1) # 拿到具體物件
book_obj.authors.clear() # 刪除其所有作者

多表查詢

資料準備

   還是用多表操作中的結構,資料有一些不同。

   書籍與出版社是一對多

   書籍與作者是多對多

   作者與作者詳情是一對一

select * from app01_publish;  -- 出版社

+----+-----------------+-----------------------------+--------------------+
| id | name            | addr                        | email              |
+----+-----------------+-----------------------------+--------------------+
|  1 | 北京出版社      | 北京市海淀區                | BeiJing@gmail.com  |
|  2 | 上海出版社      | 上海浦東區                  | ShangHai@gmail.com |
|  3 | 西藏出版社      | 烏魯木齊其其格樂區          | XiZang@gmail.com   |
+----+-----------------+-----------------------------+--------------------+

select * from app01_author;  -- 作者

+----+--------------+-----+------------------+
| id | name         | age | author_detail_id |
+----+--------------+-----+------------------+
|  1 | 雲崖         |  18 |                1 |
|  2 | 浪裡白條     |  17 |                2 |
|  3 | 及時雨       |  21 |                3 |
|  4 | 玉麒麟       |  22 |                4 |
|  5 | 入雲龍       |  21 |                5 |
+----+--------------+-----+------------------+

select * from app01_book;  -- 書籍

+----+-------------------+--------+--------------+------------+
| id | title             | price  | publish_date | publish_id |
+----+-------------------+--------+--------------+------------+
|  1 | Django精講        |  99.23 | 2020-09-11   |          1 |
|  2 | HTML入門          |  78.34 | 2020-09-07   |          2 |
|  3 | CSS入門           | 128.00 | 2020-07-15   |          3 |
|  4 | Js精講            | 152.00 | 2020-06-09   |          1 |
|  5 | Python入門        |  18.00 | 2020-08-12   |          1 |
|  6 | PHP全套           |  73.53 | 2020-09-15   |          2 |
|  7 | GO入門到精通      | 166.00 | 2020-07-14   |          3 |
|  8 | JAVA入門          | 123.00 | 2020-04-22   |          2 |
|  9 | C語言入門         |  19.00 | 2020-02-03   |          3 |
| 10 | FLASK原始碼分析     | 225.00 | 2019-11-06   |          1 |
+----+-------------------+--------+--------------+------------+


select * from app01_book_authors;  -- 作者書籍關係

+----+---------+-----------+
| id | book_id | author_id |
+----+---------+-----------+
|  1 |       1 |         1 |
|  2 |       2 |         1 |
|  3 |       2 |         2 |
|  4 |       2 |         3 |
|  5 |       2 |         5 |
|  6 |       3 |         4 |
|  7 |       3 |         5 |
|  8 |       4 |         2 |
|  9 |       5 |         2 |
| 10 |       6 |         2 |
| 11 |       7 |         1 |
| 12 |       7 |         2 |
| 13 |       7 |         3 |
| 14 |       8 |         4 |
| 15 |       9 |         1 |
| 16 |       9 |         5 |
| 17 |      10 |         5 |
+----+---------+-----------+

select * from app01_authordetail;  -- 作者詳情

+----+-------------+
| id | phone       |
+----+-------------+
|  1 | 15163726474 |
|  2 | 17738234753 |
|  3 | 15327787382 |
|  4 | 13981080124 |
|  5 | 13273837482 |
+----+-------------+

正反向

   正反向的概念就是看外來鍵建立在那張表之上。

   book(fk) ---> publish : 正向 publish ---> book(fk) : 反向

   切記一句話:

   正向查詢用外來鍵

   反向查詢用 表名_set

物件跨表

   使用物件跨表語法如下:

正向:
	book_obj = models.書籍.objects.get(col=xxx)
	publish_obj = book_obj.fk_出版社		 # 這裡publish_obj就是與book_obj相關的出版社了
	publish_obj.all()/.filter(col=xxx)/.get(col_xxx)  
反向:
	publish_obj = models.出版社.objects.get(col=xxx)
	book_obj = publish_obj.book_set		   # 這裡book_obj就是與publish_obj相關的書籍了
	book_obj.all()/.filter(col=xxx)/.get(col_xxx)

   查詢id為1的出版社出版的所有書籍,反向

from app01 import models

publish = models.Publish.objects.get(pk=1)
book_queryset = publish.book_set
res = book_queryset.all()
print(res)

# <QuerySet [<Book: 物件-Django精講>, <Book: 物件-Js精講>, <Book: 物件-Python入門>, <Book: 物件-FLASK原始碼分析>]>

   查詢id為2的書籍作者,正向

from app01 import models

book = models.Book.objects.get(pk=2)
authors_queryset = book.authors
res = authors_queryset.all()
print(res)

雙下跨表

   雙下劃線也可以進行正向或者反向的跨表,並且支援跨多張表。

   注意:雙下劃線能在filter()中使用,也能在values()中使用。拿物件用filter(),拿單一欄位用values()

   查詢id為1的出版社出版的所有書籍,反向,拿物件

from app01 import models

res_queryset = models.Book.objects.filter(publish__id=1)
print(res_queryset)

# <QuerySet [<Book: 物件-Django精講>, <Book: 物件-Js精講>, <Book: 物件-Python入門>, <Book: 物件-FLASK原始碼分析>]>

   查詢id為2的書籍作者姓名,正向,拿欄位

from app01 import models

res_queryset = models.Book.objects.filter(id=2).values("authors__name")
print(res_queryset)

# <QuerySet [{'authors__name': '雲崖'}, {'authors__name': '浪裡白條'}, {'authors__name': '及時雨'}, {'authors__name': '入雲龍'}]>

   查詢入雲龍出的所有書籍,正向,拿物件

from app01 import models

res_queryset = models.Book.objects.filter(authors__name="入雲龍")
print(res_queryset)

# <QuerySet [<Book: 物件-HTML入門>, <Book: 物件-CSS入門>, <Book: 物件-C語言入門>, <Book: 物件-FLASK原始碼分析>]>

   查詢入雲龍出的所有書籍,拿物件,反向查(這個需要配合物件跨表才行)

from app01 import models

res_queryset = models.Author.objects.filter(name="入雲龍").first().book_set.all()
print(res_queryset)

# <QuerySet [<Book: 物件-HTML入門>, <Book: 物件-CSS入門>, <Book: 物件-C語言入門>, <Book: 物件-FLASK原始碼分析>]>

   跨多表,從pk為1的出版社中查到其所有作者的手機號,拿欄位。

from app01 import models

res_queryset = models.Publish.objects.filter(pk=1).values("book__authors__author_detail__phone")
print(res_queryset)

# <QuerySet [{'book__authors__author_detail__phone': 15163726474}, {'book__authors__author_detail__phone': 17738234753}, {'book__authors__author_detail__phone': 17738234753}, {'book__authors__author_detail__phone': 13273837482}]>

聚合查詢

聚合函式

   使用聚合函式前應進行匯入,在ORM中,單獨使用聚合查詢的關鍵字是aggregate()

from django.db.models import Min,Max,Avg,Count,Sum

   聚合函式應該配合分組一起使用,而不是單獨使用。

   單獨使用一般都是對單表進行操作。

   注意:單獨使用聚合函式後,返回的並非一個QuerySet物件,這代表filter()/values()等方法將無法繼續使用。

   查詢書籍的平均價格:

from app01 import models

from django.db.models import Max,Min,Avg,Count,Sum
res = models.Book.objects.aggregate(Avg("price")) # 如不進行分組,則整張表為一組
print(res) # {'price__avg': 108.21}  # 單獨使用聚合,返回的是字典

   查詢最貴的書籍,以及最便宜的書籍。

from app01 import models

from django.db.models import Max, Min, Avg, Count, Sum
res = models.Book.objects.aggregate(Max("price"),Min("price"))
print(res)  # {'price__max': Decimal('225.00'), 'price__min': Decimal('18.00')}

分組查詢

   在ORM中,分組查詢使用關鍵字annotate()

   切記以下幾點:

   1.只要對錶使用了annotate(),則按照pk進行分組

   2.如果分組前指定values(),則按照該欄位進行分組,如values("name").annotate()

   3.分組函式中可以使用聚合函式,並且不同於使用aggregate()後的返回物件是一個字典,使用分組函式進行聚合查詢後,返回依舊是一個QuerySet

   統計每本書的作者個數

from app01 import models

from django.db.models import Max, Min, Avg, Count, Sum

# authors_num 別名
res = models.Book.objects.annotate(authors_num=Count("authors__pk"),).values('title','authors_num')
print(res)  
# <QuerySet [{'title': 'Django精講', 'authors_num': 1}, {'title': 'HTML入門', 'authors_num': 4}, {'title': 'CSS入門', 'authors_num': 2}, {'title': 'Js精講', 'authors_num': 1}, {'title': 'Python入門', 'authors_num': 1}, {'title': 'PHP全套', 'authors_num': 1}, {'title': 'GO入門到精通', 'authors_num': 3}, {'title': 'JAVA入門', 'authors_num': 1}, {'title': 'C語言入門', 'authors_num': 2}, {'title': 'FLASK原始碼分析', 'authors_num': 1}]>

   統計每個出版社中賣的最便宜的書籍

from app01 import models

from django.db.models import Max, Min, Avg, Count, Sum

# book_name 別名
res = models.Publish.objects.annotate(book_name=Min("book__price"),).values('name','book_name')
print(res)  

# <QuerySet [{'name': '北京出版社', 'book_name': Decimal('18.00')}, {'name': '上海出版社', 'book_name': Decimal('73.53')}, {'name': '西藏出版社', 'book_name': Decimal('19.00')}]>

   統計不止一個作者的圖書

from app01 import models

from django.db.models import Max, Min, Avg, Count, Sum

# authors_num 別名   只要返回的QuerySet物件,就可以使用filter
res = models.Book.objects.annotate(authors_num=Count("authors__pk"),).values('title','authors_num').filter(authors_num__gt=1)
print(res)  

# <QuerySet [{'title': 'HTML入門', 'authors_num': 4}, {'title': 'CSS入門', 'authors_num': 2}, {'title': 'GO入門到精通', 'authors_num': 3}, {'title': 'C語言入門', 'authors_num': 2}]>

   查詢每個作者出書的總價

from app01 import models

from django.db.models import Max, Min, Avg, Count, Sum

# sum_price 別名
res = models.Author.objects.annotate(sum_price=Sum("book__price"),).values('name','sum_price')
print(res)

# <QuerySet [{'name': '雲崖', 'sum_price': Decimal('362.57')}, {'name': '浪裡白條', 'sum_price': Decimal('487.87')}, {'name': '及時雨', 'sum_price': Decimal('244.34')}, {'name': '玉麒麟', 'sum_price': Decimal('251.00')}, {'name': '入雲龍', 'sum_price': Decimal('450.34')}]>

F&Q過濾

F查詢

   F查詢能夠拿到欄位的資料。使用前應該先進行匯入

form django.db.moduls import F

   注意:如果要操縱的欄位是字元型別,要對其進行拼接操作還需匯入Concat進行拼接

   將所有書籍的價格提升50元

from app01 import models

from django.db.models import Max, Min, Avg, Count, Sum
from django.db.models import F, Q

res = models.Book.objects.update(price=F("price")+50)
print(res)  # 10

   在每本書名字後面加上爆款二字(注意:操縱字元型別,需要用到ConcatValue函式)

from app01 import models

from django.db.models import Max, Min, Avg, Count, Sum
from django.db.models import F, Q
# 必須配合下面的兩個進行使用
from django.db.models.functions import Concat
from django.db.models import Value

res = models.Book.objects.update(title=Concat(F("title"), Value("爆款")))
print(res)  # 10

Q查詢

   在filter()中,只要多欄位篩選中用逗號進行分割都是AND連結,如何進行ORNOT呢?此時就要用到Q,還是要先進行匯入該功能。

form django.db.moduls import Q
符號描述
filter(Q(col=xxx),Q(col=xxx)) AND關係,只要在filter()中用逗號分割,就是AND關係
filter(Q(col=xxx)!Q(col=xxx)) OR關係,用|號進行連結
filter(~Q(col=xxx)!Q(col=xxx)) NO關係,用~號進行連線

   查詢書籍表中入雲龍或雲崖出版的書籍

from app01 import models

from django.db.models import Max, Min, Avg, Count, Sum
from django.db.models import F, Q

res = models.Book.objects.filter(Q(authors__name='雲崖')|Q(authors__name="入雲龍"))
print(res)
# <QuerySet [<Book: 物件-Django精講爆款>, <Book: 物件-HTML入門爆款>, <Book: 物件-GO入門到精通爆款>, <Book: 物件-C語言入門爆款>, <Book: 物件-HTML入門爆款>, <Book: 物件-CSS入門爆款>, <Book: 物件-C語言入門爆款>, <Book: 物件-FLASK原始碼分析爆款>]>

   查詢書籍表中不是雲崖和入雲龍出版的書籍

from app01 import models

from django.db.models import Max, Min, Avg, Count, Sum
from django.db.models import F, Q


res = models.Book.objects.filter(~Q(authors__name='雲崖'),Q(authors__name="入雲龍"))
print(res)
# <QuerySet [<Book: 物件-CSS入門爆款>, <Book: 物件-FLASK原始碼分析爆款>]>

Q進階

   如果我們的Q查詢中,能去讓使用者來輸入字串指定欄位進行查詢,那麼就非常厲害了。

   其實這也是能夠實現。

from app01 import models

from django.db.models import Max, Min, Avg, Count, Sum
from django.db.models import F, Q

q = Q() # 例項化出一個物件
q.connector = "and"  # 指定q的關係

# 兩個Q
q.children.append(("price__lt","120")) 
q.children.append(("price__gt","50"))

res = models.Book.objects.filter(q)
# 相當於: models.Book.objects.filter(Q(price__lt=120),Q(price__gt=50))

print(res)
# <QuerySet [<Book: 物件-Python入門爆款>, <Book: 物件-C語言入門爆款>]>

查詢優化

   使用單表或多表時,可以使用以下方法進行查詢優化。

only

   only取出的記錄物件中只有指定的欄位,沒有其他欄位。

   因此查詢速度會非常快。

   預設會拿出所有欄位。

from app01 import models

obj = models.Book.objects.all().first()

print(obj.title)
print(obj.price)

SELECT `app01_book`.`id`, `app01_book`.`title`, `app01_book`.`price`, `app01_book`.`publish_date`, `app01_book`.`publish_id` FROM `app01_book` ORDER BY `app01_book`.`id` ASC LIMIT 1;

   使用only只會拿出指定欄位,但是如果要拿指定欄位以外的欄位。則會再次進行查詢

from app01 import models

obj = models.Book.objects.only("title").first()

print(obj.title)
print(obj.price)

SELECT `app01_book`.`id`, `app01_book`.`title` FROM `app01_book` ORDER BY `app01_book`.`id` ASC LIMIT 1; 
SELECT `app01_book`.`id`, `app01_book`.`price` FROM `app01_book` WHERE `app01_book`.`id` = 1; 

defer

   deferonly正好相反,拿除了指定欄位以外的其他欄位。

   但是如果要拿指定欄位的欄位。則會再次進行查詢

from app01 import models

obj = models.Book.objects.defer("title").first()


print(obj.title)
print(obj.price)

SELECT `app01_book`.`id`, `app01_book`.`price`, `app01_book`.`publish_date`, `app01_book`.`publish_id` FROM app01_book` ORDER BY `app01_book`.`id` ASC LIMIT 1;
SELECT `app01_book`.`id`, `app01_book`.`title` FROM `app01_book` WHERE `app01_book`.`id` = 1;

select_related

   原本的跨表,尤其是使用物件跨表,都會查詢兩次。

   但是select_related指定連表,則只會查詢一次。

   注意!select_related中指定的連表,只能是外來鍵名。並且不能是多對多關係

   原本跨表,兩次查詢語句。

from app01 import models

obj = models.Book.objects.all().first()
obj_fk = obj.publish
print(obj)
print(obj_fk)

SELECT `app01_book`.`id`, `app01_book`.`title`, `app01_book`.`price`, `app01_book`.`publish_date`, `app01_book`.`publish_id` FROM `app01_book` ORDER BY `app01_book`.`id` ASC LIMIT 1;

SELECT `app01_publish`.`id`, `app01_publish`.`name`, `app01_publish`.`addr`, `app01_publish`.`email` FROM `app01_publish` WHERE `app01_publish`.`id` = 1;

   如果是使用select_related,則只需要查詢一次。

from app01 import models

obj = models.Book.objects.select_related("publish").first()
obj_fk = obj.publish
print(obj)
print(obj_fk)


SELECT `app01_book`.`id`, `app01_book`.`title`, `app01_book`.`price`, `app01_book`.`publish_date`, `app01_book`.`publish_id`, `app01_publish`.`id`, `app01_publish`.`name`, `app01_publish`.`addr`, `app01_publish`.`email` FROM `app01_book` INNER JOIN `app01_publish` ON (`app01_book`.`publish_id` = `app01_publish`.`id`) ORDER BY `app01_book`.`id` ASC LIMIT 1;

prefetch_related

   prefetch_related是子查詢。它會預設走兩次SQL語句,相比於select_related,它的查詢次數雖然多了一次,但是量級少了很多,不用拼兩張表全部內容。

   注意!prefetch_related中指定的連表,只能是外來鍵名。並且不能是多對多關係

from app01 import models

obj = models.Book.objects.prefetch_related("publish").first()
obj_fk = obj.publish
print(obj)
print(obj_fk)

SELECT `app01_book`.`id`, `app01_book`.`title`, `app01_book`.`price`, `app01_book`.`publish_date`, `app01_book`.`publish_id` FROM `app01_book` ORDER BY `app01_book`.`id` ASC LIMIT 1;

SELECT `app01_publish`.`id`, `app01_publish`.`name`, `app01_publish`.`addr`, `app01_publish`.`email` FROM `app01_publish` WHERE `app01_publish`.`id` IN (1);  # 注意in,只查這一條

   如果要使用兩張表中許多資料,則使用select_related拼出整表。

   如果連表需要的資料不多,可使用prefetch_related子查詢。

原生SQL

   如果你覺得ORM有一些查詢搞不定,就可以使用原生SQL語句。

   官方文件:https://docs.djangoproject.com/zh-hans/3.1/topics/db/sql/

connection

   這和pymysql的使用基本一致,但是並不提供返回dict型別的資料。所以我們需要自定義一個函式來進行封裝。

from app01 import models
from django.db import connection, connections

def dictfetchall(cursor):
    "Return all rows from a cursor as a dict"
    columns = [col[0] for col in cursor.description]
    return [
    dict(zip(columns, row))
    for row in cursor.fetchall()
]

print(help(connection.cursor))

# 1.1 獲取遊標物件
cursor = connection.cursor()  # 預設連線default資料庫。
cursor.execute(
    """
    SELECT * FROM app01_book INNER JOIN app01_publish
    on app01_book.publish_id = app01_publish.id
    where app01_book.id = 1;
    """
,()) #第二引數,字串拼接。與pymysql使用相同,防止sql注入。

# 1.2 返回封裝結果
row = dictfetchall(cursor)
print(row)

# [{'id': 1, 'title': 'Django精講', 'price': Decimal('99.23'), 'publish_date': datetime.date(2020, 9, 11), 'publish_id': 1, 'name': '北京出版社', 'addr': '北京市海淀區', 'email': 'BeiJing@gmail.com'}]

raw

   raw是借用一個模型類,返回結果將返回至模型類中。

   返回結果是一個RawQuerySet物件,這種用法必須將主鍵取出

from app01 import models

res_obj = models.Book.objects.raw("select id,name from app01_author",params=[]) # 必須取出其他表的主鍵
# params = 格式化。防止sql注入

for i in res_obj:
	print(i.name)  # 雖然返回的Book例項化,但是其中包含了作者的欄位。可以通過屬性點出來

# 雲崖
# 浪裡白條
# 及時雨
# 玉麒麟
# 入雲龍

   你可以用 raw()translations 引數將查詢語句中的欄位對映至模型中的欄位。這是一個字典,將查詢語句中的欄位名對映至模型中的欄位名。說白了就是as一個別名。

   例如,上面的查詢也能這樣寫:

from app01 import models
name_map = {'pk': 'aid', 'name': 'a_name',}  # 不要使用id,使用pk。它知道是id  

res_obj = models.Book.objects.raw("select * from app01_author", params=[],translations=name_map)
# params = 格式化。防止sql注入

for i in res_obj:
	print(i.a_name) 

# 雲崖
# 浪裡白條
# 及時雨
# 玉麒麟
# 入雲龍

extra

   原生與ORM結合,這個可用於寫子查詢。

   我沒怎麼用過這個,很少使用。

def extra(self, select=None, where=None, params=None, tables=None, order_by=None, select_params=None)
    # 構造額外的查詢條件或者對映,如:子查詢

    Entry.objects.extra(select={'new_id': "select col from sometable where othercol > %s"}, select_params=(1,))
    Entry.objects.extra(where=['headline=%s'], params=['Lennon'])
    Entry.objects.extra(where=["foo='a' OR bar = 'a'", "baz = 'a'"])
    Entry.objects.extra(select={'new_id': "select id from tb where id > %s"}, select_params=(1,), order_by=['-nid'])

開啟事務

   Django中開啟事務非常簡單,因為又with()上下文管理器的情況,所以我們只需要在沒出錯的時候提交,出錯後回滾即可。

from django.db import transaction

 
def func_views(request):
    try:
        with transaction.atomic():      
			# 操作1
			# 操作2
			# 如果沒發生異常自動提交事務
            # raise DatabaseError 資料庫異常,說明sql語句有問題    
    except DatabaseError:     # 自動回滾,不需要任何操作
            pass
            

詳解QuerySet

惰性求值

   QuerySet的一大特性就是惰性求值,即你不使用資料時是不會進行資料庫查詢。

from app01 import models
res_queryset = models.Book.objects.all()
# 無列印SQL

快取機制

   QuerySet具有快取機制,下次再使用同一變數時,不會走資料庫查詢而是使用快取。

from app01 import models
res_queryset = models.Book.objects.filter(pk=1)

for i in res_queryset:
	print(i)

for i in res_queryset:
	print(i)

# (0.001) SELECT `app01_book`.`id`, `app01_book`.`title`, `app01_book`.`price`, `app01_book`.`publish_date`, `app01_book`.`publish_id` FROM `app01_book` WHERE `app01_book`.`id` = 1; args=(1,)
# 物件-Django精講
# 物件-Django精講

避免髒資料

   因為QuerySet的快取機制,所以使得可能出現髒資料。

   即資料庫中修改了某個資料,但是QuerySet快取中依舊是舊資料。

   避免這種問題的解決方案有兩種:

   1.不要重複使用同一變數。每次使用都應該重新賦值(雖然增加查詢次數,但是保證資料準確)

   2.使用迭代器方法,不可重複用

from app01 import models
res_queryset = models.Book.objects.filter(pk=1)

for i in res_queryset.iterator(): # 迭代器
	print(i)

for i in res_queryset:  # 快取沒有,重新獲取
	print(i)

# (0.001) SELECT `app01_book`.`id`, `app01_book`.`title`, `app01_book`.`price`, `app01_book`.`publish_date`, `app01_book`.`publish_id` FROM `app01_book` WHERE `app01_book`.`id` = 1; args=(1,)
# 物件-Django精講
# (0.003) SELECT `app01_book`.`id`, `app01_book`.`title`, `app01_book`.`price`, `app01_book`.`publish_date`, `app01_book`.`publish_id` FROM `app01_book` WHERE `app01_book`.`id` = 1; args=(1,)
# 物件-Django精講

常用欄位及引數

mysql對照

DjangoMySQL
AutoField integer AUTO_INCREMENT
BigAutoField bigint AUTO_INCREMENT
BinaryField longblob
BooleanField bool
CharField varchar(%(max_length)s)
CommaSeparatedIntegerField varchar(%(max_length)s)
DateField date
DecimalField numeric(%(max_digits)s, %(decimal_places)s)
DurationField bigint
FileField varchar(%(max_length)s)
FilePathField varchar(%(max_length)s)
IntegerField integer
BigIntegerField bigint
IPAddressField char(15)
GenericIPAddressField char(39)
NullBooleanField bool
OneToOneField integer
PositiveIntegerField nteger UNSIGNED
SlugField varchar(%(max_length)s)
SmallIntegerField smallint
TextField longtext
TimeField time
UUIDField char(32)

數值型別

Django欄位描述是否有注意事項
AutoField int自增列,必須填入引數 primary_key=True 有,見描述
BigAutoField bigint自增列,必須填入引數 primary_key=True 有,見描述
SmallIntegerField 小整數 -32768 ~ 32767  
PositiveSmallIntegerField 正小整數 0 ~ 32767  
IntegerField 整數列(有符號的) -2147483648 ~ 2147483647  
PositiveIntegerField 正整數 0 ~ 2147483647  
BigIntegerField 長整型(有符號的) -9223372036854775808 ~ 9223372036854775807  
BooleanField 布林值型別  
NullBooleanField 可以為空的布林值  
FloatField 浮點型  
DecimalField 10進位制小數 有,見說明
BinaryField 二進位制型別  

   說明補充:

DecimalField(Field)
- 10進位制小數
- 引數:
	max_digits,小數總長度
	decimal_places,小數位長度

   其他補充:自定義無符號整數型別

class UnsignedIntegerField(models.IntegerField): # 必須繼承
	def db_type(self, connection):
		return 'integer UNSIGNED' # 返回真實存放的型別

字元型別

Django欄位描述是否有注意事項
CharField 必須提供max_length引數, max_length表示字元長度 有,見描述
EmailField 字串型別,Django Admin以及ModelForm中提供驗證機制  
IPAddressField 字串型別,Django Admin以及ModelForm中提供驗證 IPV4 機制  
GenericIPAddressField 字串型別,Django Admin以及ModelForm中提供驗證 Ipv4和Ipv6 有,見說明
URLField 字串型別,Django Admin以及ModelForm中提供驗證 URL  
SlugField 字串型別,Django Admin以及ModelForm中提供驗證支援 字母、數字、下劃線、連線符(減號)  
CommaSeparatedIntegerField 字串型別,格式必須為逗號分割的數字  
UUIDField 字串型別,Django Admin以及ModelForm中提供對UUID格式的驗證  
FilePathField 字串,Django Admin以及ModelForm中提供讀取資料夾下檔案的功能 有,見說明
FileField 字串,路徑儲存在資料庫,檔案上傳到指定目錄 有,見說明
ImageField 字串,路徑儲存在資料庫,檔案上傳到指定目錄 有,見說明

   說明補充:

GenericIPAddressField(Field)
- 字串型別,Django Admin以及ModelForm中提供驗證 Ipv4和Ipv6
- 引數:
    protocol,用於指定Ipv4或Ipv6, 'both',"ipv4","ipv6"
    unpack_ipv4, 如果指定為True,則輸入::ffff:192.0.2.1時候,可解析為192.0.2.1,開啟刺功能,需要protocol="both"

FilePathField(Field)
- 字串,Django Admin以及ModelForm中提供讀取資料夾下檔案的功能
- 引數:
    path,                      資料夾路徑
    match=None,                正則匹配
    recursive=False,           遞迴下面的資料夾
    allow_files=True,          允許檔案
    allow_folders=False,       允許資料夾

FileField(Field)
- 字串,路徑儲存在資料庫,檔案上傳到指定目錄
- 引數:
    upload_to = ""      上傳檔案的儲存路徑
    storage = None      儲存元件,預設django.core.files.storage.FileSystemStorage


ImageField(FileField)
- 字串,路徑儲存在資料庫,檔案上傳到指定目錄
- 引數:
    upload_to = ""      上傳檔案的儲存路徑
    storage = None      儲存元件,預設django.core.files.storage.FileSystemStorage
    width_field=None,   上傳圖片的高度儲存的資料庫欄位名(字串)
    height_field=None   上傳圖片的寬度儲存的資料庫欄位名(字串)

時間型別

Django欄位描述是否有注意事項
DateTimeField 日期+時間格式 YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ] 有,見說明
DateField 日期格式 YYYY-MM-DD 有,見說明
TimeField 時間格式 HH:MM[:ss[.uuuuuu]]  
DurationField 長整數,時間間隔,資料庫中按照bigint儲存,ORM中獲取的值為datetime.timedelta型別  

   說明補充:

時間型別的欄位都有兩個引數:
auto_now=False  # 當記錄更新時是否自動更新當前時間
auto_now_add=False # 當記錄建立時是否自動更新當前時間

條件引數

條件引數描述
null 資料庫中欄位是否可以為空,接受布林值
db_column 資料庫中欄位的列名,接受字串
default 資料庫中欄位的預設值,接受時間,數值,字串
primary_key 資料庫中欄位是否為主鍵,接受布林值(一張表最多一個主鍵)
db_index 資料庫中欄位是否可以建立索引,接受布林值
unique 資料庫中欄位是否可以建立唯一索引,接受布林值
unique_for_date 資料庫中欄位【日期】部分是否可以建立唯一索引,接受布林值
unique_for_month 資料庫中欄位【月】部分是否可以建立唯一索引,接受布林值
unique_for_year 資料庫中欄位【年】部分是否可以建立唯一索引,接受布林值

choices

   choices是一個非常好用的引數。它允許你資料庫中存一個任意型別的值,但是需要使用時則是使用的它的描述。

   如下:

gender = models.BooleanField(choices=((0,"male"),(1,"female")),default=0)
# 實際只存0和1,但是我們取的時候會取male或者female

   取出方法:

get_col_display()

後端示例:
	obj = models.User.objects.get(pk=1)
	gender = obj.get_gender_display()
	print(gender)  # male
	
前端示例:
	{{obj.get_gender_display}}  <!-- 前端不用加括號,自己呼叫 -->

元資訊

class UserInfo(models.Model):
        nid = models.AutoField(primary_key=True)
        username = models.CharField(max_length=32)
        class Meta:
            # 資料庫中生成的表名稱 預設 app名稱 + 下劃線 + 類名
            db_table = "table_name"

            # 聯合索引
            index_together = [
                ("pub_date", "deadline"),
            ]

            # 聯合唯一索引
            unique_together = (("driver", "restaurant"),)

            # admin中顯示的表名稱
            verbose_name

            # verbose_name加s
            verbose_name_plural

多表關係引數

ForeignKey(ForeignObject) # ForeignObject(RelatedField)
        to,                         # 要進行關聯的表名
        to_field=None,              # 要關聯的表中的欄位名稱
        on_delete=None,             # 當刪除關聯表中的資料時,當前表與其關聯的行的行為
                                        - models.CASCADE,刪除關聯資料,與之關聯也刪除
                                        - models.DO_NOTHING,刪除關聯資料,引發錯誤IntegrityError
                                        - models.PROTECT,刪除關聯資料,引發錯誤ProtectedError
                                        - models.SET_NULL,刪除關聯資料,與之關聯的值設定為null(前提FK欄位需要設定為可空)
                                        - models.SET_DEFAULT,刪除關聯資料,與之關聯的值設定為預設值(前提FK欄位需要設定預設值)
                                        - models.SET,刪除關聯資料,
                                                      a. 與之關聯的值設定為指定值,設定:models.SET(值)
                                                      b. 與之關聯的值設定為可執行物件的返回值,設定:models.SET(可執行物件)

                                                        def func():
                                                            return 10

                                                        class MyModel(models.Model):
                                                            user = models.ForeignKey(
                                                                to="User",
                                                                to_field="id"
                                                                on_delete=models.SET(func),)
        related_name=None,          # 反向操作時,使用的欄位名,用於代替 【表名_set】 如: obj.表名_set.all()
        related_query_name=None,    # 反向操作時,使用的連線字首,用於替換【表名】     如: models.UserGroup.objects.filter(表名__欄位名=1).values('表名__欄位名')
        limit_choices_to=None,      # 在Admin或ModelForm中顯示關聯資料時,提供的條件:
                                    # 如:
                                            - limit_choices_to={'nid__gt': 5}
                                            - limit_choices_to=lambda : {'nid__gt': 5}

                                            from django.db.models import Q
                                            - limit_choices_to=Q(nid__gt=10)
                                            - limit_choices_to=Q(nid=8) | Q(nid__gt=10)
                                            - limit_choices_to=lambda : Q(Q(nid=8) | Q(nid__gt=10)) & Q(caption='root')
        db_constraint=True          # 是否在資料庫中建立外來鍵約束
        parent_link=False           # 在Admin中是否顯示關聯資料


    OneToOneField(ForeignKey)
        to,                         # 要進行關聯的表名
        to_field=None               # 要關聯的表中的欄位名稱
        on_delete=None,             # 當刪除關聯表中的資料時,當前表與其關聯的行的行為

                                    ###### 對於一對一 ######
                                    # 1. 一對一其實就是 一對多 + 唯一索引
                                    # 2.當兩個類之間有繼承關係時,預設會建立一個一對一欄位
                                    # 如下會在A表中額外增加一個c_ptr_id列且唯一:
                                            class C(models.Model):
                                                nid = models.AutoField(primary_key=True)
                                                part = models.CharField(max_length=12)

                                            class A(C):
                                                id = models.AutoField(primary_key=True)
                                                code = models.CharField(max_length=1)

    ManyToManyField(RelatedField)
        to,                         # 要進行關聯的表名
        related_name=None,          # 反向操作時,使用的欄位名,用於代替 【表名_set】 如: obj.表名_set.all()
        related_query_name=None,    # 反向操作時,使用的連線字首,用於替換【表名】     如: models.UserGroup.objects.filter(表名__欄位名=1).values('表名__欄位名')
        limit_choices_to=None,      # 在Admin或ModelForm中顯示關聯資料時,提供的條件:
                                    # 如:
                                            - limit_choices_to={'nid__gt': 5}
                                            - limit_choices_to=lambda : {'nid__gt': 5}

                                            from django.db.models import Q
                                            - limit_choices_to=Q(nid__gt=10)
                                            - limit_choices_to=Q(nid=8) | Q(nid__gt=10)
                                            - limit_choices_to=lambda : Q(Q(nid=8) | Q(nid__gt=10)) & Q(caption='root')
        symmetrical=None,           # 僅用於多對多自關聯時,symmetrical用於指定內部是否建立反向操作的欄位
                                    # 做如下操作時,不同的symmetrical會有不同的可選欄位
                                        models.BB.objects.filter(...)

                                        # 可選欄位有:code, id, m1
                                            class BB(models.Model):

                                            code = models.CharField(max_length=12)
                                            m1 = models.ManyToManyField('self',symmetrical=True)

                                        # 可選欄位有: bb, code, id, m1
                                            class BB(models.Model):

                                            code = models.CharField(max_length=12)
                                            m1 = models.ManyToManyField('self',symmetrical=False)

        through=None,               # 自定義第三張表時,使用欄位用於指定關係表
        through_fields=None,        # 自定義第三張表時,使用欄位用於指定關係表中那些欄位做多對多關係表
                                        from django.db import models

                                        class Person(models.Model):
                                            name = models.CharField(max_length=50)

                                        class Group(models.Model):
                                            name = models.CharField(max_length=128)
                                            members = models.ManyToManyField(
                                                Person,
                                                through='Membership',
                                                through_fields=('group', 'person'),
                                            )

                                        class Membership(models.Model):
                                            group = models.ForeignKey(Group, on_delete=models.CASCADE)
                                            person = models.ForeignKey(Person, on_delete=models.CASCADE)
                                            inviter = models.ForeignKey(
                                                Person,
                                                on_delete=models.CASCADE,
                                                related_name="membership_invites",
                                            )
                                            invite_reason = models.CharField(max_length=64)
        db_constraint=True,         # 是否在資料庫中建立外來鍵約束
        db_table=None,              # 預設建立第三張表時,資料庫中表的名稱欄位以及引數

M2M建立

   M2M的建立方式有三種,分別是全自動,半自動,全手動。

   全自動使用最方便,但是擴充套件性最差。

   半自動介於全自動與全手動之間。

   全手動使用最麻煩,但是擴充套件性最好。

全自動

   自動建立的第三張表,即為全自動。

   最上面多表關係中的多對多,使用的便是全自動建立。

   全自動建立操縱及其方便,如add/set/remove/clear

半自動

   手動建立第三張表,並指定此表中那兩個欄位是其他兩張表的關聯關係。

   可使用__跨表,正反向查詢。

   但是不可使用如add/set/remove/clear

   在實際生產中,推薦使用半自動。它的擴充套件性最強

class Book(models.Model):
	name = models.CharField(max_length=32)
	authors = models.ManyToManyField(
		to="Author", # 與作者表建立關係。
		through="M2M_BookAuthor", # 使用自己建立的表
		through_fields=('book','author'), # 這兩個欄位是關係  注意!那張表上建立多對多,就將欄位放在前面
	)
	
class Author(models.Model):
	name = models.CharField(max_length=32)
	# book = models.ManyToManyField(
		# to="Book",
		# through="M2M_BookAuthor",
		# through_fields=('author','book'), Author建立,所以author放在前面
	
	# )
	
class M2M_BookAuthor(models.Model):
	book = models.ForeignKey(to="Book")
	author = models.ForeignKey(to="Author")
	# 兩個fk,自動新增_id字尾。
	
    class Meta:
    	unique_together = (("author", "book"),)
    	# 聯合唯一索引

全手動

   不可使用add/set/remove/clear,以及__跨表,正反向查詢。

   所有資料均手動錄入。

class Book(models.Model):
	name = models.CharField(max_length=32)

	
class Author(models.Model):
	name = models.CharField(max_length=32)
	
class M2M_BookAuthor(models.Model):
	book = models.ForeignKey(to="Book")
	author = models.ForeignKey(to="Author")
	# 兩個fk,自動新增_id字尾。
	
    class Meta:
    	unique_together = (("author", "book"),)
    	# 聯合唯一索引

相關文章