iT邦幫忙

0

Django學習紀錄 18.資料庫與模型進階技巧

之前有在用Django寫一些小網站,現在暑假想說再來複習一下之前買的這本書
https://ithelp.ithome.com.tw/upload/images/20190724/20118889bj9fH1vhuR.jpg
於是我就把它寫成一系列的文章,也方便查語法
而且因為這本書大概是2014年出的,如今Django也已經出到2.多版
有些內容也變得不再支援或適用,而且語法或許也改變了
所以我會以最新版的Python和Django來修正這本書的內容跟程式碼

目錄:django系列文章-Django學習紀錄

18. 資料庫與模型進階技巧

18.1 主題1-欄位查詢技巧

restaurants = Restaurant.objects.filter(name__contains='餐廳')

可以過濾出所有name欄位包含'餐廳'的所有資料
而這個查詢語句會對應到SQL語言的

SELECT  ... WHERE name LIKE '%餐廳%';

整理一下常見的雙底線欄位查詢(field lookups)技巧:

field lookups 說明 範例
exact 與指定的值完全相等 Restaurant.objects.filter(name__exact='goodgoodeat')
iexact 同exact,但不分大小寫 Restaurant.objects.filter(name__iexact='GoOdgoodEat')
contains 包含指定的值 Restaurant.objects.filter(name__contains='eat')
icontains 同contains,但不分大小寫 Restaurant.objects.filter(name__icontains='Eat')
in 在指定的串列中 Restaurant.objects.filter(name__in=['goodgoodeat','badbadeat'])
gt 大於指定的值 Food.objects.filter(price__gt=100)
gte 大於等於指定的值 Food.objects.filter(price__gte=100)
lt 小於指定的值 Food.objects.filter(price__lt=100)
lte 小於等於指定的值 Food.objects.filter(price__lte=100)
startswith 以指定的字串開頭 Food.objects.filter(name__startswith='宮保')
endswith 以指定的字串結尾 Food.objects.filter(price__endswith='雞丁')

18.2 主題2-自定義模型管理器

18.2.1 自己的管理器與新的方法

當我們想要查詢所有辣的食物且想要讓它們照價格排列
我們可以使用連鎖查詢的技巧:

Food.objects.filter(is_spicy=True).order_by('price')

但是如果很常用到的話,會非常麻煩
於是我們可以自己定義一個管理器
models.py

class FoodManager(models.Manager):
    def sfood_order_by_price(self):
        return self.filter(is_spicy=True).order_by('price')

class Food(models.Model):
    ...
    objects = FoodManager()
    ...

首先自定義FoodManager管理器,繼承自models.Manager這個預設的管理器類別
接著加入自定義的管理器新方法,這裡的self代表該管理器
因為繼承了models.Manager,所以原本一般的管理器查詢方法它也會有
接著必須在Food模型中將objects這個管理器設定為我們自定義的FoodManager
之前我們都不用設定是因為,如果沒有指定objects的話,Django預設會使用models.Manager來做為該模型的管理器
接著打開Django shell

>>> from restaurants.models import Food
>>> Food.objects.sfood_order_by_price()
<QuerySet [<Food: 宮保雞丁>]>

同樣的如果想要有一個管理器方法可以查詢所有辣的食物

class FoodManager(models.Manager):
    def sfood(self):
        return self.filter(is_spicy=True)

而管理器的方法不只侷限在回傳一個查詢集
比如我們想要知道價格低於100元以下的食物有幾道

class FoodManager(models.Manager):
    def cheap_food_num(self):
        return self.filter(price__lt=100).count()

其中的count()方法可以得知查詢集中的資料筆數

18.2.2 自定義原始查詢集

原始查詢集就是我們利用全查詢得到的查詢集
不過只要透過get_queryset方法就能改變原始查詢集

class FoodManager(models.Manager):
    def get_queryset(self):
        return super(FoodManager, self).get_queryset().filter(is_spicy=True)

    def cheap_food_num(self):
        return self.filter(price__lt=100).count()

管理器的all方法或是filter方法都是依靠呼叫get_queryset來獲取原始查詢集再做處理
所以只要在自定義的管理器裡覆寫get_queryset方法就可以改變基礎的查詢集
super(子類別名, self)可以幫助我們拿到基礎類別的方法
在python3中,可以只寫成super()這樣即可
結果

>>> from restaurants.models import Food
>>> Food.objects.all()
<QuerySet [<Food: 宮保雞丁>]>
>>> Food.objects.cheap_food_num()
0

18.2.3 使用多個管理器

class SpicyFoodManager(models.Manager):
    def get_queryset(self):
        return super(SpicyFoodManager, self).get_queryset().filter(is_spicy=True)

class NotSpicyFoodManager(models.Manager):
    def get_queryset(self):
        return super(NotSpicyFoodManager, self).get_queryset().filter(is_spicy=False)
class Food(models.Model):
    ...
    objects = models.Manager()
    s_objects = SpicyFoodManager()
    ns_objects = NotSpicyFoodManager()
    ...

結果

>>> from restaurants.models import Food
>>> Food.objects.all()
<QuerySet [<Food: 炒青菜>, <Food: 宮保雞丁>]>
>>> Food.s_objects.all()
<QuerySet [<Food: 宮保雞丁>]>
>>> Food.ns_objects.all()
<QuerySet [<Food: 炒青菜>]>

要注意的是,如果有用到自定義管理器,objects一定要賦值,不然會導致該模型不存在objects管理器,如果將這個例子的objects = models.Manager()刪去

>>> from restaurants.models import Food
>>> Food.objects.all()

會出現錯誤

AttributeError: type object 'Food' has no attribute 'objects'

18.3 主題3-執行原始SQL查詢

雖然我們可以在Django外面對資料庫server下達SQL,不過Django有提供在內部下達SQL的方法,那就是使用django.db.connection物件
如果想要利用SQL語法來查詢食物價格剛好為120的食物

SELECT name FROM restaurants_food WHERE price=120

則可以這樣做:

>>> from django.db import connection
>>> cursor = connection.cursor()
>>> cursor.execute('SELECT name FROM restaurants_food WHERE price=120')
<django.db.backends.sqlite3.base.SQLiteCursorWrapper object at 0x036D5298>
>>> print(cursor.fetchone()[0])
宮保雞丁

使用cursor方法建立一個cursor,cursor.execute執行SQL語句
獲得的查詢集可以用cursor.fetchone()來獲得第一個結果或是用cursor.fetchall()來獲得整個查詢集,因為是回傳元組,所以用[0]取出第一個元素
接著也可以放在自定義的管理器中

from django.db import models, connection

class FoodManager(models.Manager):
    def get_120_food(self):
        cursor = connection.cursor()
        cursor.execute("""
            SELECT name 
            FROM restaurants_food 
            WHERE price=120
        """)
        return [result[0] for result in cursor.fetchall()]
        
class Food(models.Model):
    ...
    objects = FoodManager()
    ...

SQL語句特別使用三引號的多行字串來擺放整齊
另外這邊使用了list comprehension的手法,讓回傳的串列是一個字串的串列而不是元組的串列
而使用SQL直接查詢的資料並非模型的實例,而是真實資料庫中指定型態的資料
結果

>>> from restaurants.models import Food
>>> for f in Food.objects.get_120_food():
...     print(f)
...
宮保雞丁

18.4 主題4-資料庫交易

接下來要介紹一種使用資料庫的機制-資料庫交易(Transaction)
假設A帳戶有100元,B帳戶有99999元,而每個帳戶最多只能存10萬元
現在要從A帳戶裡取出100元轉帳到B帳戶裡
一般撰寫程式會先把A帳戶-100
再將B帳戶+100
但是當執行B帳戶+100時會發生錯誤,因為加完後會超出最大值10萬
於是程式實際上只有執行A帳戶-100
所以這時資料庫的資料會變為A帳戶0元,B帳戶仍為99999元
這時就會變得尷尬了,於是我們需要將資料庫中的資料回復成轉帳前的狀態
而這個取消動作讓資料變為原本的狀態的機制就稱為資料庫交易(Transaction)
這個機制有分成四種特性:

特性 說明
原子性(Atomicity) 在同一個交易內的異動(資料庫的操作)需全部成功,否則全都取消,這個取消的動作稱為回滾(rollback),即回復至交易(操作)尚未發生之前
一致性(Consistency) 資料在交易(操作)前後仍維持完整,也就是說輸入或修改的資料需正確的符合表單格式及欄位規範,使資料庫仍正常工作不會錯誤
隔離性(Isolation) 當有多個交易(操作)同時進行時不會交互影響
持久性(Durability) 已完成的交易(操作)需保存在資料庫裡

這四個特性統稱為ACID
接下來用剛才的轉帳例子來示範Django內建的資料庫交易機制
models.py

from django.db import models

class Account(models.Model):
    money = models.IntegerField()

mysite/mysite/actions.py

import logging
from django.db import transaction

@transaction.atomic
def transfer_money(_from, _to, quota):
    if _from.money < 15:
        raise ValueError("連手續費都付不起,請回吧!!")
    # 收取手續費
    _from.money = _from.money - 15
    _from.save()
    # 取得回滾的基準點
    sid = transaction.savepoint()
    try:
        _from.money = _from.money - quota
        if _from.money < 0:
            raise ValueError("超額提領!")
        _from.save()
        _to.money = _to.money + quota
        if _to.money > 100000:
            raise ValueError("超額儲存!")
        _to.save()
        # 如果操作及檢查都沒有問題,那就把資料提交到資料庫
        transaction.savepoint_commit(sid)
    except ValueError as e:
        logging.error("金額操作錯誤,訊息:<%s>", e)
        # 當發生問題時回滾到之前的基準點,還原先前操作影響的資料
        transaction.savepoint_rollback(sid)
    except Exception as e:
        logging.error("其他錯誤,訊息:<%s>", e)
        transaction.savepoint_rollback(sid)

打開Django shell

>>> from restaurants.models import Account
>>> from mysite.actions import transfer_money
>>> _from, _to = Account.objects.get(id=1), Account.objects.get(id=2)
>>> _from.money, _to.money
(100, 99999)
>>> transfer_money(_from, _to, 100)
ERROR:root:金額操作錯誤,訊息:<超額提領!>
>>> _from.money, _to.money # 注意,實例會被修改
(-15, 99999)
>>> _from, _to = Account.objects.get(id=1), Account.objects.get(id=2) # 所以要重新賦值來刷新
>>> _from.money, _to.money # 重新看資料庫裡的資料
(85, 99999) # 被扣手續費15元,不過轉帳的動作確實取消了

18.5 主題5-使用不同的資料庫

18.6 主題6-使用多個資料庫

這兩個小節我還要花時間驗證跟測試,因為目前暫時還不會用到,所以先跳過,之後如果有空可能會再更新

上一篇:Django學習紀錄 17.視圖類別

下一篇:Django學習紀錄 19.測試


尚未有邦友留言

立即登入留言