Learning Objectives
- 說明為什麼資料不能只放在 Python 的串列裡
- 使用 sqlite3 的 connect、execute、fetchone、fetchall、commit 與 row_factory
- 寫出每個請求一條連線的 get_db,並用 teardown_appcontext 關閉
- 用 schema.sql 與 executescript 建立資料表
- 用參數化查詢把使用者輸入交給資料庫
- 判斷什麼情況 SQLite 夠用、什麼情況要換成 MySQL 這類資料庫
資料放在串列裡有什麼問題
前幾課的留言都放在 Python 的串列裡。串列存在記憶體,程式一結束就消失:重新啟動、更新程式、伺服器當機,資料全部不見。正式環境常常同時開好幾個程序處理請求,每個程序各有一份串列,內容還會不一致。
資料庫把資料存在磁碟上,並且處理好多人同時讀寫。這一課用 SQLite:它不需要另外安裝伺服器,整個資料庫就是一個檔案,Python 也內建了 sqlite3 模組。
如果還不熟 SQL,這一課用到的只有 CREATE TABLE、INSERT、SELECT,看範例就能理解,詳細語法在 SQL 軌。想直接打開資料庫檔案查看內容,可以用 SQLiteStudio、DB Browser for SQLite 這類工具。
逐步看:sqlite3 的基本操作
這個範例還沒有用到 Flask。請注意 commit 出現的位置,以及 fetchone、fetchall 各回傳什麼。
逐步看:每個請求一條連線
網站會同時處理很多請求,所以每個請求各開一條連線,處理完就關掉。請看「開啟連線」與「關閉連線」什麼時候印出來,以及 /hello 為什麼一次都沒有印。
這段寫法後面每一課都會用到
def get_db():
if 'db' not in g:
g.db = sqlite3.connect('site.db')
g.db.row_factory = sqlite3.Row
return g.db
@app.teardown_appcontext
def close_db(exc):
db = g.pop('db', None)
if db is not None:
db.close()
為什麼不在程式開頭開一條全域連線,大家共用?因為同時進來的請求會互相干擾:一個請求還沒做完,另一個就可能把它的修改一起 commit 或還原掉。
第 5 課提過 g 只活在一次請求,這裡正好用上。teardown_appcontext 的函數在錯誤發生時也會執行,連線一定會被關閉。
逐步看:用 schema.sql 建立資料表
把建立資料表的 SQL 放在獨立的 schema.sql(在輸出區的檔案分頁),程式啟動時執行一次。這段程式執行完,請到檔案分頁確認 schema.sql 的內容。
參數化查詢
把使用者輸入放進 SQL 只有一種正確的寫法:
# 正確:問號+參數
db.execute('SELECT * FROM notes WHERE id = ?', (note_id,))
# 有問題的寫法:把值直接接進字串
db.execute(f'SELECT * FROM notes WHERE id = {note_id}')
用字串接起來的話,輸入裡的引號會被當成 SQL 的一部分。輕則像下面的練習一樣直接出錯,重則被人改變查詢的意思,這叫 SQL 注入,第 17 課會實際操作。
參數化查詢把 SQL 和值分開送給資料庫,值再怎麼寫都只會被當成值。? 只能放值,不能放表名和欄位名。
資料庫面板看到的資料,和用 SQLiteStudio 開檔案看到的是同一份。
動手做:新增與查詢筆記
get_db 和資料表都寫好了。完成兩個 view 函數(程式最下面的測試不要改):
add_note:用參數化查詢寫入一筆,確認寫入,回傳新資料的編號(字串)get_note:用參數化查詢找出這一筆,找不到時回 404,找到時回傳標題
SQLite 夠用嗎
SQLite 官方的說法是:多數流量中低的網站,SQLite 都很合適。它的限制在於同一時間只有一個程式能寫入,而且資料庫要和程式放在同一台機器上。
| 情況 | 選擇 |
|---|---|
| 課堂作業、個人專案、小型網站 | SQLite |
| 資料和程式在同一台機器,寫入不頻繁 | SQLite |
| 很多使用者同時大量寫入 | MySQL、PostgreSQL |
| 好幾台伺服器要連同一個資料庫 | MySQL、PostgreSQL |
| 需要不同帳號各有不同權限 | MySQL、PostgreSQL |
SQL 語法大多相通,要換時改的主要是連線方式、參數符號(MySQL 的 pymysql 用 %s)與部分資料型別。MySQL 的用法可以到 SQL 軌學習。
想一想
下面的程式會印出什麼?
import sqlite3
con = sqlite3.connect("site.db")
con.execute("CREATE TABLE t (x)")
con.execute("INSERT INTO t VALUES (1)")
con.close()
con = sqlite3.connect("site.db")
print(con.execute("SELECT COUNT(*) FROM t").fetchone()[0])
下面的程式會丟出什麼例外?
import sqlite3
con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE t (id INTEGER)")
con.execute("SELECT * FROM t WHERE id = ?", (5))
下面的程式會印出什麼?
import sqlite3
con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE t (id INTEGER)")
print(con.execute("SELECT * FROM t WHERE id = ?", (5,)).fetchone())
為什麼連線要放在 g 裡,而不是開一條全域變數讓所有請求共用?
下面哪一種情況,最需要從 SQLite 換成 MySQL 這類資料庫?
重點整理
- 串列在記憶體,重新啟動就不見;資料庫在磁碟
connect→execute→fetchone/fetchall;有修改要commit;row_factory = sqlite3.Row才能用欄位名稱取值- 每個請求一條連線:
get_db放進g,teardown_appcontext負責關閉 schema.sql+executescript建立資料表,寫IF NOT EXISTS才能重複執行- 使用者輸入一律用
?參數化,只有一個值時 tuple 要寫逗號 - SQLite 適合單機、寫入不多的網站;多台伺服器、大量同時寫入時換 MySQL 這類資料庫
下一課用這些工具做出完整的留言板:新增、查詢、修改、刪除,以及出錯時怎麼處理。