上篇地址:https://segmentfault.com/a/11...
雖然上一篇,已經說明,但仍是強調一下,peewee是 python-ORM(只支持 MySQL,Sqlite,postgresql )
雖然ORM能夠與多種數據庫無縫相接,而且兼容性好, 可是某些細微的語法並非數據庫共有的。
我用MySQL, 因此下面說的都是基於MySQL(其餘2種數據庫也差不了多少, 99%是同樣的)
總官檔地址:http://docs.peewee-orm.com/en...
官方Github地址:https://github.com/coleifer/p...php
方式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, 返回結果都是模型實例"(也就意味着建立了一個實例)" # 而本方式,返回結果是 最新插入的主鍵值"(也就意味着不會建立實例)"
若是存在外鍵關聯,假如存在 Pet類 引用的 Owner的主鍵,插入數據方式有2種:html
方式1: 用新建對象變量傳值: lin = Owner.create(name='lin', age=20) tom1 = Pet.create(name='Tom', age=1, owner=lin) # 注意 owner = lin 方式2: 手動維護主鍵 id,經過主鍵傳值(或者經過查詢id): lin = Owner.create(id=100, name='lin', age=20) # id本身給的值爲 100 tom1 = Pet.create(name='Tom', age=1, owner=100) # 注意 owner=100
插入多條數據:(官檔有好幾種方法,我只說最提倡,最快速的方法(好處就是一次性提交,不用循環))python
方式1: """ 注意格式 [ {},{},{} ] 每一個字典,對應一條記錄。 """ data = [ {'name': 'Alice', 'age': 18}, {'name': 'Jack', 'age': 17}, ] Owner.insert_many(data).execute() 方式2: (就是不用在數據中都指定鍵了,方便一點) """ 注意格式 [ (),(),() ] 每一個元組,對應一條記錄。 """ data = [ ('Alice', 18), ('Jack', 17), ] User.insert_many(data, fields=[Owner.name, Owner.age]).execute() 注意一下:尾部都必需要帶一個execute()
若是數據量過大,可能會出現OOM等問題。你能夠手動分批,可是 peewee 給咱們提供了成品的 apimysql
from peewee import chunked with mysql_db.atomic(): # 官檔建議用事務包裹 for batch in chunked(data, 100): # 一次100條, chunked() 返回的是可迭代對象 Owner.insert_many(batch).execute()
防止數據重複插入的2種辦法(或者防止設置了主鍵,重複插入拋出異常,致使程序沒法運行):git
方法1: INGORE關鍵字 (這種方式是若是衝突了,就自動忽略) 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; # 若是衝突了,能夠從新設置值 peewee: Owner.insert(name='lin', age=30).on_conflict( preserve=[Owner.name, Owner.age], # 若衝突,你想保留不變的字段 update={Owner.name: 'lin', Owner.age: 30} # 若衝突,你想更新什麼 ).execute() # 注: preserve 和 update 按狀況用,通常設置一個用就好了。
方法1: php = Owner.get(name='PHP') # 獲取單條對象 php.delete_instance() # 注意: delete_instance() 只能刪除單條對象, 若是用select()查出來的,須要遍歷才能刪 方法2: Owner.delete().where(Owner.name == 'lin').execute() # 注意這種方法和添加相似, 最後也必須有個 execute()
方式1:(不推薦) owner= 查詢單個對象結果 owner.name = 'Pack' owner.name = 50 owner.save() # 你能夠看見,咱們還須要手動調用一下save() 方式2:(推薦) query = Owner.update(name='Pack', age=50).where(Owner.name == 'Zhang') query.execute()
"""存在則返回原有對象, 不存在則拋error""" one_owner = Owner.get(name='Zhang2') print(one_woner.age) 擴展版1: get_or_create """存在則返回原有對象。不存在則插入數據,並返回新對象""" 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')
正常查詢全部數據github
owners = Owner.select() # 返回結果 owners 是對象集合,須要遍歷 for owner in owners: # owner 是每一個對象(對應每條記錄) print(woner.name)
固然你能夠在查詢後轉爲 python 類dict格式:sql
owners = Owner.select().dicts() # 返回結果 owners 是 "類字典對象集合" for owner in owners: # owner是每一個字典對象, (它 對應每條記錄) print(owner['name']) # 字典語法取值,懂了吧,很少說了。
上面的查詢若是在數據大量的狀況下可能會致使OOM,所以可轉爲迭代:數據庫
"""再每一個查詢的最後加上 .iterator() 便可""" eg: owners = Owner.select().iterator() owners = Owner.select().dicts().iterator()
首先我先強調個,"MySQL是否區分大小寫" 的事:segmentfault
MySQL5.7+,是區分大小寫的; (MySQL8,和 MariaDB 我沒試, 應該和 5.7是同樣的) 但這個區分大小寫 僅僅僅僅僅僅 是 針對於 SQL語句的表名 "" 引號外面的(就是非字符串語法) 舉個例子: 現有一表,名叫 owner desc owner # 正確 desc OWNER # 錯誤,表不存在 這種狀況下,由於不涉及字符串的 "" 引號操做,因此是嚴格區分大小寫的。 "而引號裏面" (其實就是涉及字符串)的數據語法,是 不區分 大小寫的。 舉個例子(由於下面例子都有 "" 字符串操做,因此都 不區分 大小寫): SQL: 查詢例子: select * from owner where name='zHang' select * from owner where name='ZHANG' 他們倆查詢的是同一個數據。 插入例子: insert into owner values("zhaNg") insert into owner values("zhang") 他們倆 插入的 也是同一個數據 peewee: 查詢例子: ...where(name="zhang") ...where(name="ZHaNg") 他們倆查詢的是 同一個數據。 插入例子: ...insert({'name':'Zhang') ...insert({'name': 'zhANG') 他們倆 插入的 也是同一個數據
官檔-條件操做符:http://docs.peewee-orm.com/en...
上邊的鏈接是官檔操做符大全,下面我把部分經常使用摘出來講一下。api
與或非:
與:& 模型類.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的數據 (前閉後閉) 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)
以..開頭 & 以..結尾
以..開頭: startswith 以..結尾: endswith # 把以 ali 開頭的 都查詢出來 for owner in Owner.select().where(Owner.name.startswith('ali')): print(owner.name)
模糊查詢:
# 將包含 li 字符串的數據查詢出來 for owner in Owner.select().where(Owner.name.contains('li')): print(owner.name)
正則查詢:
這個就有意思了。前面咱們強調過,MySQL帶引號字符串是不區分大小寫的。 而正則功能提供給咱們區分大小寫的API。(這是個特例,只有正則區分大小寫的功能。記住) 例子條件: 假如咱們有一個數據 name爲 Alice regexp: 嚴格區分大小寫的正則 # 用的是 regexp,區分大小寫, 條件給的是 al小寫, 因此固然 查不出來,返回空 for owner in Owner.select().where(Owner.name.regexp('al*')): print(owner.name) iregexp:不區分大小寫的正則 # 用的是 iregexp, 不區分大小寫。 所以即便 你給 al小寫, 也可以將 Alice查出來。 for owner in Owner.select().where(Owner.name.iregexp('al*')): print(owner.name)
print(MyModel.select().count())
"""跳過前2行,從第2+1行開始,取1條, 其實取出的就是第3行""" for x in Owner.select().offset(2).limit(1).dicts(): print(x)
""" 1. paginate 第1個參數爲 第幾頁 2. paginate 第2個參數爲 一頁幾個數據 3. paginate會自動根據查詢的全部記錄總數 和 你傳的 兩個 參數來爲你自動分頁 """ for obj in MyModel.select().paginate(1,3).dicts(): # 第一頁,每頁三個數據 print(obj) # peewee提供給咱們分頁就這麼多,想要更多需求,須要咱們本身發散思惟。 # 下面是我本身粗略寫的一個笨拙的分頁。。能夠參考下。。 def page(document_count=None, per_page_size=None, start_page=1): page_count = (document_count // per_page_size) # 整除的頁數(可能有殘頁) is_rest = (document_count % per_page_size) # 總數/每頁數:是否能除盡 # 除盡表明整頁直接返回,除不盡有殘頁 ,頁碼+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() # 先獲取記錄總數 for obj in page(document_count=document_count, per_page_size=3, start_page=1): print(obj) # 若是你有需求分頁切片或索引, 那麼你能夠封裝成類,而後實現 __getitem__ 方法
document_count = MyModel.select().count()
for obj in page(document_count=document_count, per_page_size=3, start_page=1):
print(obj)
# 默認升序 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)
# 用姓名分組,統計人頭數大於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}的 人數爲{owner.total_num}個') 分組注意事項,說幾點: 1. 分組操做,和SQL的group by同樣, group by後面寫了什麼字段, 前面select同時也必須包含 2. .alias('統計結果字段名'),是給統計後的結果起一個新字段名。 3. SQL('total_num') 的做用是給臨時命名的查詢字符串,看成臨時字段使用,支持,desc()等API 4. peewee的API是高仿SQL寫的,方便使用者。所以咱們最好同步SQL的語法規範,按以下順序: where > group_by > having > order_by
一會講peewee的fn聚合原理會涉及到 __getattr__(),若是你不瞭解,能夠看下我以前寫過的文章。
https://segmentfault.com/a/11...
聚合原理以下: (以上面分組的 fn.count() 爲例) fn是我事先導入進來的(開篇我就說過 from peewee import * )就導入了一切(建議練習使用) fn可使用聚合操做,我看了一下源碼:講解下思路(不必定特別正確): fn是 Function類實例的出的對象 Function() 定義了 __getattr__方法,(__getattr__開頭我已經給連接了,不懂的能夠傳送) 當你使用 fn.xx() : xx 就會被看成字符串傳到 __getattr__ , __getattr__裏面用裝飾器模式,將你 xx 這個字符串。 通過一系列操做,映射爲同名的SQL語句 (這系列操做包括大小寫轉換等) 因此你用 fn.count 和 fn.CoUNt 是同樣的 說到底 fn.xx() , 的意思就是 fn 把 xx 看成字符串映射到SQL語句,能映射到就能執行
fn.count() 統計總人頭數: for owner in Owner.select(fn.count(Owner.name).alias('total_num')): print(owner.total_num) fn.lower() / fn.upper() 名字轉小寫/大寫(注意是臨時轉,並無真的轉),並查詢出來: 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() 一般用於亂序查詢 (默認是升序的哦): for owner in Owner.select().order_by() print(owner.name)
from peewee import * mysql_db = MySQLDatabase('你的數據庫名', 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)
方式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']},學生:{obj['student_name']},課程:{obj['course_name']}")
方式2:switch (鏈接順序 Teacer -> Student , Teacher -> Course)
# 說明,我給的數據例子,可能並不適用這種方式的語義,只是單純拋出語法。 query = Teacher.select(Teacher.teacher_name, Student.student_name, Course.course_name) \ .join(Student) \ # Teacher -> Student .switch(Student) \ # 注意這裏,把join上下文權力還給了 Teacher .join(Course, JOIN.LEFT_OUTER) \ # Teacher -> Course .dicts() for obj in query: print(f"教師:{obj['teacher_name']},學生:{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']},學生:{obj['student_name']},課程:{obj['course_name']}")
方式4:關聯子查詢
(說明:關聯子查詢的意思就是:以前咱們join的是個表,而如今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版本以下:
# 子查詢(以學生的老師外鍵分組,統計每一個老師的學生個數) temp_query = Student.select( Student.teacher.alias('new_teacher'), # 記住這個更名 fn.count(Student.student_name).alias('stu_count') # 統計學生,記住別名,照應下面.c語法 ).group_by(Student.teacher) # 以學生表中的老師外鍵分組 # 主查詢 query = Student.select( Student, # select 傳整個類表明,查詢 temp_query.c.stu_count # 指定查詢字段爲 子查詢的字段, 因此須要用 .c 語法來指定 ).join( temp_query, # 關聯 子查詢 on=(temp_query.c.new_teacher == Student.teacher) # 關聯條件 ).dicts() for obj in query: print(obj)
方式5: 無外鍵關聯查詢 (無外鍵也能夠join哦,本身指定on就好了)
從新創建一個無外鍵的表,並插入數據
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)
如今咱們實現無外鍵關聯查詢:
"""查詢學生 對應老師 的姓名""" query = Student1.select( Student1, # 上面其實已經講過了,select裏面傳某字段就查某字段,傳類就查全部字段 Teacher1 # 由於後面是join了,但peewee默認是不列出 Teacher1這張外表的。 # 因此須要手動指定Teacher1 (若是咱們想查Teacher1表信息,這個必須指定) ).join( Teacher1, # 雖然無外鍵關聯,可是依舊是能夠join的(原生SQL也如此的) on=(Student1.teacher_id==Teacher1.id) # 這個 on必須手動指定了 # 強調一下,有外鍵的時候,peewee會自動爲咱們作on操做,因此咱們不須要指定 # 可是,這個是無外鍵關聯的狀況,因此必須手動指定on, 否則找不着 ).dicts() for obj in query: print(obj)
方式6: 自關聯查詢
# 新定義個表 class Category(Model): name = CharField() parent = ForeignKeyField('self', backref='children') # 注意一下,外鍵引用這裏寫的是 "self" ,這是是固定字符串哦 ;backref是反向引用,說過了。 # 建立表 mysql_db.create_tables([Category]) # 插入數據 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 # 可能有點繞,我把插入結果直接貼出來吧 mysql> select * from category; +----+-------------+-----------+ | id | name | parent_id | +----+-------------+-----------+ | 1 | grandfather | NULL | | 2 | father | 1 | | 3 | son | 2 | +----+-------------+-----------+ # 開始查詢 Parent = Category.alias() # 這是表的(臨時查詢)更名操做。 接受參數 Parent 即爲表名 # 由於自關聯嘛,本身和本身,複製一份(更名就至關於臨時自我拷貝) query = Category.select( Category, Parent ).join( Parent, join_type=JOIN.LEFT_OUTER, # 由於頂部類爲空,而且默認鏈接方式爲 inner # 因此最頂端的數據(grandfather)是查不到的 # 因此查全部數據須要用 ==> 左鏈接 # on=(Parent.id == Category.parent) # 官檔說 on 須要指定,但我試了,不寫也能關聯上 ).dicts()
至此,關聯查詢操做介紹結束!
接下來對以上六種所有方式的作一些強調和說明:
你能夠看見我以前六種方式都是用的dicts(),返回的是類字典格式。(此方式的字段名符合SQL規範) 固然你也能夠以類對象的格式返回,(這種方式麻煩一點,我推薦仍是用 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) # 這行應該沒問題吧。自己Teacher就有teacher_name字段 # 注意了,按SQL原理來講,既然已經作了join查詢,那麼查詢結果就應該直接具備全部表的字段的 # 按理說 的確是這樣,可是peewee,須要咱們先指定多表的表名,在跟寫多表的字段,正確寫法以下 print(obj.student.student_name) # 而不是 obj.student_name直接調用 print(obj.course.course_name) # 而不是 obj.course_name直接調用 # 先埋個點, 若是你看到下面的 N+1查詢問題的實例代碼和這個有點像。 # 但我直接說了, 這個是用了預先join()的, 因此涉及到外表查詢後,不會觸發額外的外表查詢 # 天然也不會出現N+1的狀況。 # 但若是你沒有用join,但查詢中涉及了外表,那麼就會觸發額外的外表查詢,就會出現N+1的狀況。
什麼是N+1 query? 看下面例子:
# 數據沒有什麼特殊的,假設, 老師 和 學生的關係是一對多(注意,咱們用了外鍵)。 class Teacher(BaseModel): teacher_name = CharField() class Student(BaseModel): student_name = CharField() teacher_id = ForeignKeyField(Teacher, backref='student') # 查詢 teachers = Teacher.select() # 這是 1 次, 查出N個數據 for teacher_obj in teachers: for student in teacher_obj.student: # 這是 N 次循環(N表明查詢的數據) print(student.student_name) # 每涉及一個外表屬性,都須要對外表進行額外的查詢, 額外N次 # 因此你能夠看到, 咱們總共查詢 1+N次, 這就是 N+1 查詢。 # (其實咱們先作個 錶鏈接,查詢一次就可解決問題了。。 這 N+1這種方式 屬實弟弟) # 下面咱們介紹2種避免 N+1 的方式
peewee解決N+1問題有兩種方式:
方式1:(join)
用 join 先鏈接好,再查詢(前面說了6種方式的join,總有一種符合你需求的) 由於 peewee是支持用戶顯示調用join語法的, 因此 join是個 特別好的解決 N+1 的問題
方式2: (peewee的prefetch)
# 固然,除了 join,你也可使用peewee提供的下面這種方式 # 乍眼一看,你會發現和咱們上面寫的 n+1 查詢方式的例子差很少,不同,你仔細看看 teacher = Teacher.select() # 先預先把 主表 查出來 student = Student.select() # 先預先把 從表 查出來 teacher_and_student = prefetch(teacher, student) # 使用 prefetch方法 (關鍵) for teacher in teacher_and_student: # 下面就和N+1同樣了 print(teacher.teacher_name) for student in teacher.student: print(student.student_name) 說明: 0. prefetch, 原理是,將有外鍵關係的主從表,隱式"一次性"取出來。"須要時"按需分配便可。 1. 使用prefetch先要把,有外鍵關聯的主從表查出來(注意,"必須必需要有外鍵,否則很差使") 2. prefetch(主表,從表) # 傳進去就行,peewee會自動幫咱們根據外鍵找關係 3. 而後正常 之外鍵字段 爲橋樑 查其餘表的信息便可 4. ( 題外話,djnago也有相似的prefetch功能,(反正都是避免n+1,優化ORM查詢) 貌似給外鍵字段 設置select_related() 和 prefetch_related() 屬性 )
本篇主要講了,CRUD, 特別是針對查詢作了大篇幅說明。
我還會有下一篇來介紹peewee的擴展功能。
上一篇傳送門:https://segmentfault.com/a/11...
下一篇傳送門:https://segmentfault.com/a/11...