# Python Flask 高級編程視頻筆記
[TOC]
### flask 路由的基本原理
`add_url_route`可以傳入`rule`、`view_function`、`endpoint`,如果不傳`endpoint`,會默認將`view_function`的名字作為`endpoint`。
`add_url_route`會將最終的`rule`存放在`url_map`里面,視圖函數存放在`view_function`的字典里面。
> `view_functions`為字典,鍵為`endpoint`,值為視圖函數
* 首先`url_map`這個 `map`對象里面必須由我們的 `url` -> `search endpoint`的指向;
* 同時`view_functions`里面必須記錄`search endpoint`所指向的視圖函數。
這樣當一個請求進入 `flask`之后才能根據 `url` 順利的找到對應的視圖函數,這就是 `flask`路由的基本原理。
### 循環引用圖解

### 1\. **拆分視圖函數**到單獨的模塊中去
將視圖函數從主執行文件分離出來時,不能直接導入flask的核心對象,導致不能使用flask核心對象來注冊視圖函數的路由
### 2\. 只有由**視圖函數**或者`http`請求觸發的`request`才能得到`get`返回的結果數據
* `flask`默認`request`類型是`localproxy`(本地代理)

### 3\. **驗證層**
**驗證層**:使用`wtforms`進行參數校驗
### 4\. **MVC**模型
* **MVC**模型里絕對不是只有數據,如果只有數據的話,那只能算作數據表
* **MVC**里的*M*是一個業務模型,必須定義很多操作一個個數據字段的業務方法
> **經典面試問題**: 業務邏輯應該寫在**MVC**里的哪一層? 業務邏輯最好應該是在**M**(*model*)層編寫
### 5\. **ORM**的含義
**ORM**:關系型數據庫和實體間做映射,操作對象的屬性和方法,跳過SQL語句
對象關系映射(英語:`Object Relational Mapping`,簡稱`ORM`,或`O/RM`,或`O/R mapping`),用于實現面向對象編程語言里不同類型系統的數據之間的轉換。其實是創建了一個可在編程語言里使用的`虛擬對象數據庫`。`Object`是可以繼承的,是可以使用接口的,而`Relation`沒有這個概念。 
### 6\. 最基本的數據結構
* **棧**:**后進先出**
* **隊列**:**先進先出**
### 7\. **`with`語句**
`with`語句:上下文管理器可以使用`with`語句
> 實現了上下文協議的對象就可以使用`with`語句 實現了上下文協議的對象通常稱為**上下文管理器** 一個對象只要實現了`__enter__`和`__exit__`兩個方法,就是實現了**上下文協議** 上下文表達式(with后面的表達式)必須返回一個上下文管理器
示例1:
~~~
1class A:2 ? ?def __enter__(self):3 ? ? ? ?a = 14?5 ? ?def __exit__(self, exc_type, exc_val, exc_tb):6 ? ? ? ?b = 27?8with A() as obj_A: ?9 ? ?pass
~~~
* `obj_A` 是 `None`;`A()`直接實例化 `A`,返回的是上下文管理器對象
* as 語句后面的變量不是上下文管理器
* `__enter__` 方法所返回的值會賦給 as 語句后面的變量
~~~
1class A:2 ? ?def __enter__(self):3 ? ? ? ?a = 14 ? ? ? ?return a5?6 ? ?def __exit__(self, exc_type, exc_val, exc_tb):7 ? ? ? ?b = 28?9with A() as obj_A: ?# obj_A :110 ? ?pass
~~~
示例2:`文件讀寫`
~~~
1try:2 ? ?f = open(r'D:\t.txt')3 ? ?print(f.read())4finally:5 ? ?f.close()
~~~
使用**with語句**改寫
~~~
1with open(r'D:\t.txt') as f:2 ? ?print(f.read())
~~~
示例3:`with語句處理異常`
~~~
1class MyResource:2 ? ?def __enter__(self):3 ? ? ? ?print('connect to resource')4 ? ? ? ?return self5?6 ? ?def __exit__(self, exc_type, exc_val, exc_tb):7 ? ? ? ?if exc_tb:8 ? ? ? ? ? ?print('process exception')9 ? ? ? ?else:10 ? ? ? ? ? ?print('no exception')11 ? ? ? ?print('close resource connection')12 ? ? ? ?# return True ? ? # 返回 True 表示已經在 __exit__ 內部處理過異常,不需要在外部處理13 ? ? ? ?return False ? ?# 返回 False 表示沒有在 __exit__ 內部處理異常,需要在外部處理14 ? ? ? ?# 如果 __exit__ 沒有返回,則默認返回 False15?16 ? ?def query(self):17 ? ? ? ?print('query data')18?19?20try:21 ? ?with MyResource() as resource:22 ? ? ? ?1/023 ? ? ? ?resource.query()24except Exception as ex:25 ? ?print('with語句出現異常')26 ? ?pass
~~~
### 8.操作數據庫的流程
* 連接數據庫
* `flask-sqlalchemy`連接數據庫
~~~
1SQLALCHEMY_DATABASE_URI = 'mysql+cymysql://username:password@localhost:3306/fisher?charset=utf8'
~~~
參數解釋:
`cymysql`:`mysql`數據庫的鏈接驅動
`username:password@localhost:3306`:用戶名:密碼@服務器地址:端口
`fisher`:數據庫名稱
`charset=utf8`:指定數據庫的編碼方式為`utf8`
> 采坑:
>
> **`mysql`數據庫必須指定編碼方式,否則`commit`的時候會出錯。** 時間:2018年10月18日20:39:43 就因此踩了坑,花費了三天的時間。剛開始沒有指定數據庫的編碼方式,結果在用戶之間發起魚漂的時候,儲存魚漂到數據庫的時候報如下錯誤:
>
> ~~~
> 1sqlalchemy.exc.InternalError: (cymysql.err.InternalError) (1366,...)
> ~~~
>
> 使用 `vscode`進行遠程調試,主要調試了提交數據庫的幾個操作:
>
> * 用戶注冊的時候,需要儲存用戶數據到數據庫,這類 `commit`沒問題,儲存的是 `user`表
>
> * 贈送數據的時候,需要將禮物(書籍)數據添加到數據庫,這類 `commit`沒問題,儲存的是 `gift`表
>
> * 添加心愿書籍的時候,需要儲存心愿,這類 `commit`沒問題,儲存的是 `wish`表
>
> * 儲存魚漂的時候,`commit`就會報錯,這類 `commit`儲存的是 `drift`表
>
>
> 查 `google`確實查到了是 `mysql`編碼的問題,
>
> * 嘗試1:修改 `mysql`編碼模式為 `utf8`,結果:無效
>
> * 嘗試2:修改已創建的 `fisher`數據庫的編碼模式為 `utf8`,結果:無效
>
> * 嘗試3:修改`mysql`連接方式`SQLALCHEMY_DATABASE_URI = 'mysql+cymysql://username:password@localhost:3306/fisher?charset=utf8'`,結果:無效
>
> * 嘗試4:修改
>
> * 嘗試4:刪除 `fisher`數據庫,重新讓 `sqlalchemy`建立數據表,結果:**有效**
>
>
> 原因嘛,猜測為`drift`表的編碼模式出現了問題。
>
> 至于為什么其他表的編碼模式沒問題,只有 `drift`這個搞不清楚,以后在捉摸吧。
* `SQL`操作
* 釋放資源
使用
* `try`
* `except`
* `finally`
無論出現什么異常,最終都會執行`final`語句,不會浪費資源,很優雅 另一種方式就是使用**with語句**
### 9\. 進程和線程
#### **進程**
進程:是競爭計算機資源的基本單位
* 每一個應用程序至少需要一個進程
* 進程是分配資源的
* 多進程管理的資源是不能相互訪問的
* **多進程**資源共享需要使用**進程通信技術**
#### **線程**
線程:是進程的一部分,一個進程可以有一個或多個線程
* 線程:利用 `cpu` 執行代碼
* 線程屬于進程
* 線程不擁有資源,但是可以訪問進程的資源
* 多線程可以更加充分的利用 `cpu` 的性能優勢
* 多個線程共享一個進程的資源,就會造成進程不安全
#### **GIL**
**全局解釋器鎖**(英語:`Global Interpreter Lock`,縮寫**GIL**)
* `python`解釋器
* `cpython`:有GIL
* `jpython`:無GIL
* **鎖**
* **細粒度鎖**:是程序員主動添加的
* **粗粒度鎖**:**GIL**,多核 `cpu` 只有 `1` 個線程執行,一定程度上保證了線程安全
* 還有特例情況(無法保證線程安全)
> 例: `a += 1`
>
> * 在 `python` 解釋器中會被翻譯成 `bytecode`(字節碼),`a += 1` 可能會被翻譯成多段 `bytecode`,如果解釋器正在執行多段 `bytecode` 其中一段的時候被掛起去執行第二個線程,等第二個線程執行完之后再回來,接著執行 `a += 1` 剩下的幾段 `bytecode` 的時候就不能保證線程安全了。
>
> * 如果 `a += 1` 這段代碼的多段 `bytecode` 會一直執行完(不會中斷),則可以保證線程安全,但是**GIL**做不到,它只能認一段段的 `bytecode`,它是以 `bytecode` 為基本單位來執行的。
>
* `python`多線程到底是不是雞肋?
> node.js 是單進程、單線程的語言
* `cpu`密集型程序:一段代碼的大部分執行時間是消耗在 `cpu` 計算上的程序
* 例如:圓周率的計算、視頻的解碼
* `IO`密集型程序:一段代碼的大部分執行時間是消耗在**查詢數據庫**、**請求網絡資源**、**讀寫文件**等 `IO` 操作上的程序
* 現實中目前寫的絕大多數程序都是`IO`密集型的程序
* `python`多線程在 `IO` 密集型程序里具有一定意義,但是不適合`cpu`密集型程序
### 10\. 多線程
在多線程的情況下,多個請求傳入進來,如果用同一個變量名`request`來命名多個線程里的請求會造成混亂,該如何處理?
* 可以用字典來處理(字典是`python`中非常依賴的一種數據結構)
* `request = {key1:value1, key2:value2, key3:value3, ...}`
* 多線程的每個線程都有它的唯一標識`thread_key`
* 解決方案:`request = {thread_key1:Request1, thread_key2:Request2, thread_key3:Request3, ...}`,一個變量指向的是字典的數據結構,字典的內部包含不同的線程創建的不同的`Request`實例化對象
* 線程隔離
* 用不同的線程`id`號作為鍵,其實就是**線程隔離**
* 不同的線程在字典中有不同的狀態,各個線程的狀態都被保存在字典中,互不干擾
* **不同線程**操作**線程隔離**的對象時,互不影響
> 線程`t1`操作`L.a`(對象L的屬性a)與線程`t2`操作`L.a`(對象L的屬性a) 兩者是互不干擾的,各自進行各自的操作
#### 普通對象
* **不同線程操作普通對象的情況**
~~~
1import threading2import time3?4?5class A:6 ? ?b = 17?8?9my_obj = A()10?11?12def worker():13 ? ?# 新線程14 ? ?my_obj.b = 215?16?17new_t = threading.Thread(target=worker, name='my_test_thread')18new_t.start()19time.sleep(1)20?21?22# 主線程23print(my_obj.b)24# 新線程的修改影響到了主線程的打印結果,因為對象A只是普通對象,不是線程隔離的對象
~~~
#### 線程隔離對象
* **不同線程操作線程隔離對象的情況**
~~~
1import threading2import time3?4from werkzeug.local import Local5?6?7# class A(Local):8# ? ? b = 19?10?11my_obj = Local()12my_obj.b = 113?14?15def worker():16 ? ?# 新線程17 ? ?my_obj.b = 218 ? ?print('in new thread b is:' + str(my_obj.b))19?20?21new_t = threading.Thread(target=worker, name='my_test_thread')22new_t.start()23time.sleep(1)24?25?26# 主線程27print('in main thread b is:' + str(my_obj.b))28# 新線程對my_obj.b的修改不影響主線程的打印結果,因為my_obj是線程隔離的對象
~~~
* `from werkzeug.local import Local`,`Local`是`werkzeug`包里面的,不是`flask`的
* `LocalStack`是可以用來做線程隔離的棧,封裝了`Local`對象,把`Local`對象作為自己的一個屬性,從而實現線程隔離的棧結構
* `Local`是一個可以用來做線程隔離的對象,使用字典的方式實現線程隔離
* `stack`是棧結構
#### **封裝**
* **軟件世界里的一切都是由封裝來構建的,沒有什么是封裝解決不了的問題!**
* **如果一次封裝解決不了問題,那么就再來一次!**
* **編程也是一種藝術,代碼風格要含蓄!**

#### `LocalStack`
##### 作為棧
> 基本上來講,要實現**棧**結構,必須要實現`push`、`pop`、`top`這三個操作 `push` 推入棧 `top` 取棧頂元素,不彈出該元素 `pop` 取棧頂元素,并彈出該元素 **棧**結構只能取棧頂元素(后進先出) (如果棧可以隨意的按照下標去結構中的元素,那么**棧**和**列表**之類的數據結構有什么區別呢?) **`規律:很多數據結構,實際上就是限制了某些能力`**
~~~
1from werkzeug.local import LocalStack2?3?4s = LocalStack()5s.push(1)6?7print(s.top)8print(s.top)9print(s.pop())10print(s.top)11?12?13s.push(1)14s.push(2)15?16print(s.top)17print(s.top)18print(s.pop())19print(s.top)20----------------------------------------------21執行結果:22123124125None262272282291
~~~
##### 作為線程隔離對象
> 兩個線程擁有兩個棧,是相互隔離的,互不干擾
~~~
1import threading2import time3?4from werkzeug.local import LocalStack5?6?7my_stack = LocalStack() # 實例化具有線程隔離屬性的LocalStack對象8my_stack.push(1)9print('in main thread after push, value is:' + str(my_stack.top))10?11?12def worker():13 ? ?# 新線程14 ? ?print('in new thread before push, value is:' + str(my_stack.top))15 ? ?# 因為線程隔離,所以在主線程中推入1跟其他線程無關,故新線程中的棧頂是沒有值的(None)16 ? ?my_stack.push(2)17 ? ?print('in new thread after push, value is:' + str(my_stack.top))18?19?20new_t = threading.Thread(target=worker, name='my_new_thread')21new_t.start()22time.sleep(1)23?24# 主線程25print('finally, in main thread value is:' + str(my_stack.top))26# 因為線程隔離,在新線程中推入2不影響主線程棧頂值得打印27------------------------------------------------------------------------------------28執行結果:29in main thread after push, value is:130in new thread before push, value is:None31in new thread after push, value is:232finally, in main thread value is:1
~~~
##### **經典面試問題**
> 1. `flask`使用`LocalStack`是為了隔離什么對象? 答:這個問題很簡單 ,什么對象被推入棧中就是為了隔離什么對象,`AppContext`(應用上下文)和`RequestContext`(請求上下文)被推入棧中,所以是為了隔離`AppContext`(應用上下文)和`RequestContext`(請求上下文)
>
> 2. 為什么要隔離這些對象? 表面原因:在多線程的環境下,每個線程都會創建一些對象,如果我們不把這些對象做成線程隔離的對象,那么很容易發生混淆,一旦發生混淆之后,就會造成我們程序運行的錯誤 根本原因:我們需要用一個變量名,同時指向多個線程創建的多個實例化對象,這是不可能的。但是我們可以做到當前線程在引用到`request`這個變量名的時候,可以正確的尋找到當前線程(它自己)創建的實例化對象。
>
> 3. 什么是線程隔離的對象和被線程隔離的對象? `LocalStack`和`Local`是**線程隔離的對象** `AppContext`(應用上下文)和`RequestContext`(請求上下文)是**被線程隔離的對象**
>
> 4. `AppContext`(應用上下文)和`Flask`核心對象的區別? 這兩個是兩個對象,`Flask`核心對象`app`將作為一個屬性存在于`AppContext`(應用上下文)下!!!
>
> 5. `Flask`的核心對象可以有多個嗎? `Flask`核心對象在全局里只有一個。因為`app`是在入口文件里創建的,入口文件是在主線程里去執行的,所以以后無論啟動多少個線程,都不會執行`create_app()`了。
>
* **使用線程隔離的意義**:`使當前線程能夠正確引用到他自己所創建的對象,而不是引用到其他線程所創建的對象`
 
### 11\. `flask`開啟多線程的方法
在入口文件將`threaded=True`開啟 
### 12\. `ViewModel`層的作用

* 頁面所需數據結構與**原始數據結構**是一一對應的時候:
* 原始數據可以直接傳給頁面
* 頁面所需數據結構與**原始數據結構**不一致:
* 需要`ViewModel`對原始數據結構進行`裁剪`、`修飾`、`合并`
因為原始數據并不一定能滿足客戶端顯示的要求,`ViewModel`給了調整數據的機會,不同的頁面對數據差異化的要求,可以在`ViewModel`里進行集中處理。
> 將`author`列表轉換成字符串再傳給客戶端: 
>
>  **返回`author`列表的靈活性要比返回字符串高**,返回列表,客戶端可以根據需求使用不同的符號將作者分割鏈接,但是字符串就限制了客戶端的選擇。 對于返回客戶端`author`數據的格式,`web`編程經驗的個人建議:
>
> * 如果我們正在做的是單頁面前后端分離的應用程序,建議`author`保持列表的形式直接返回到客戶端去,讓客戶端使用`JavaScript`來操作或者解析列表。
>
> * 如果是在做網站的話,建議`author`在`ViewModel`里處理。
>
>
> 原因:`JavaScript`處理這些事情非常方便,但如果是模板渲染的方式來渲染`html`的話,我們先把數據處理好,直接往模板里填充會是更好的選擇! **數據在哪里處理,可以根據實際情況而定!**
### 13\. 面向對象
#### `面向對象`的類
* 描述自己的特征(數據)
* 使用類變量、實例變量來描述自己的特征
* 行為
* 用方法來定義類的行為
> 對面向對象理解不夠深刻的同學經常寫出只有行為沒有特征的類,他們所寫的類里大量的都是方法而沒有類變量、實例變量,這種類的本質還是`面向過程`,因為面向過程的思維方式是人類最為熟悉的一種思維方式,所以比較容易寫出面向過程的類。 `面向過程`的基本單位是函數,`面向對象`的基本單位是類 雖然使用了`class`關鍵字并且將一些方法或者函數封裝到`class`內部,但是并沒有改變這種面向過程的實質。 `面向對象`是一種思維方式,并不在于你的代碼是怎么寫的,如果說你的思維方式出現錯誤,那你肯定是寫不出面向對象的代碼的。
* 如何去審視自己的類?去判斷我們寫出來的類到底是不是一個`偽面向對象`?
* 如果一個類有大量的**可以被標注為**`classmethod`或者`staticmethod` 的靜態方法,那么你的類封裝的是不好的,并沒有充分利用面向對象的特性。
### 14\. 代碼解釋權的反轉
* 代碼的解釋權不再由函數的編寫方所定義的,而是把解釋的權利交給了函數的調用方 
~~~
1return jsonify(books)2# 報錯,因為books是一個對象,對象時無法進行序列化的3?4return jsonify(books.__dict__)5# 將books取字典之后同樣會報錯,因為books里面包含對象,所包含的對象無法進行序列化6?7return json.dumps(books, default=lambda o: o.__dict__)8# 最后使用json.dumps(),完美解決問題9?
~~~
> 在`json.dumps()`的內部處理這些別人傳過來的參數的時候,我們是不知道怎么去解釋它的,所以我們把解釋權交給函數的調用方,由函數的調用方把不能序列化的類型轉化為可以序列化的類型,轉移解釋權的思維使用`函數式編程`是很容易編寫的。在設計`json.dumps()`的時候要求函數的調用方傳遞進來一個函數,傳遞進來的函數它的具體的實現細節是由函數的調用方編寫的,我們不需要關心函數內部具體的實現細節,一旦遇到了不能序列化的對象就調用`func(obj)`函數,讓`func(obj)`負責把不能序列化的類型轉化為可序列化的類型,我們只需要關注`return`的結果就行了


### 15\. 單頁面和網站的區別
> 經典面試問題: 單頁面和普通網站的區別?
>
> * `單頁面`:
>
> 1. 并不一定只有一個頁面;
>
> 2. 最大的特點在于數據的渲染是在客戶端進行的;
>
> 3. 單頁面應用程的業務邏輯,也就是說數據的運算主要還是集中在客戶端,用`JS`去操作的。
>
> * `多頁面普通網站`:大多數情況下數據的渲染或者模板的填充是在服務端進行的。
>
#### **普通網站**
 
#### **單頁面**
* 單頁面中`html`也是靜態資源
 
### 16\. `flask`靜態文件訪問原理
* 在實例化`flask`對象的時候,可以指定`static_folder`(靜態文件目錄)、`static_url_path`(靜態文件訪問路由)
> 
>
> 
>
> 
>
>  `_static_folder`和`_static_url_path`默認值為`None`
* `blueprint`(藍圖)的靜態資源操作與`flask`核心對象操作一樣 
### 17\. 模板文件的位置與修改方案
`templates`文件位置可以由`template_folder`指定模板文件路徑
* 需求:將字典填充到`html`里,再將填充之后的`html`返回到客戶端去
`flask`為了讓我們能夠在模板里面很好的解析和展示數據,它引入了一個模板引擎`Jinja2`
> 像`Jinja`這種可以幫助我們在`html`中渲染和填充數據的語言通常被稱為`模板語言`

在`Jinja`里有兩個流程控制語句是經常用到的:(`Jinja`流程控制語句都必須寫在`{% %}`里面)
* `if`語句(條件語句)
~~~
1 ? {% if data.age < 18 %}2 ? ? ? <ul>{{ data.name }}</ul>3 ? {% elif data.age == 18%}4 ? ? ? <ul>do some thing</ul>5 ? {% else %}6 ? ? ? <ul>{{ data.age }}</ul>7?8 ? {% endif %}
~~~
* `for in`語句(循環控制語句)
~~~
1 ? {% for foo in [1,2,3,4,5] %}2 ? ? ? {{ foo }}3 ? ? ? <div>999</div>4?5 ? {% endfor %}6?7 ? {% for key, value in data.items() %}8 ? ? ? {# 注意for后面跟的是鍵值對,用逗號分開。#}9 ? ? ? {# data后面要加上iterms(),要確保是個可迭代對象,否則會報錯 #}10 ? ? ? {{ key }}11 ? ? ? {{ value }}12?13 ? {% endfor %}
~~~
#### 模板繼承的用法

1. 寫好一級`html`頁面:`layout.html` ,包含`block`模塊。
2. 再需要繼承的`html`頁面頂端導入基礎`html`:`{% extends ‘layout.html' %}`。
3. 使用`{{ super() }}`關鍵字可以繼承一級`html`頁面中的`block`模塊的內容,不使用的話只能替換,無法繼承。
#### 模板語言的過濾器
過濾器的基本用法是在關鍵是后面加上豎線 `|`
* `default`過濾器:是用來判斷屬性是否存在的,當訪問一個不存在的屬性時,`default`后面的賦值才會被顯示,當訪問一個存在的屬性時,`default`后面的賦值不會被顯示。
~~~
1{# data.name data.age 存在,data.school 不存在#}2?3{{ data.school | default=('未名') }}
~~~
* `|` 有點像`linux`里的管道符號,其表達的意思是`值得傳遞`
~~~
1{# data.name = None, data.age 存在,data.school 不存在 #}2?3{{ data.name == None | default=('未名') }}4{# 頁面最終返回的結果是 True,豎線并不是表示前面的語句成立,就執行后面的語句5豎線表示的是值的傳遞,前面等式成立為 True,將 值True 傳給后面的語句,default判斷出 True是存在的,所以頁面返回的是 True #}
~~~
更復雜的示例:
~~~
{# data.name = None, data.age 存在,data.school 不存在 #}
{{ data.school | default(data.school) | default=('未名') }}
{# 頁面最終返回的結果是未名
第一個 default 首先判斷第一個 data.school 是否存在,不存在
然后 defuult 再對其括號內的 data.school 求值,不存在
然后再將值傳給第二個 default,因為接收到的是不存在的結果
所以第二個 default 會把 '未名' 顯示出來 #}
~~~
* `length`過濾器(長度過濾器):是用來判斷長度的
~~~
{# data.name、data.age 存在 #}
{{ data | length() }}
{# 頁面最終的返回結果為 2 #}
~~~
### 18\. 反向構建`URL`
反向構建`url`使用的是`url_for`,使用方法:
~~~
{{ url_for('static', filename='test.css') }}
{# static 是靜態文件,test.css 是需要加載的 css文件 #}
~~~
**凡是涉及到`url`生成的時候,建議都使用`url_for`,例如`CSS`文件的加載、`JS`文件的加載、圖片的加載,包括視圖函數里重定向的時候一樣可以使用`url_for`來生成**
* 加載`css`文件的方法
* 使用硬路徑
* 缺點:當服務器域名地址、域名端口等需要改動過的時候非常麻煩
~~~
<link rel="stylesheet" href="http://localhost/5000/static/test.css">
~~~
* 使用相對路徑
* 缺點:當需要修改靜態資源(css、js等)路徑的時候非常麻煩
~~~
<link rel="stylesheet" href="../static/test.css">
~~~
* 使用反向構建`url`的方法
* 非常方便、完美解決問題
~~~
<link rel="stylesheet" href="{{ url_for('static', filename='test.css') }}">
~~~
### 19\. `Messaging Flash`消息閃現
#### 消息閃現的用法
* 導入`flash`函數:`from flask import flash`
* 在視圖函數中調用`flash(message, category='message')`函數:
~~~
flash('你好,這里是消息閃現!', category='errors')
flash('Hello, this is messaging flash!', category='warning')
flash('你好,這里是消息閃現!')
~~~
* 在`html`頁面使用模板語言使用`get_flashed_messages()`方法獲得需要閃現的消息
* 問題1:如何獲取`get_flashed_messages()`函數的調用結果呢?
* 按照`python`慣有的操作思維,先定義一個變量,再引用這個變量就可以獲得這個函數的調用結果了。
* 問題2:在模板語言里如何定義一個變量?
~~~
{# 使用`set`關鍵字定義一個變量 #}
{% set messages = get_flashed_messages %}
{{ messages }}
{# 正常調用 messages #}
-------------------------------------------
['你好,這里是消息閃現!', 'Hello, this is messaging flash!', '你好,這里是消息閃現!']
{# 頁面最終返回結果 #}
~~~
* 需要在配置文件里配置`SECRET_KEY`才能正確顯示消息閃現
* `SECRET_KEY`本身就是一串字符串,但是要盡可能的保證它是獨一無二的,換句話說`SECRET_KEY`就是一個秘鑰
* 當`flask`需要去操作加密數據的時候,它需要讀取`SECRET_KEY`,并且把秘鑰運用到它的一系列算法中,最終生成加密數據
* `flask`消息閃現需要用到`session`,`flask`里面的`session`是客戶端的不是服務端的,所以說加密對`flask`來說是極其重要的,所以說我們需要給應用程序配置`SECRET_KEY`
* 服務端的數據是相對比較安全的,但是如果數據是儲存在客戶端的,那么最好把數據加密,因為客戶端是不能信任的,很容易被篡改
#### `block`變量作用域
在一個`block`里定義的變量的作用于只存在于該`block`中,不能在其他`block`中使用
#### `with`語句變量作用域
模板語言里的`with`語句內定義的變量,只能在`with`語句內部使用,不能在外部使用
~~~
{% with messages = get_flashed_messages() %}
{{ messages }}
{% endwith %}
{# messages 變量只能在 with 語句內部使用 #}
~~~
> **Filtering Flash Messages Optionally you can pass a list of categories which filters the results of get\_flashed\_messages(). This is useful if you wish to render each category in a separate block.**
~~~
{% with errors = get_flashed_messages(category_filter=["error"]) %}
{% if errors %}
<div class="alert-message block-message error">
<a class="close" href="#">×</a>
<ul>
{%- for msg in errors %}
<li>{{ msg }}</li>
{% endfor -%}
</ul>
</div>
{% endif %}
{% endwith %}
~~~
### 20\. 搜索頁面詳解
建議:
* 加載`CSS`文件的時候,一般寫在`html`文件的頂部
* 加載`JS`文件的時候,一般寫在`html`文件的底部
~~~
class BookViewModel:
def __init__(self, book):
self.title = book['title']
self.publisher = book['publisher']
self.author = '丶'.join(book['author'])
self.pages = book['pages'] or ''
self.price = book['price']
self.summary = book['summary'] or ''
self.image = book['image']
@property
def intro(self):
intros = filter(lambda x: True if x else False,
[self.author, self.publisher, self.price])
return ' / '.join(intros)
# filter 為過濾器
# lambda 表達式
~~~
> `filter`函數(過濾器): 過濾規則是由`lambda`表達式來定義的, 如果`lambda`表達式某一項數據是`False`,那么該項數據就會被過濾掉; 如果返回的是`Ture`,該項數據會被保留。
在`html`頁面使用模板語言調用`intro`的數據:
~~~
{% for book in books.books %}
<div class="row col-padding">
<a href="{{ url_for('web.book_detail', isbn=book.isbn) }}" class="">
<div class="col-md-2">
<img class="book-img-small shadow" src="{{ book.image }}">
</div>
<div class="col-md-7 flex-vertical description-font">
<span class="title">{{ book.title }}</span>
{# <span>{{ [book.author | d(''), book.publisher | d('', true) , '¥' + book.price | d('')] | join(' / ') }}</span>#}
<span>{{ book.intro }}</span>
<span class="summary">{{ book.summary | default('', true) }}</span>
</div>
</a>
</div>
{% endfor %}
~~~
頁面最終的展示效果: 
> 因為搜索結果頁面展示的時候,需要展示`作者`、`出版社`、`價格`并將這三項數據使用`/`連接,但是獲取的數據并不是都有這三項數據,或者原始數據包含該項數據,但是該項數據為`空`,那么就會造成`曹雪芹//23.00元`這種情況,所以為了解決這個問題,引入了`intro`函數,我們在`intro`函數里判斷三項原始數據是否存在且是否為空,若存在且不為空則返回數據,如果存在且為空則返回`False`,如果不存在則返回`False`,過濾完成后得到一個`intros`列表,最后使用`return`語句將`intros`列表使用`/`連接起來再返回。
#### `@property`裝飾器
使用`@property`是讓`intro`函數可以作為屬性的方式訪問,就是將類的方法轉換為屬性
* 模板語言里 intro 作為`函數`的形式訪問:`{{ book.intro() }}`
* 模板語言里 intro 作為`屬性`的形式訪問:`{{ book.intro }}`
> 前文示例中的`intro`是數據,應該用屬性訪問的方式來獲取數據,而不應該用行為的方式來表達數據
對象的兩個特性:
* 數據是用來描述它的特征的
* 方法(或者函數)是用來描述它的行為的

### 21\. 業務模型
* `book`模型
* `user`模型
* `gift`模型:展示用戶與書籍之間關系的
> 如何在`gift`模型中表示`user`呢? 在`gift`模型里面引用`user`模型,`sqlalchemy`提供了`relationship`函數用來表明引用的關系。
~~~
from sqlalchemy import Column, Integer, Boolean, ForeignKey, String
from sqlalchemy.orm import relationship
from app.models.base import Base
class Gift(Base):
id = Column(Integer, primary_key=True)
user = relationship('User')
uid = Column(Integer, ForeignKey('user.id'))
isbn = Column(String(15), nullable=False)
# 因為書籍的數據是從yushu.im的 API 獲取的,不是從數據庫獲取的,所以不能從數據庫關聯
# 贈送書籍的 isbn 編號是可以重復的,原因很簡單
# 例如:A 送給 B 挪威的森林;C 也可以送給 D 挪威的森林。
# 從數據庫關聯 book 模型的寫法跟關聯 user 模型的寫法一樣
# book = relationship('Book')
# bid = Column(Integer, ForeignKey('book.id'))
launched = Column(Boolean, default=False)
# 表明書籍有沒有贈送出去,默認 False 未贈送出去
~~~
#### 假刪除
業務模型最終都會在數據庫生成一條一條的記錄
* 物理刪除:直接從數據庫里刪除記錄
* 缺點:刪除之后找不回來
> 互聯網有時候需要分析用戶的行為: 一個用戶曾經要贈送一份禮物,后來他把贈送禮物取消了,不想再贈送書籍了。 如果把這條記錄直接從數據庫里刪除之后,是沒有辦法對用戶的歷史行為進行分析的 所以大多是情況都不會采用物理刪除,而是用`假刪除`或者`軟刪除`
`假刪除`或者`軟刪除`:是新增加一個屬性`status`,用`status`表示這條數據是否被刪除
> `status = Column(SmallInteger, default=1)` `1`表示保留記錄,`0`表示刪除記錄 通過更改`status`的狀態在決定是否展示這條記錄,實際上這條數據一直存在于數據庫當中 基本上所有模型都需要`status`屬性,所以將`status`寫在基類里,通過繼承使所有模型獲得`status`屬性
~~~
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import Column, Integer, SmallInteger
db = SQLAlchemy()
class Base(db.Model):
__abstract__ = True # 作用:不讓 sqlalchemy 創建 Base 數據表
create_time = Column('create_time', Integer)
status = Column(SmallInteger, default=1)
~~~
> 創建`Base`之后,`sqlalchemy`會自動創建`Base`數據表,但是我們并沒有在`Base`類里定義`primary_key`,所以運行時會報錯。創建`Base`類只是想讓模型繼承它,并不想創建`Base`數據表(沒有需求,沒有任何意義)。在`Base`類里加入`__abstract__ = True`可以讓`sqlalchemy`不去創建數據表
### 22\. 用戶注冊

* `request.form`可以獲取用戶提交的表單信息
* `wtforms.validators.DataRequired`驗證,也就是代表了該字段為必填項,表單提交時必須非空。
~~~
from wtforms import Form, StringField, PasswordField
from wtforms.validators import DataRequired, Length, Email
class RegisterForm(Form):
email = StringField(validators=[DataRequired(), Length(8, 64), Email(message='電子郵件不符合規范')])
password = PasswordField(validators=[DataRequired(message='密碼不可以為空,請輸入你的密碼'), Length(6, 32)])
nickname = StringField(validators=[DataRequired(), Length(2, 10, message='昵稱至少需要兩個字符,最多10個字符')])
~~~
#### 用戶密碼加密
使用`werkzeug.security`包里的`generate_password_hash`函數對用戶密碼加密
~~~
from werkzeug.security import generate_password_hash
user.password = generate_password_hash(form.password.data)
~~~
#### 修改數據庫表單的名稱
默認情況下,在模型里定義的字段的名字就是生成的數據庫表單的名字
* 修改生成的數據庫表名稱的方法:
* 使用`__tablename__`修改
* 修改生成的數據庫表字段名稱的方法:
* `傳入字符串`
~~~
from sqlalchemy import Column
from app.models.base import Base
class User(Base):
__tablename__ = 'user' # 添加 __tablename__ 指定生成的數據庫表名為 user
_password = Column('password') # 在 Column 里傳遞字符串指定表字段的名字
id = Column(Integer, primary_key=True)
nickname = Column(String(24), nullable=False)
phone_number = Column(String(18), unique=True)
confirmed = Column(Boolean, default=False)
~~~
#### `python`動態賦值
~~~
@web.route('/register', methods=['GET', 'POST'])
def register():
form = RegisterForm(request.form) # 實例化注冊表單,獲取用戶提交的表單
if request.method == 'POST' and form.validate():
user = User() # 實例化用戶
user.set_attrs(form) # 將真實用戶與服務器用戶綁定,相應屬性賦值
return render_template('auth/register.html', form={'data': {}})
~~~
`set_attrs`使用了`python`作為動態語言的優勢,在基類`Base`里復寫了`set_attrs`方法,所有模型都繼承了`set_attrs`方法
~~~
class Base(db.Model):
__abstract__ = True
create_time = Column('create_time', Integer)
status = Column(SmallInteger, default=1)
def set_attrs(self, attrs_dict):
for key, value in attrs_dict.items():
if hasattr(self, key) and key != 'id': # 主鍵 id 不能修改
setattr(self, key, value)
# set_attrs 接收一個字典類型的參數,如果字典里的某一個 key 與模型里的某一個屬性相同
# 就把字典里 key 所對應的值賦給模型的相關屬性
~~~
#### 自定義驗證器
如何校驗業務邏輯相關的規則?(使用自定義驗證器)
> 比如:`email`符合電子郵箱規范,但是假如數據庫里已經存在了一個同名的`email`,這種情況該怎么處理?大多數同學在寫代碼的時候也會進行業務性質的校驗,但是很多人都會把業務性質的校驗寫到視圖函數里面去。建議:**業務性質的校驗也應該放到`form`里進行統一校驗**
~~~
from wtforms import Form, StringField, PasswordField
from wtforms.validators import DataRequired, Length, Email, ValidationError
from app.models.user import User
class RegisterForm(Form):
email = StringField(validators=[
DataRequired(), Length(8, 64), Email(message='電子郵件不符合規范')])
password = PasswordField(validators=[
DataRequired(message='密碼不可以為空,請輸入你的密碼'), Length(6, 32)])
nickname = StringField(validators=[
DataRequired(), Length(2, 10, message='昵稱至少需要兩個字符,最多10個字符')])
# 自定義驗證器,驗證 email
def validate_email(self, field):
if User.query.filter_by(email=field.data).first():
raise ValidationError('電子郵件已被注冊')
# 自定義驗證器,驗證 nickname
def validate_nickname(self, field):
if User.query.filter_by(nickname=field.data).first():
raise ValidationError('該昵稱已被注冊')
~~~
#### `cookie`
`cookie`本來的機制:哪個網站寫入的`cookie`,哪個網站才能獲取這個`cookie`
> 也有很多技術實現跨站`cookie`共享
`cookie`的用途:
* 用戶票據的保存
* 廣告的精準投放
#### 用戶驗證
建議:將用戶密碼驗證的過程放在用戶模型里,而不要放在視圖函數里
* 郵箱驗證
* 直接在數據庫查詢郵箱
* 密碼驗證
* 1. 現將用戶提交的明文密碼加密
* 2. 再與數據庫儲存的加密密碼進行比對
~~~
from werkzeug.security import check_password_hash
def check_password(self, raw):
return check_password_hash(self._password, raw)
# raw 為用戶提交的明文密碼
# self._password 為數據庫儲存的加密的密碼
# 使用 check_password_hash 函數可以直接進行加密驗證
# 驗證過程:
# 1.先將明文密碼 raw 加密;
# 2.再與數據庫里的密碼進行比對;
# 3.如果相同則返回 True,如果不相同則返回 False
~~~
#### 用戶登錄成功
用戶登陸成功之后:
1. 需要為用戶生成一個票據
2. 并且將票據寫入`cookie`中
3. 還要負責讀取和管理票據
> 所以說整個登錄機制是非常繁瑣的,自己去實現一整套的`cookie`管理機制是非常不明智的, 幸運的是`flask`提供了一個插件,可以完全使用過這個插件來管理用戶的登錄信息
##### 使用`flask-login`插件管理用戶的登錄信息
1. 安裝插件
~~~
pip install flask-login
~~~
2. 插件初始 因為`flask-login`插件是專為`flask`定制的插件,所以需要在`app`目錄下的`init.py`文件中將插件初始化
~~~
from flask_login import LoginManager
login_manager = LoginManager()
# 實例化 LoginManager
def create_app():
app = Flask(__name__)
app.config.from_object('app.secure')
app.config.from_object('app.setting')
register_blueprint(app)
db.init_app(app)
login_manager.init_app(app)
# 初始化 login_manager
# 寫法一:傳入關鍵字參數
# db.create_all(app=app)
# 寫法二:with語句 + 上下文管理器
with app.app_context():
db.create_all()
return app
~~~
3. 保存用戶的票據信息
~~~
from flask_login import login_user
@web.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm(request.form)
if request.method == 'POST' and form.validate():
user = User.query.filter_by(email=form.email.data).first()
if user and user.check_password(form.password.data):
login_user(user, remember=True)
# remember=True 表示免密登錄,flask默認免密登錄的有效時長為365天
# 如果需要更改免密登錄的有效時長,則可以在 flask 的配置文件中設置 REMENBER_COOKIE_DURATION 的值
else:
flash('賬號不存在或密碼錯誤')
return render_template('auth/login.html', form=form)
~~~
> 這里并不直接操作`cookie`,而是通過`login_user(user)`間接的把用戶票據寫入到`cookie`中。 票據到底是什么?我們往`cookie`里寫入的又是什么?
>
> * 我們往`login_user(user)`里傳入了一個我們自己定義的`user`模型,那么是不是說`login_user(user)`把我們用戶模型里所有的數據全部寫入到`cookie`中了呢? 并不是,因為這個模型是我們自己定義的,我們自己定義的數據可能非常的多,全部寫入不現實;而且其中的一些信息是根本不需要寫入`cookie`中的。
>
> * 那么最關鍵的、最應該寫入`cookie`中的是什么信息?是用戶的`id`號!因為`id`號才能代表用戶的身份
>
> * 那么`login_user(user)`怎么知道我們自己定義的`user`模型下面這么多個屬性里,哪一個才是代表用戶身份信息的`id`號呢? 所以`flask_login`這個插件要求在`user`模型下定義一系列的可以獲取用戶相應屬性的方法,這樣`flask_login`可以直接調用這些方法去獲取模型的屬性,而不用去識別用戶屬性
>
> * 可以繼承`flask_login`插件里`UserMixin`基類,從而獲得這些方法(獲取模型屬性的方法),避免重復寫。此種方法**對模型里屬性的名稱有硬性要求**,比如`id`不能為`id`,因為調用的時候會繼承`UserMixin`里的方法,`UserMixin`方法里是寫死了的,如果屬性名稱不一樣需要在`user`模型里覆寫獲取屬性的相關方法。 
>
#### 訪問權限控制
網站的視圖函數大致分為兩類:
* 需要用戶登錄才能訪問
* 不需要登錄即可訪問
> 如果需要限制一個視圖函數需要登錄才能訪問,怎么辦? 如果僅僅是將用戶信息寫入到`cookie`中,我們完全不需要引入第三方插件,但是如果考慮到要對某些視圖函數做權限的控制的話,第三方插件就非常有用了。 對于一個用戶管理插件而言,最復雜的實現的地方就在于對權限的控制
使用第三方插件的裝飾器來進行登錄權限的控制:
1. 在視圖函數前加上裝飾器 
#### 重定向攻擊
1. 當用戶訪問一個需要登錄的視圖函數的時候,會自動跳轉到登錄頁面(生成附加`next`信息的登錄頁面`url`) 
2. 登錄頁面的`url`后面會添加`next`信息,例如: `http://127.0.0.1:5000/login?next=%2Fmy%2Fgifts` `next`表示的就是登陸完成后所需要跳轉的頁面(重定向),一般是定向為原來需要訪問的頁面
3. 如果用人惡意篡改`next`信息,例如: `http://127.0.0.1:5000/login?next=http://www.baidu.com` 使用該鏈接登錄之后就會跳轉到百度頁面,這種被稱為`重定向攻擊`
4. 那么如何防止這種重定向攻擊呢? 需要在視圖函數中判斷`next`的內容是否為`/`開頭,因為如果是`/`開頭的話表明還是在我們域名內部跳轉,要跳轉到其他域名的話必須以`http://`開頭 
> `next.startswith()`函數可以判斷`next`信息是否以`/`開頭
### 23\. `wish`模型
#### 1\. 分析得到`wish`模型幾乎和`gift`一模一樣,所以直接復制過來稍微改一下就行了
#### 2\. 點擊`贈送此書`和`加入到心愿清單`要求用戶登錄,在這兩個視圖函數前加上`@login_required`
~~~
- 贈送此書:`save_to_gift`
- 加入到心愿清單:`save_to_wish`
~~~

#### 3\. `gift`還有`uid`屬性需要賦值,`uid`從哪里獲取呢?
當一個用戶登錄之后,我們可以通過第三方用戶登錄管理插件`flask_login`的`current_user`獲取當前用戶的`id`,`current_user`為什么可以獲取當前用戶呢?因為之前我們在`user`模型里定義了`get_user`方法,該方法可以讓我們通過`uid`獲取用戶模型,所以這里的`current_user`本質上就是一個實例化的`user`模型,所以可以用`current_user.id`獲取當前用戶的`uid`。
~~~
from app import login_manager
@login_manager.user_loader
def get_user(uid):
return User.query.get(int(uid))
~~~
~~~
@web.route('/gifts/book/<isbn>')
@login_required
def save_to_gifts(isbn):
gift = Gift()
gift.isbn = isbn
gift.uid = current_user.id
db.session.add(gift)
db.session.commit(gift) # 將數據寫入到數據庫
~~~
#### 4\. 魚豆
 為了后期方便修改上傳一本書所獲得的魚豆數量,所以將上傳一本書所獲得的魚豆數量設定為變量寫到配置文件里,再在需要的時候讀取配置文件。
~~~
BEANS_UPDATE_ONE_BOOK = 0.5
current_user.beans += current_app.config['BEANS_UPDATE_ONE_BOOK']
~~~
#### 5\. 前面第3點中的`save_to_gift`視圖函數有很多問題
* `isbn`編號沒有驗證
* 不清楚傳入的`isbn`編號符不符合`isbn`規范
* 不清楚傳入的`isbn`編號是否已存在與數據庫當中
* 如果這本書不在數據庫,則需要上傳之后才能贈送
* 不清楚傳入的`isbn`編號是否已經存在于贈送清單中
* 如果這本書在贈送清單,則不需要加入贈送清單了
* 不清楚傳入的`isbn`編號是否已經存在于心愿清單中
* 如果這本在心愿清單,表示用戶沒有這本書,那么用戶就無法贈送這本書
* 在`user`模型的內部添加判斷書籍能否添加到贈送清單的條件:
~~~
class User(UserMixin, Base):
...
def can_save_to_list(self, isbn):
if is_isbn_or_key(isbn) != 'isbn':
return False
yushu_book = YuShuBook()
yushu_book.search_by_isbn(isbn)
if not yushu_book.first():
return False
# 不允許一個用戶同時贈送多本相同的圖書
# 一個用戶不能同時成為一本圖書的贈送者和索要者
# 這本圖書既不在贈送清單中也不再心愿清單中才能添加
gifting = Gift.query.filter_by(isbn=isbn, uid=self.id, lunched=False).first()
wishing = Wish.query.filter_by(isbn=isbn, uid=self.id, lunched=False).first()
if gifting and wishing:
return True
else:
return False
~~~
* 在`save_to_gift`視圖函數中調用判斷條件的函數來判斷是否將書籍添加到贈送清單:
~~~
@web.route('/gifts/book/<isbn>')
@login_required
def save_to_gifts(isbn):
if current_user.can_save_to_list(isbn):
gift = Gift()
gift.isbn = isbn
gift.uid = current_user.id
current_user.beans += current_app.config['BEANS_UPDATE_ONE_BOOK']
db.session.add(gift)
db.session.commit(gift) # 將數據寫入到數據庫
else:
flash('這本書已添加至你的贈送清單或已存在與你的心愿清單,請不要重復添加')
~~~
> **`can_save_to_list`寫在`user`里正不正確?** `can_save_to_list`是用來做校驗的,之前做校驗的時候都是建議大家把校驗放在`Form`里,為什么這里沒有寫在`Form`里呢?其實這個原因就在于編程是沒有定論的,不是說只要是校驗的都要全部放在`Form`里,而是要根據你的實際情況來選擇,放在`Form`里有放在`Form`里的好處,放在`user`模型里有放在`user`模型里的好處。你把`can_save_to_list`看做參數的校驗,放在`Form`里是沒有錯的,但是這個`can_save_to_list`可不可以看做是用戶的行為呢?也可以看做是用戶的行為,既然是用戶的行為,那么放在`user`模型里也是沒有錯的。而且放在`user`模型里是有好處的,好處就是它的復用性會更強一些,以后如果需要相同搞的判斷你的時候,放在`Form`校驗里用起來是很不方便的,但是如果放在`user`模型里用起來是相當方便的。 所以說呢要根據實際情況而定,編程是沒有定論的,只要你能找到充分的依據,那么你就可以這么做。
#### 6\. 事物
事物是數據庫里的概念,但是放到模型的方式里,它也是存在的。 
這里是操作了兩張數據表:
* `gift`表
* `user`表
如果在操作完`gift`表之后突然程序中斷了,`user`表中的`beans`并沒有加上,這樣就會造成數據庫數據的異常。所以說我們必須要保證要么兩個數據表同時操作,要么都不操作,這樣才能保證我們數據的完整性。 那么這樣在數據庫保證數據一致性的方法叫做`事物`
> 那么如何使用`sqlalchemy`來進行事物的操作? 其實`sqlalchemy`就是天然支持這種事物的,其實我們這種寫法已經用到事物,為什么呢?因為在`db.session.commit(gift)`前面的所有操作,都是真正的提交到數據庫里去,一直到調用`db.session.commit(gift)`才提交的。 道理是這個道理,但是上述代碼還是有問題的,那就是沒有執行數據庫的`rollback`回滾操作
~~~
try:
gift = Gift()
gift.isbn = isbn
gift.uid = current_user.id
current_user.beans += current_app.config['BEANS_UPDATE_ONE_BOOK']
db.session.add(gift)
db.session.commit(gift) # 將數據寫入到數據庫
except Exception as e:
db.session.rollback()
raise e
~~~
> 為什么一定要執行`db.session.rollback()`? 如果說執行`db.session.commit(gift)`的時候,出現了錯誤,而我們有沒有進行回滾操作,不僅僅這次的插入操作失敗了,還有后續的所有插入操作都會失敗。 建議:以后只要進行`db.session.commit()`操作都要用`try except`將其包裹起來
~~~
try:
...
db.session.add(gift)
db.session.commit(gift)
except Exception as e:
db.session.rollback()
raise e
~~~
#### 7\. `python @conetentmanager`
思考問題:對于`db.session.commit()`我們可能在整個項目的很多地方都需要使用到,每次寫`db.session.commit()`的時候都要重復寫這樣一串代碼,那么有沒有什么方法可以避免寫這樣重復的代碼呢?
* 認識`python @conetentmanager` `@conetentmanager`給了我們一個機會,讓我們可以把原來不是上下文管理器的類轉化為上下文管理器。 假如`MyResource`是`flask`提供給我們的或者是其他第三方類庫提供給我們的話,我們去修改別人的源碼,在源碼里添加`__enter__`方法和`__exit__`方法這樣合適嗎?顯然不合適。但是我們可以在`MyResource`的外部把`MyResource`包裝成上下文管理器,
~~~
class MyResource:
# def __enter__(self):
# print('connect to resource')
# return self
#
# def __exit__(self, exc_type, exc_val, exc_tb):
# print('close resource connection')
def query(self):
print('query data')
# with MyResource as r:
# r.query()
from contextlib import contextmanager
@contextmanager
def Make_Resource():
print('connect to resource')
yield MyResource()
print('close resource connection')
with Make_Resource() as r:
r.query()
----------------------------------------------------------------
運行結果:
connect to resource
query data
close resource connection
~~~
> 帶有`yield`關鍵字的函數叫做`生成器` `yield`關鍵字可以讓函數在處理到`yield`返回`MyResource`之后處于`中斷`的狀態,然后讓程序在外面執行完`r.query()`之后再次回到`yield`這里執行后面的代碼。
我們整體來看下,這里使用`@conetentmanager`之后與正常使用`with`語句的代碼到底是減少了還是增多了? 很多教程里都說`@conetentmanager`這個內置的裝飾器可以簡化上下文管理器的定義,但是我不這么認為,我認為這種做法是完全不正確的。本身的`@conetentmanager`裝飾器在理解上就是比較抽象的,其實還不如`__enter__`和`__exit__`方法來的直接。
* 靈活運用`@conetentmanager`
需求場景:我現在需要打印一本書的名字《且將生活一飲而盡》,書名前后需要使用書名號《》將書名括起來。這個書名是我從數據庫里查出來的,我們在數據庫里保存書名的時候肯定不會在書名前后加上書名號,我現在取出來了,我想讓它在顯示的時候加上書名號,怎么辦呢?有沒有什么辦法可以自動把書名號加上呢? 答:可以使用`@conetentmanager`內置裝飾器輕松的在書名的前后加上書名號
~~~
from contextlib import contextmanager
print('《且將生活一飲而盡》')
@contextmanager
def book_mark():
print('《', end='')
yield
print('》', end='')
with book_mark():
print('且將生活一飲而盡', end='')
-----------------------------------------------
運行結果:
《且將生活一飲而盡》
《且將生活一飲而盡》
~~~
* 使用`@conetentmanager`重構代碼 我們最核心的代碼是`db.session.commit()`,但是我現在想在`db`下面新增一個方法,然后我們用`with`語句去調用`db`下面我們自己定義的方法,就可以實現自動在前后加上`try`和`except`。
> `db`是`sqlalchemy`第三方插件的,我們如何在第三方類庫里面新增加一個方法呢? 很簡單,繼承`sqlalchemy`
> **小技巧**: 有時候我們在給類定義子類的時候,子類的名字非常難取,那么子類的名字難取,那不如我們先更改父類的名字,然后將之前父類的名字給子類(使用`from import`更改父類的名字) 
~~~
@web.route('/gifts/book/<isbn>')
@login_required
def save_to_gifts(isbn):
if current_user.can_save_to_list(isbn):
# try:
with db.auto_commit():
gift = Gift()
gift.isbn = isbn
gift.uid = current_user.id
current_user.beans += current_app.config['BEANS_UPDATE_ONE_BOOK']
db.session.add(gift)
# db.session.commit(gift) # 將數據寫入到數據庫
# except Exception as e:
# db.session.rollback()
# raise e
else:
flash('這本書已添加至你的贈送清單或已存在與你的心愿清單,請不要重復添加')
~~~
#### 8\. 為`create_time`賦值
我們發現`gift`數據庫里有一個`create_time`字段,但是并沒有值,為什么沒有值呢? 我們回想一個,數據庫里有字段,那肯定是在我們定義模型的時候定義了該字段。`create_time`是在我們模型的基類`Base`里定義的。
`create_time`表示用戶當前行為的發生時間,該怎么給`create_time`賦值呢? `create_time`表示當前模型生成和保存的時間。用戶發生行為,用戶是模型的實例化,所以肯定是在用戶模型的實例屬性里給`create_time`賦值,調用實例屬性的時候就是用戶發生行為的時候,所以我們可以在`Base`里定義`__init__`初始化函數,來賦值。 
> 導入`datetime`: `from datetime import datetime` `datetime.now()`表示獲取當前時間 `timestamp()`表示轉化為時間戳的格式
> **注意**: 類的**類變量**與類的**實例變量**的的區別: 類的類變量是發生在類的定義的過程中,并不是發生在對象實例化的過程中; 類的實例變量是發生在對象實例化的過程中。 區別示例: 如果我們在上述定義`create_time`的時候將其定義在`create_time = Column('create_time', Integer, default=int(datetime.now().timestamp()))`,那么將會導致數據庫里所有的`create_time`都是同一個時間(創建基類`Base`的時間),很顯然這種做法是錯誤的。
#### 9.`ajax`技術
對于這種:原來在什么頁面,由于我們要提交某些信息,最后又要回到這個頁面的這種操作,很多情情況下我們可以使用`ajax`技術。 
> `ajax`是前段的技術,做網站歸最網站,但是也要善于使用`ajax`技術來改善我們服務器的性能。
在上述過程不使用`ajax`技術的時候,最消耗服務器性能的是`book_detail`,`book_detail`又要把詳情頁面模板渲染再返回,這個模板渲染是最消耗服務器性能的。
**解決辦法**:把整個頁面當做靜態文件緩存起來,也是很多網站經常用到的一種技術。緩存起來之后,直接把頁面從緩存讀取出來再返回回去就行了。
#### 10\. 將數據庫里的時間戳轉換為正常的年月日
~~~
數據庫時間戳python時間對象正常時間
~~~
`time=single.create_time.strftime('%Y-%m-%d')` `create_time`是一個整數,整數下面是沒有`strftime()`方法的,只有`python`的時間類型才有`strftime()`方法,所以需要先把`create_time`轉化為`python`的時間類型對象。
因為`create_time`是所有模型都有的屬性,所以建議在模型的基類里進行轉換。使用`fromtimestamp()`函數轉換。
~~~
@property
def create_datetime(self):
if self.create_time:
return datetime.fromtimestamp(self.create_time)
else:
return None
~~~
~~~
def __map_to_trade(self, single):
if single.create_datetime:
time = single.create_datetime.strftime('%Y-%m-%d')
else:
time = '未知'
return dict(
user_name=single.user.nickname,
time=time,
id=single(id)
)
~~~
#### 11\. 再次提到`MVC`
* `MVC`: `M`:模型層,對應`Models`,例如:`book`模型、`user`模型、`gift`模型、`wish`模型 `V`:視圖層,對應`Template`模板 `C`:控制層,對應視圖函數 
* `Django`里的`MVT`: `M`:模型層 `V`:控制層 `T`:視圖層
> **經典面試問題**: 業務邏輯應該寫在`MVC`的哪一層里? 答:業務邏輯應該寫在`M`模型層里 很多同學會回答業務邏輯應該寫在`C`控制層里面,這個答案是錯誤的。 那是因為他們沒有搞清楚模型層和`數據層`的區別,在早期的時候,數據層的概念確實是存在的,它的全稱應該叫做`數據持久化層`。 `數據持久化層`它的主要作用是什么? 以前我們沒有`ORM`,當我們的模型層要使用數據的時候,它是有可能需要使用到不同的數據庫的,比如有些數據是儲存在`Oracle`、有些數據是儲存在`MySQL`里的、還有些數據是儲存在`MongoDB`里的,由于這些數據的數據源不通,有時候它們的一些具體的`SQL`的操作方法也不同,但是為了讓我們模型使用更加舒服,要保持一個不同數據庫的統一調用接口,我們需要用數據層來做封裝。但是我們`ORM`就不需要關注底層接口的,`ORM`已經幫我們做好了封裝。比如我們的`SQLAlchemy`,它自身就可以完成對接不同的數據庫。 
> **小知識**: `Model`模型層是可以分層的,在**復雜的業務邏輯**里我們可以在`Model`模型層里進一步的細分為`Model`、`Logic`、`Service` 
#### 12\. 復寫`filter_by`
* **問題背景**: 我們之前所有的查詢語句里都是有個嚴重的錯誤的,那么這個錯誤在什么地方呢? 我們項目里采用的數據刪除方式不是物理刪除,而是軟刪除,而軟刪除是通過一個狀態標示位`status`來表示這條數據是否已被刪除,如果我們在查詢的時候不加上這個關鍵條件`status=1`的話,那么我們查詢出來的結果會包括已經被我們刪掉的數據
* **解決方法**: 我們確實可以在每個`filter_by`里面加上`status=1`的搜索條件,但是這樣會很繁瑣。換種思維方式,既然我要做數據庫的查詢,那么我的目的就是要查詢沒有被刪除的數據。我們所有的查詢條件都是傳入到`filter_by`這個查詢函數里的,那么我們其實是可以考慮改寫`filter_by`這個函數內部的相關代碼從而讓我們的查詢條件可以自動覆蓋`status=1`。 問題是這個`filter_by`函數不是我們自己定義的,它是第三方庫`SQLAlchemy`提供的,最容易想到的方案就是繼承相關的類,然后用自己的實現方法去覆蓋這個`filter_by`函數。 如果我們要去覆蓋類庫里面的相關對象的話,一定要搞清楚這個類庫對象的繼承關系。

> **小知識**:
>
> * 上圖中的`**kwargs`實際上就是一組查詢條件:`isbn=isbn, launched=False, uid=current_user.id`類似這種,我們只需要在這組查詢條件里添加上`status=1`是不是就可以了?
>
> * 那么`**kwargs`到底是什么呢? 這就比較考驗同學的`python`基礎,我們在`python`基礎教程里面已經說過了`**kwargs`是一個字典,既然是字典那就好處理了。我們直接使用字典添加鍵值對的方式把`status=1`添加上就行了,`kwargs['status'] = 1`。 但是我們這里最好判斷一下,萬一別人在使用的時候傳入了`status=1`,我們就沒有必要賦值了。就是使用判斷字典中是否存在該鍵的普通方法。
>
~~~
if 'status' not in kwargs.keys():
kwargs['status'] = 1
~~~
> * 以上只是實現了自己的邏輯,我們還需要完成原有的`filter_by`的邏輯。 調用基類下面`filter_by`方法就可以了`super(Query, self).filter_by(**kwargs)`
>
> * **注意:原有`filter_by`傳入的參數 \*\*kwargs 是有雙星號的,這里的雙星號也不能少,否則會出現錯誤。在傳入一個字典的時候必須對這個字典進行解包,使用雙星號解包。** 最后因為原有`filter_by`函數有`return`語句,所以我們也需要添加是`return`。
>
> * 自定義的`Query`還沒有替換`BaseQuery`,那么怎么使用`Query`替換原有的`BaseQuery`呢? 查看源碼得知:`flask_sqlalchemy`里的`SQLAlchemy`的構造函數里是允許我們傳入一個我們自己的`BaseQuery`的。 所以我們只需要在實例化的時候傳入我們自定義的`Query`就可以了。
>
~~~
db = SQLAlchemy(query_class=Query)
~~~
#### 13\. 復雜`SQL`語句的編寫方案
問題背景: 前面我們已經完成了搜索結果頁面和圖書詳情頁面,下面我們來完成最近上傳頁面。最近上傳也是我們網站的首頁,最近上傳頁面的規則如下圖: 