之前有在用Django写一些小网站,现在暑假想说再来複习一下之前买的这本书
于是我就把它写成一系列的文章,也方便查语法
而且因为这本书大概是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, connectionclass 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)
这个机制有分成四种特性:
from django.db import modelsclass Account(models.Model): money = models.IntegerField()
mysite/mysite/actions.py
import loggingfrom django.db import transaction@transaction.atomicdef 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-使用多个资料库
这两个小节我还要花时间验证跟测试,因为目前暂时还不会用到,所以先跳过,之后如果有空可能会再更新