聲明
上篇地址:https://segmentfault.com/a/11...
雖然上一篇,已經(jīng)說明,但還是強調(diào)一下,peewee是 python-ORM(只支持 MySQL,Sqlite,postgresql )
雖然ORM可以與多種數(shù)據(jù)庫無縫相接,并且兼容性好, 但是某些細(xì)微的語法并不是數(shù)據(jù)庫共有的。
我用MySQL, 所以下面說的都是基于MySQL(其他2種數(shù)據(jù)庫也差不了多少, 99%是一樣的)
總官檔地址:http://docs.peewee-orm.com/en...
官方Github地址:https://github.com/coleifer/p...
增加數(shù)據(jù)
方式1:(推薦)
zhang = Owner.create(name='Zhang', age=20)
方式2:
zhang = Owner(name='Zhang1', age=18)
zhang.save()
# 你可以看見,它需要用save(),所以推薦用上一種方式。
方式3:(推薦)
cython = Owner.insert(name='Cython', age=15).execute()
# 方式1 和 方式2, 返回結(jié)果都是模型實例"(也就意味著創(chuàng)建了一個實例)"
# 而本方式,返回結(jié)果是 最新插入的主鍵值"(也就意味著不會創(chuàng)建實例)"
如果存在外鍵關(guān)聯(lián),假如存在 Pet類 引用的 Owner的主鍵,插入數(shù)據(jù)方式有2種:
方式1: 用新建對象變量傳值:
lin = Owner.create(name='lin', age=20)
tom1 = Pet.create(name='Tom', age=1, owner=lin) # 注意 owner = lin
方式2: 手動維護(hù)主鍵 id,通過主鍵傳值(或者通過查詢id):
lin = Owner.create(id=100, name='lin', age=20) # id自己給的值為 100
tom1 = Pet.create(name='Tom', age=1, owner=100) # 注意 owner=100
插入多條數(shù)據(jù):(官檔有好幾種方法,我只說最提倡,最快速的方法(好處就是一次性提交,不用循環(huán)))
方式1:
"""
注意格式 [ {},{},{} ]
每個字典,對應(yīng)一條記錄。
"""
data = [
{'name': 'Alice', 'age': 18},
{'name': 'Jack', 'age': 17},
]
Owner.insert_many(data).execute()
方式2: (就是不用在數(shù)據(jù)中都指定鍵了,方便一點)
"""
注意格式 [ (),(),() ]
每個元組,對應(yīng)一條記錄。
"""
data = [
('Alice', 18),
('Jack', 17),
]
User.insert_many(data, fields=[Owner.name, Owner.age]).execute()
注意一下:尾部都必須要帶一個execute()
如果數(shù)據(jù)量過大,可能會出現(xiàn)OOM等問題。你可以手動分批,但是 peewee 給我們提供了成品的 api
from peewee import chunked
with mysql_db.atomic(): # 官檔建議用事務(wù)包裹
for batch in chunked(data, 100): # 一次100條, chunked() 返回的是可迭代對象
Owner.insert_many(batch).execute()
防止數(shù)據(jù)重復(fù)插入的2種辦法(或者防止設(shè)置了主鍵,重復(fù)插入拋出異常,導(dǎo)致程序無法運行):
方法1: INGORE關(guān)鍵字 (這種方式是如果沖突了,就自動忽略)
SQL:
insert ignore into owner (name,age) values ('lin',30);
peewee:
Owner.insert(name='lin', age=30).on_conflict_ignore()
方法2:用 ON DUPLICATE KEY UPDATE (這種方式,是如果沖突了,你還可以做一些操作)
SQL:
insert into owner (name,age) values ('lin',30)
ON DUPLICATE KEY
UPDATE name='lin', age=30; # 如果沖突了,可以重新設(shè)置值
peewee:
Owner.insert(name='lin', age=30).on_conflict(
preserve=[Owner.name, Owner.age], # 若沖突,你想保留不變的字段
update={Owner.name: 'lin', Owner.age: 30} # 若沖突,你想更新什么
).execute()
# 注: preserve 和 update 按情況用,一般設(shè)置一個用就行了。
刪除數(shù)據(jù)
方法1:
php = Owner.get(name='PHP') # 獲取單條對象
php.delete_instance()
# 注意: delete_instance() 只能刪除單條對象, 如果用select()查出來的,需要遍歷才能刪
方法2:
Owner.delete().where(Owner.name == 'lin').execute()
# 注意這種方法和添加類似, 最后也必須有個 execute()
修改數(shù)據(jù)
方式1:(不推薦)
owner= 查詢單個對象結(jié)果
owner.name = 'Pack'
owner.name = 50
owner.save() # 你可以看見,我們還需要手動調(diào)用一下save()
方式2:(推薦)
query = Owner.update(name='Pack', age=50).where(Owner.name == 'Zhang')
query.execute()
查詢數(shù)據(jù)
查詢單條數(shù)據(jù) (特別注意,如果你有多條,它只會給你返回第一條)
"""存在則返回原有對象, 不存在則拋error"""
one_owner = Owner.get(name='Zhang2')
print(one_woner.age)
擴展版1: get_or_create
"""存在則返回原有對象。不存在則插入數(shù)據(jù),并返回新對象"""
obj, status = Owner.get_or_create(name='Zhang23213',age=3123)
print(obj.name, status)
# obj就是返回的新對象
# status表示插入是否成功 True 或者 False
擴展版2: get_or_none
"""存在則返回原有對象, 不存在則返回 None (不會拋error)"""
Owner.get_or_none(name='abc')
查詢多條數(shù)據(jù)
正常查詢所有數(shù)據(jù)
owners = Owner.select() # 返回結(jié)果 owners 是對象集合,需要遍歷
for owner in owners: # owner 是每個對象(對應(yīng)每條記錄)
print(woner.name)
當(dāng)然你可以在查詢后轉(zhuǎn)為 python 類dict格式:
owners = Owner.select().dicts() # 返回結(jié)果 owners 是 "類字典對象集合"
for owner in owners: # owner是每個字典對象, (它 對應(yīng)每條記錄)
print(owner['name']) # 字典語法取值,懂了吧,不多說了。
上面的查詢?nèi)绻跀?shù)據(jù)大量的情況下可能會導(dǎo)致OOM,因此可轉(zhuǎn)為迭代:
"""再每個查詢的最后加上 .iterator() 即可"""
eg:
owners = Owner.select().iterator()
owners = Owner.select().dicts().iterator()
條件查詢:
首先我先強調(diào)個,"MySQL是否區(qū)分大小寫" 的事:
MySQL5.7+,是區(qū)分大小寫的; (MySQL8,和 MariaDB 我沒試, 應(yīng)該和 5.7是一樣的)
但這個區(qū)分大小寫 僅僅僅僅僅僅 是 針對于 SQL語句的表名 "" 引號外面的(就是非字符串語法)
舉個例子:
現(xiàn)有一表,名叫 owner
desc owner # 正確
desc OWNER # 錯誤,表不存在
這種情況下,因為不涉及字符串的 "" 引號操作,所以是嚴(yán)格區(qū)分大小寫的。
"而引號里面" (其實就是涉及字符串)的數(shù)據(jù)語法,是 不區(qū)分 大小寫的。
舉個例子(因為下面例子都有 "" 字符串操作,所以都 不區(qū)分 大小寫):
SQL:
查詢例子:
select * from owner where name='zHang'
select * from owner where name='ZHANG'
他們倆查詢的是同一個數(shù)據(jù)。
插入例子:
insert into owner values("zhaNg")
insert into owner values("zhang")
他們倆 插入的 也是同一個數(shù)據(jù)
peewee:
查詢例子:
...where(name="zhang")
...where(name="ZHaNg")
他們倆查詢的是 同一個數(shù)據(jù)。
插入例子:
...insert({'name':'Zhang')
...insert({'name': 'zhANG')
他們倆 插入的 也是同一個數(shù)據(jù)
官檔-條件操作符:http://docs.peewee-orm.com/en...
上邊的連接是官檔操作符大全,下面我把部分常用摘出來說一下。
常用操作符
與或非:
與:&
模型類.where( (User.is_active == True) & (User.is_admin == True) )
或:|
模型類.where( (User.is_admin) | (User.is_superuser) )
非:~
模型類.where( ~(User.username.contains('admin')) )
我說兩句,方便記憶:
1. SQL語句中"與或非" 是 "and or not" 語法, 為啥peewee不遵循?
答: 因為,"python原語法"也是這三個。。。沖突, 所以 peewee改了。
2. 看上面的例子, 每個條件操作符 "兩邊"的代碼 都用 "()" 括起來了
范圍:
# 查詢年齡18到20的數(shù)據(jù) (前閉后閉)
for owner in Owner.select().where(Owner.age.between(18,20)):
print(owner.age)
包含&不包含:
不包含:not_in (同下)
不包含:in_
# 將姓名包含 Alice和Tom的記錄找出來
for owner in Owner.select().where(Owner.name.in_(['Alice', 'Tom'])):
print(owner.name)
是否為null:
# True 就代表把所有 name 為 null 的 記錄都查出來
# False 就代表把所有 name 為 非null 的 記錄都查出來
for owner in Owner.select().where( Owner.name.is_null(True) ):
print(owner.name)
以..開頭 & 以..結(jié)尾
以..開頭: startswith
以..結(jié)尾: endswith
# 把以 ali 開頭的 都查詢出來
for owner in Owner.select().where(Owner.name.startswith('ali')):
print(owner.name)
模糊查詢:
# 將包含 li 字符串的數(shù)據(jù)查詢出來
for owner in Owner.select().where(Owner.name.contains('li')):
print(owner.name)
正則查詢:
這個就有意思了。前面我們強調(diào)過,MySQL帶引號字符串是不區(qū)分大小寫的。
而正則功能提供給我們區(qū)分大小寫的API。(這是個特例,只有正則區(qū)分大小寫的功能。記住)
例子條件:
假如我們有一個數(shù)據(jù) name為 Alice
regexp: 嚴(yán)格區(qū)分大小寫的正則
# 用的是 regexp,區(qū)分大小寫, 條件給的是 al小寫, 所以當(dāng)然 查不出來,返回空
for owner in Owner.select().where(Owner.name.regexp('al*')):
print(owner.name)
iregexp:不區(qū)分大小寫的正則
# 用的是 iregexp, 不區(qū)分大小寫。 因此即使 你給 al小寫, 也能夠?qū)?Alice查出來。
for owner in Owner.select().where(Owner.name.iregexp('al*')):
print(owner.name)
統(tǒng)計記錄數(shù) count
print(MyModel.select().count())
offset & limit
"""跳過前2行,從第2+1行開始,取1條, 其實取出的就是第3行"""
for x in Owner.select().offset(2).limit(1).dicts():
print(x)
分頁 paginate
"""
1. paginate 第1個參數(shù)為 第幾頁
2. paginate 第2個參數(shù)為 一頁幾個數(shù)據(jù)
3. paginate會自動根據(jù)查詢的所有記錄總數(shù) 和 你傳的 兩個 參數(shù)來為你自動分頁
"""
for obj in MyModel.select().paginate(1,3).dicts(): # 第一頁,每頁三個數(shù)據(jù)
print(obj)
# peewee提供給我們分頁就這么多,想要更多需求,需要我們自己發(fā)散思維。
# 下面是我自己粗略寫的一個笨拙的分頁。。可以參考下。。
def page(document_count=None, per_page_size=None, start_page=1):
page_count = (document_count // per_page_size) # 整除的頁數(shù)(可能有殘頁)
is_rest = (document_count % per_page_size) # 總數(shù)/每頁數(shù):是否能除盡
# 除盡代表整頁直接返回,除不盡有殘頁 ,頁碼+1 返回
page_count = page_count if not is_rest else page_count + 1
for page in range(start_page, page_count + 1):
for obj in MyModel.select().paginate(page, per_page_size).dicts().iterator():
yield obj
document_count = MyModel.select().count() # 先獲取記錄總數(shù)
for obj in page(document_count=document_count, per_page_size=3, start_page=1):
print(obj)
# 如果你有需求分頁切片或索引, 那么你可以封裝成類,然后實現(xiàn) __getitem__ 方法
document_count = MyModel.select().count()
for obj in page(document_count=document_count, per_page_size=3, start_page=1):
print(obj)
排序 order_by
# 默認(rèn)升序 asc()
for owner in Owner.select().order_by(Owner.age):
print(owner.age)
# 降序 desc()
for owner in Owner.select().order_by(Owner.age.desc()):
print(owner.age)
分組 group_by
# 用姓名分組,統(tǒng)計人頭數(shù)大于1的所有記錄,降序查詢
query = Owner.select(Owner.name, fn.count(Owner.name).alias('total_num')) \
.group_by(Owner.name) \
.having(fn.count(Owner.name) > 1) \
.order_by(SQL('total_num').desc())
for owner in query:
print(f'名字為{owner.name}的 人數(shù)為{owner.total_num}個')
分組注意事項,說幾點:
1. 分組操作,和SQL的group by一樣, group by后面寫了什么字段, 前面select同時也必須包含
2. .alias('統(tǒng)計結(jié)果字段名'),是給統(tǒng)計后的結(jié)果起一個新字段名。
3. SQL('total_num') 的作用是給臨時命名的查詢字符串,當(dāng)作臨時字段使用,支持,desc()等API
4. peewee的API是高仿SQL寫的,方便使用者。因此我們最好同步SQL的語法規(guī)范,按如下順序:
where > group_by > having > order_by
聚合原理
一會講peewee的fn聚合原理會涉及到 __getattr__(),如果你不了解,可以看下我之前寫過的文章。
https://segmentfault.com/a/11...
聚合原理如下: (以上面分組的 fn.count() 為例)
fn是我事先導(dǎo)入進(jìn)來的(開篇我就說過 from peewee import * )就導(dǎo)入了一切(建議練習(xí)使用)
fn可以使用聚合操作,我看了一下源碼:講解下思路(不一定特別正確):
fn是 Function類實例的出的對象
Function() 定義了 __getattr__方法,(__getattr__開頭我已經(jīng)給鏈接了,不懂的可以傳送)
當(dāng)你使用 fn.xx() :
xx 就會被當(dāng)作字符串傳到 __getattr__ ,
__getattr__里面用裝飾器模式,將你 xx 這個字符串。
經(jīng)過一系列操作,映射為同名的SQL語句 (這系列操作包括大小寫轉(zhuǎn)換等)
所以你用 fn.count 和 fn.CoUNt 是一樣的
說到底 fn.xx() , 的意思就是 fn 把 xx 當(dāng)作字符串映射到SQL語句,能映射到就能執(zhí)行
常用fn聚合函數(shù)
fn.count()
統(tǒng)計總?cè)祟^數(shù):
for owner in Owner.select(fn.count(Owner.name).alias('total_num')):
print(owner.total_num)
fn.lower() / fn.upper()
名字轉(zhuǎn)小寫/大寫(注意是臨時轉(zhuǎn),并沒有真的轉(zhuǎn)),并查詢出來:
for owner in Owner.select(fn.Upper(Owner.name).alias('lower_name')):
print(owner.lower_name)
fn.sum()
年齡求和:
for owner in Owner.select(fn.sum(Owner.age).alias('sum_age')):
print(owner.sum_age)
fn.avg()
求平均年齡:
for owner in Owner.select(fn.avg(Owner.age).alias('avg_age')):
print(owner.avg_age)
fn.min() / fn.max()
找出最小/最大年齡:
for owner in Owner.select(fn.max(Owner.age).alias('max_age')):
print(owner.max_age)
fn.rand()
通常用于亂序查詢 (默認(rèn)是升序的哦):
for owner in Owner.select().order_by()
print(owner.name)
關(guān)聯(lián)查詢前提數(shù)據(jù)準(zhǔn)備
from peewee import *
mysql_db = MySQLDatabase('你的數(shù)據(jù)庫名', user='你的用戶名', password='你的密碼',
host='你的IP', port=3306, charset='utf8mb4')
class BaseModel(Model):
class Meta:
database = mysql_db
class Teacher(BaseModel):
teacher_name = CharField()
class Student(BaseModel):
student_name = CharField()
teacher = ForeignKeyField(Teacher, backref='student')
class Course(BaseModel):
course_name = CharField()
teacher = ForeignKeyField(Teacher, backref='course')
student = ForeignKeyField(Student, backref='course')
mysql_db.create_tables([Teacher, Student, Course])
data = (
('Tom', ('stu1', 'stu2'), ('Chinese',)),
('Jerry', ('stu3', 'stu4'), ('English',)),
)
for teacher_name, stu_obj, course_obj in data:
teacher = Teacher.create(teacher_name=teacher_name)
for student_name in stu_obj:
student = Student.create(student_name=student_name, teacher=teacher)
for course_name in course_obj:
Course.create(teacher=teacher, student=student, course_name=course_name)
關(guān)聯(lián)查詢
方式1:join (連接順序 Teacer -> Student , Student -> Course)
# 注意: 你不用寫 on ,因為peewee會自動幫你配對
query = Teacher.select(Teacher.teacher_name, Student.student_name, Course.course_name) \
.join(Student, JOIN.LEFT_OUTER). \ # Teacer -> Student
join(Course, JOIN.LEFT_OUTER) \ # Student -> Course
.dicts()
for obj in query:
print(f"教師:{obj['teacher_name']},學(xué)生:{obj['student_name']},課程:{obj['course_name']}")
方式2:switch (連接順序 Teacer -> Student , Teacher -> Course)
# 說明,我給的數(shù)據(jù)例子,可能并不適用這種方式的語義,只是單純拋出語法。
query = Teacher.select(Teacher.teacher_name, Student.student_name, Course.course_name) \
.join(Student) \ # Teacher -> Student
.switch(Student) \ # 注意這里,把join上下文權(quán)力還給了 Teacher
.join(Course, JOIN.LEFT_OUTER) \ # Teacher -> Course
.dicts()
for obj in query:
print(f"教師:{obj['teacher_name']},學(xué)生:{obj['student_name']},課程:{obj['course_name']}")
方式3:join_from(和方式2是一樣的效果,只不過語法書寫有些變化)
query = Teacher.select(Teacher.teacher_name, Student.student_name, Course.course_name) \
.join_from(Teacher, Student) \ # 注意這里,直接指明連接首尾對象
.join_from(Teacher, Course, JOIN.LEFT_OUTER) \ # 注意這里,直接指明連接首尾對象
.dicts()
for obj in query:
print(f"教師:{obj['teacher_name']},學(xué)生:{obj['student_name']},課程:{obj['course_name']}")
方式4:關(guān)聯(lián)子查詢
(說明:關(guān)聯(lián)子查詢的意思就是:之前我們join的是個表,而現(xiàn)在join后面不是表,而是子查詢。)
SQL版本如下:
SELECT `t1`.`id`, `t1`.`student_name`, `t1`.`teacher_id`, `t2`.`stu_count`
FROM `student` AS `t1`
INNER JOIN (
SELECT `t1`.`teacher_id` AS `new_teacher`, count(`t1`.`student_name`) AS `stu_count`
FROM `student` AS `t1` GROUP BY `t1`.`teacher_id`
) AS `t2`
ON (`t2`.`new_teacher` = `t1`.`teacher_id`
peewee版本如下:
# 子查詢(以學(xué)生的老師外鍵分組,統(tǒng)計每個老師的學(xué)生個數(shù))
temp_query = Student.select(
Student.teacher.alias('new_teacher'), # 記住這個改名
fn.count(Student.student_name).alias('stu_count') # 統(tǒng)計學(xué)生,記住別名,照應(yīng)下面.c語法
).group_by(Student.teacher) # 以學(xué)生表中的老師外鍵分組
# 主查詢
query = Student.select(
Student, # select 傳整個類代表,查詢
temp_query.c.stu_count # 指定查詢字段為 子查詢的字段, 所以需要用 .c 語法來指定
).join(
temp_query, # 關(guān)聯(lián) 子查詢
on=(temp_query.c.new_teacher == Student.teacher) # 關(guān)聯(lián)條件
).dicts()
for obj in query:
print(obj)
方式5: 無外鍵關(guān)聯(lián)查詢 (無外鍵也可以join哦,自己指定on就行了)
重新建立一個無外鍵的表,并插入數(shù)據(jù)
class Teacher1(BaseModel):
teacher_name = CharField()
class Student1(BaseModel):
student_name = CharField()
teacher_id = IntegerField()
mysql_db.create_tables([Teacher1, Student1])
data = (
('Tom', ('zhang1', 1)),
('Jerry', ('zhang2', 2)),
)
for teacher_name, student_obj in data:
Teacher1.create(teacher_name=teacher_name)
student_name, teacher_id = student_obj
Student1.create(student_name=student_name, teacher_id=teacher_id)
現(xiàn)在我們實現(xiàn)無外鍵關(guān)聯(lián)查詢:
"""查詢學(xué)生 對應(yīng)老師 的姓名"""
query = Student1.select(
Student1, # 上面其實已經(jīng)講過了,select里面?zhèn)髂匙侄尉筒槟匙侄危瑐黝惥筒樗凶侄? Teacher1 # 因為后面是join了,但peewee默認(rèn)是不列出 Teacher1這張外表的。
# 所以需要手動指定Teacher1 (如果我們想查Teacher1表信息,這個必須指定)
).join(
Teacher1, # 雖然無外鍵關(guān)聯(lián),但是依舊是可以join的(原生SQL也如此的)
on=(Student1.teacher_id==Teacher1.id) # 這個 on必須手動指定了
# 強調(diào)一下,有外鍵的時候,peewee會自動為我們做on操作,所以我們不需要指定
# 但是,這個是無外鍵關(guān)聯(lián)的情況,所以必須手動指定on, 不然找不著
).dicts()
for obj in query:
print(obj)
方式6: 自關(guān)聯(lián)查詢
# 新定義個表
class Category(Model):
name = CharField()
parent = ForeignKeyField('self', backref='children')
# 注意一下,外鍵引用這里寫的是 "self" ,這是是固定字符串哦 ;backref是反向引用,說過了。
# 創(chuàng)建表
mysql_db.create_tables([Category])
# 插入數(shù)據(jù)
data = ("son", ("father", ("grandfather", None)))
def insert_self(data):
if data[1]:
parent = insert_self(data[1])
return Category.create(name=data[0], parent=parent)
return Category.create(name=data[0])
insert_self(data) # 這是我自己定義的一個遞歸插入的方式。。可能有點low
# 可能有點繞,我把插入結(jié)果直接貼出來吧
mysql> select * from category;
+----+-------------+-----------+
| id | name | parent_id |
+----+-------------+-----------+
| 1 | grandfather | NULL |
| 2 | father | 1 |
| 3 | son | 2 |
+----+-------------+-----------+
# 開始查詢
Parent = Category.alias() # 這是表的(臨時查詢)改名操作。 接受參數(shù) Parent 即為表名
# 因為自關(guān)聯(lián)嘛,自己和自己,復(fù)制一份(改名就相當(dāng)于臨時自我拷貝)
query = Category.select(
Category,
Parent
).join(
Parent,
join_type=JOIN.LEFT_OUTER, # 因為頂部類為空,并且默認(rèn)連接方式為 inner
# 所以最頂端的數(shù)據(jù)(grandfather)是查不到的
# 所以查所有數(shù)據(jù)需要用 ==> 左連接
# on=(Parent.id == Category.parent) # 官檔說 on 需要指定,但我試了,不寫也能關(guān)聯(lián)上
).dicts()
至此,關(guān)聯(lián)查詢操作介紹結(jié)束!
接下來對以上六種全部方式的做一些強調(diào)和說明:
你可以看見我之前六種方式都是用的dicts(),返回的是類字典格式。(此方式的字段名符合SQL規(guī)范)
當(dāng)然你也可以以類對象的格式返回,(這種方式麻煩一點,我推薦還是用 dicts() )
如果想返回類對象,見如下代碼(下面這種方式多了點東西):
query = Teacher.select(Teacher.teacher_name, Student.student_name, Course.course_name) \
.join_from(Teacher, Student) \
.join_from(Teacher, Course, JOIN.LEFT_OUTER) # 注意,我沒有用dicts()
for obj in query:
print(obj.teacher_name) # 這行應(yīng)該沒問題吧。本身Teacher就有teacher_name字段
# 注意了,按SQL原理來說,既然已經(jīng)做了join查詢,那么查詢結(jié)果就應(yīng)該直接具有所有表的字段的
# 按理說 的確是這樣,但是peewee,需要我們先指定多表的表名,在跟寫多表的字段,正確寫法如下
print(obj.student.student_name) # 而不是 obj.student_name直接調(diào)用
print(obj.course.course_name) # 而不是 obj.course_name直接調(diào)用
# 先埋個點, 如果你看到下面的 N+1查詢問題的實例代碼和這個有點像。
# 但我直接說了, 這個是用了預(yù)先join()的, 所以涉及到外表查詢后,不會觸發(fā)額外的外表查詢
# 自然也不會出現(xiàn)N+1的情況。
# 但如果你沒有用join,但查詢中涉及了外表,那么就會觸發(fā)額外的外表查詢,就會出現(xiàn)N+1的情況。
關(guān)聯(lián)N+1查詢問題:
什么是N+1 query? 看下面例子:
# 數(shù)據(jù)沒有什么特殊的,假設(shè), 老師 和 學(xué)生的關(guān)系是一對多(注意,我們用了外鍵)。
class Teacher(BaseModel):
teacher_name = CharField()
class Student(BaseModel):
student_name = CharField()
teacher_id = ForeignKeyField(Teacher, backref='student')
# 查詢
teachers = Teacher.select() # 這是 1 次, 查出N個數(shù)據(jù)
for teacher_obj in teachers:
for student in teacher_obj.student: # 這是 N 次循環(huán)(N代表查詢的數(shù)據(jù))
print(student.student_name)
# 每涉及一個外表屬性,都需要對外表進(jìn)行額外的查詢, 額外N次
# 所以你可以看到, 我們總共查詢 1+N次, 這就是 N+1 查詢。
# (其實我們先做個 表連接,查詢一次就可解決問題了。。 這 N+1這種方式 屬實弟弟)
# 下面我們介紹2種避免 N+1 的方式
peewee解決N+1問題有兩種方式:
方式1:(join)
用 join 先連接好,再查詢(前面說了6種方式的join,總有一種符合你需求的)
因為 peewee是支持用戶顯示調(diào)用join語法的, 所以 join是個 特別好的解決 N+1 的問題
方式2: (peewee的prefetch)
# 當(dāng)然,除了 join,你也可以使用peewee提供的下面這種方式
# 乍眼一看,你會發(fā)現(xiàn)和我們上面寫的 n+1 查詢方式的例子差不多,不一樣,你仔細(xì)看看
teacher = Teacher.select() # 先預(yù)先把 主表 查出來
student = Student.select() # 先預(yù)先把 從表 查出來
teacher_and_student = prefetch(teacher, student) # 使用 prefetch方法 (關(guān)鍵)
for teacher in teacher_and_student: # 下面就和N+1一樣了
print(teacher.teacher_name)
for student in teacher.student:
print(student.student_name)
說明:
0. prefetch, 原理是,將有外鍵關(guān)系的主從表,隱式"一次性"取出來。"需要時"按需分配即可。
1. 使用prefetch先要把,有外鍵關(guān)聯(lián)的主從表查出來(注意,"必須必須要有外鍵,不然不好使")
2. prefetch(主表,從表) # 傳進(jìn)去就行,peewee會自動幫我們根據(jù)外鍵找關(guān)系
3. 然后正常 以外鍵字段 為橋梁 查其他表的信息即可
4. (
題外話,djnago也有類似的prefetch功能,(反正都是避免n+1,優(yōu)化ORM查詢)
貌似給外鍵字段 設(shè)置select_related() 和 prefetch_related() 屬性
)
未結(jié)束語
本篇主要講了,CRUD, 特別是針對查詢做了大篇幅說明。
我還會有下一篇來介紹peewee的擴展功能。
上一篇傳送門:https://segmentfault.com/a/11...
下一篇傳送門:https://segmentfault.com/a/11...
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
