之前有在用Django寫一些小網站,現在暑假想說再來複習一下之前買的這本書
於是我就把它寫成一系列的文章,也方便查語法
而且因為這本書大概是2014年出的,如今Django也已經出到2.多版
有些內容也變得不再支援或適用,而且語法或許也改變了
所以我會以最新版的Python和Django來修正這本書的內容跟程式碼
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='雞丁')
當我們想要查詢所有辣的食物且想要讓它們照價格排列
我們可以使用連鎖查詢的技巧:
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()
方法可以得知查詢集中的資料筆數
原始查詢集就是我們利用全查詢得到的查詢集
不過只要透過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
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'
雖然我們可以在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)
...
宮保雞丁
接下來要介紹一種使用資料庫的機制-資料庫交易(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元,不過轉帳的動作確實取消了
這兩個小節我還要花時間驗證跟測試,因為目前暫時還不會用到,所以先跳過,之後如果有空可能會再更新