Learning Objectives
- 把新增、查詢、修改、刪除對應到路由與 SQL
- 說明修改與刪除為什麼不可以用 GET
- 用 rowcount 與 abort(404) 處理找不到資料的情況
- 說明忘記 commit 的結果,並用 rollback 處理失敗的交易
- 用 is_deleted 欄位實作軟刪除
大部分網站都在做這四件事
留言板、筆記、課程公告、購物車,雖然畫面不同,對資料做的動作都是這四種,合稱 CRUD:
| 動作 | 路由 | 方法 | SQL |
|---|---|---|---|
| Create 新增 | /posts/new |
GET 顯示表單、POST 寫入 | INSERT |
| Read 查詢 | /、/posts/<id> |
GET | SELECT |
| Update 修改 | /posts/<id>/edit |
GET 顯示表單、POST 寫入 | UPDATE |
| Delete 刪除 | /posts/<id>/delete |
GET 確認頁、POST 執行 | DELETE 或 UPDATE |
這一課把前面學的表單、資料庫、轉址、錯誤頁組起來,做出一個完整的留言板,並處理它們會出錯的地方。
逐步看:一則留言的一生
程式依序新增、查詢、修改、改一筆不存在的、刪除,再查一次。範例比較長,可以拖動滑桿,或按「下一步」看呼叫堆疊怎麼進到各個 view 函數。
修改與刪除不可以用 GET
底下這種寫法看起來很方便,放一個連結就能刪除,但它有嚴重的問題:
# 有問題的寫法
@app.route('/posts/<int:post_id>/delete') # 只要開這個網址就刪除
def delete(post_id):
...
GET 在 HTTP 的語意裡是安全方法,代表它不會改變伺服器的資料。所有工具都這樣假設:
- 瀏覽器可能會預先載入連結,你還沒點,請求就送出了
- 搜尋引擎的爬蟲會沿著連結一路開,每個刪除連結都會被開到
- 別的網站放一張
<img src="https://你的網站/posts/1/delete">,瀏覽器就會送出這個請求,沒有其他防護時還會帶著你的登入狀態(第 17 課)
所以刪除要分兩步:GET 只顯示確認頁,真正的刪除由確認頁的表單用 POST 送出。修改也一樣。
硬刪除與軟刪除: DELETE 是硬刪除,資料就此消失。很多網站改用軟刪除:加一個 is_deleted 欄位,刪除只是把它設成 1。這樣可以復原,也保留了紀錄,代價是每一個查詢都要記得加 is_deleted = 0,漏了一個,已刪除的資料就會出現。
逐步看:軟刪除
請比較輸出的最後一行和列表:資料還在資料表裡,只是不再顯示。另外看第二次刪除為什麼回 404。
交易:一組動作要一起成功
報名讀書會要做兩件事:名額減一、新增一筆報名紀錄。如果第一步成功、第二步失敗,名額就憑空少了一個。
交易(transaction)把多個修改包成一組,只有兩種結果:
commit():全部生效rollback():全部還原,就像沒發生過
sqlite3 在第一個修改之前會自動開始一筆交易,直到你呼叫 commit 或 rollback。如果沒有 commit 就關閉連線,修改會消失:這就是「程式沒有報錯,資料卻沒有存進去」最常見的原因。
逐步看:失敗時還原
signups 資料表限制同一個人不能報名兩次(UNIQUE)。小安報名兩次,請看第二次名額先被扣掉,後來怎麼被還原。
動手做:修改與軟刪除
完成 edit_note 和 delete_note。兩者都只處理還沒刪除的資料,找不到時回 404,成功時確認寫入並轉址到 /。程式最下面的測試不要改。
想一想
下面的程式會印出什麼?
import sqlite3
con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE t (id INTEGER)")
con.execute("INSERT INTO t VALUES (1)")
cur = con.execute("UPDATE t SET id = 2 WHERE id = 9")
print(cur.rowcount)
下面的程式會印出什麼?
import sqlite3
con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE t (id INTEGER)")
con.execute("INSERT INTO t VALUES (1)")
con.commit()
con.execute("INSERT INTO t VALUES (2)")
con.rollback()
con.execute("INSERT INTO t VALUES (3)")
con.commit()
print(con.execute("SELECT COUNT(*) FROM t").fetchone()[0])
/posts/1/delete 寫成只要 GET 就刪除,下面哪一個不是它會造成的問題?
使用者回報「修改後沒有報錯,但資料沒有變」。最先要檢查什麼?
改用軟刪除之後,最容易出現什麼錯誤?
重點整理
- CRUD 對應
INSERT、SELECT、UPDATE、DELETE;每個修改都要commit - 修改與刪除用 POST;GET 只讀取,刪除前先給確認頁
- 找不到資料:
fetchone()回傳None、rowcount是 0,都要自己判斷並abort(404) - 多個修改要一起成功時,失敗就
rollback - 軟刪除用
is_deleted,每個查詢都要加條件,或把查詢集中在一個函數
到這裡你已經能做出一個有資料庫的網站。下一課把同樣的資料用 JSON 提供出去,讓其他程式也能使用:REST API。