Python
Python 資料庫操作入門:SQLite、CRUD 與防 SQL Injection
Python 資料庫操作入門:SQLite、CRUD 與防 SQL Injection
你的 Python 程式跑起來很順,但資料存在哪裡?如果答案是「存成一個 JSON 檔案」,是時候認識資料庫了。這篇帶你用 Python 內建的 SQLite 建立資料庫、寫基本的 CRUD 指令,學會用參數化查詢擋掉最常見的資安漏洞 SQL Injection,最後看懂 ORM(SQLAlchemy)到底在幫你做什麼。
跟 AI 一起寫資料庫程式時,最常出現的三個雷是忘記 commit、忘記關閉連線、還有用字串拼接組 SQL。學完這篇,你會有一句隨時能用的口訣,一眼看穿 AI 寫出來的 SQL 有沒有問題。
你將學到什麼
SQLite 連線與建表
用 Python 內建模組建立資料庫,不用另外安裝伺服器。
SQL 四大指令 CRUD
新增、查詢、更新、刪除,一次學會參數化的正確寫法。
防 SQL Injection
看懂字串拼接的風險,學會用問號佔位符寫安全的查詢。
ORM 概念入門
用 SQLAlchemy 的 Python 物件操作資料庫,不用手寫 SQL。
AI 常見資料庫陷阱
忘記 commit、忘記關連線、用索引取欄位,三個雷一次看懂。
SQLite 連線與基本操作
SQLite 是 Python 內建的輕量資料庫,不需要另外安裝伺服器,一個 .db 檔就是一個完整的資料庫,最適合學習和小型專案。
import sqlite3
# 連線:檔案不存在會自動建立
conn = sqlite3.connect("students.db")
cursor = conn.cursor()
# 建立資料表,IF NOT EXISTS 代表表已存在時不會報錯
cursor.execute("""
CREATE TABLE IF NOT EXISTS students (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
score INTEGER DEFAULT 0
)
""")
conn.commit() # 寫入操作一定要 commit
conn.close()
# 推薦寫法:用 with 自動管理連線
with sqlite3.connect("students.db") as conn:
conn.row_factory = sqlite3.Row # 讓查詢結果可用欄位名稱存取
cursor = conn.cursor()
cursor.execute("SELECT * FROM students")
rows = cursor.fetchall()
cursor.fetchall()預設回傳一串 tuple;設定 conn.row_factory = sqlite3.Row 之後改回傳 Row 物件,可以用欄位名稱取值,例如 row 的 name,可讀性大幅提升。
SQL 四大指令:CRUD
with sqlite3.connect("students.db") as conn:
cursor = conn.cursor()
# CREATE:新增,用問號參數化,防止 Injection
cursor.execute(
"INSERT INTO students (name, score) VALUES (?, ?)",
("Alice", 92)
)
conn.commit() # 寫入一定要 commit
# READ:查詢
cursor.execute("SELECT * FROM students WHERE score >= ?", (60,))
rows = cursor.fetchall()
# UPDATE:更新
cursor.execute(
"UPDATE students SET score = ? WHERE name = ?",
(95, "Alice")
)
conn.commit()
# DELETE:刪除
cursor.execute("DELETE FROM students WHERE score < ?", (60,))
conn.commit()
# 批次新增:executemany
data = [("Bob", 78), ("Carol", 85)]
cursor.executemany(
"INSERT INTO students (name, score) VALUES (?, ?)", data
)
conn.commit()
SELECT 不需要 commit;INSERT、UPDATE、DELETE 都需要。AI 有時會忘記 commit,資料看起來沒有存入,看到寫入操作就要確認後面有沒有接 conn.commit()。
參數化查詢:防 SQL Injection
SQL Injection 是讓攻擊者能夠輸入惡意 SQL 指令的漏洞。參數化查詢(用問號佔位符)是唯一正確的防護方式,AI 生成的初版程式常用字串拼接,你必須看得出來並要求修正。
把使用者輸入直接接進 SQL 字串,輸入內容就變成可以被執行的指令。
name = input("name: ")
sql = "SELECT * FROM students WHERE name = '" + name + "'"
cursor.execute(sql)
# 輸入 ' OR '1'='1 會查出所有資料
# 輸入 '; DROP TABLE students; -- 會直接刪除整張表
SQLite 會自動處理跳脫,惡意輸入不會被當作 SQL 指令執行。
name = input("name: ")
cursor.execute("SELECT * FROM students WHERE name = ?", (name,))
SQL 字串裡出現加號做字串拼接、用百分比格式化、或把變數直接塞進 SQL 字串,這三種都是危險寫法,一律改成問號佔位符加上一組參數。
「請檢查以下 SQL 程式碼,把所有字串拼接改成參數化查詢,用問號佔位,並加中文說明為什麼這樣更安全。」
ORM 概念:SQLAlchemy 入門
ORM(Object-Relational Mapping)讓你用 Python 物件操作資料庫,不需要手寫 SQL。一個 class 對應一張表,一個實例對應一筆資料。
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import DeclarativeBase, Session
class Base(DeclarativeBase): pass
class Student(Base): # 一個 class 對應一張表
__tablename__ = "students"
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
score = Column(Integer, default=0)
engine = create_engine("sqlite:///students.db")
Base.metadata.create_all(engine) # 自動建表
with Session(engine) as session:
alice = Student(name="Alice", score=92)
session.add(alice)
session.commit()
results = session.query(Student).filter(
Student.score >= 60
).all()
for r in results:
print(r.name, r.score) # 直接用屬性存取
小型專案、一次性查詢,用原生 SQLite 就夠了。需要切換不同資料庫、或處理複雜的物件關係,再用 ORM。記住:ORM 底層仍然是 SQL,懂 SQL 才 debug 得動 ORM 的問題。
AI 常見資料庫陷阱
陷阱一:沒有 commit,資料沒有存入
寫入指令跑完看起來沒錯,但沒有呼叫 conn.commit(),程式結束後資料就消失了。
cursor.execute("INSERT INTO students VALUES (?, ?)", ("Alice", 92))
# 沒有呼叫 conn.commit,程式結束後資料就消失
陷阱二:不用 with,連線資源洩漏
連線開了忘記關,長期運行下去會耗盡連線資源。
conn = sqlite3.connect("db.sqlite")
# 中間做了一堆操作
# 忘記 conn.close,長期運行耗盡連線資源
# 改用:with sqlite3.connect("db.sqlite") as conn:
陷阱三:用索引取欄位,可讀性差
用數字索引取欄位,看程式碼的人完全看不出來取的是哪一欄,資料表欄位順序一調整就整段壞掉。
row = cursor.fetchone()
print(row[0], row[2]) # 這是哪兩個欄位?
設定一次 row_factory,之後全部改用欄位名稱取值,一看就懂。
conn.row_factory = sqlite3.Row
row = cursor.fetchone()
print(row["name"], row["score"]) # 清楚可讀
三道遞進題:讀懂、改寫、抓錯
這三題分別練不同的能力:讀懂是追蹤資料從新增到查詢的完整流程;改寫是把危險的字串拼接改成參數化查詢;抓錯是一次抓出 commit 缺失、Injection 漏洞、連線未關閉這三種最常見的組合問題。
題目一:讀懂(基礎)
import sqlite3
with sqlite3.connect(":memory:") as conn:
conn.row_factory = sqlite3.Row
cur = conn.cursor()
cur.execute("CREATE TABLE t (name TEXT, score INT)")
cur.executemany(
"INSERT INTO t VALUES (?, ?)",
[("A", 90), ("B", 55), ("C", 75)]
)
conn.commit()
cur.execute(
"SELECT * FROM t WHERE score >= ? ORDER BY score DESC",
(60,)
)
for row in cur.fetchall():
print(row["name"], row["score"])
任務:說明冒號加 memory 這種寫法、executemany、row_factory 各自的作用,以及最終輸出結果。
- 冒號加 memory 是記憶體資料庫,只存在於程式執行期間,程式結束即消失,常用於測試。
- executemany 用同一句 SQL 批次執行多筆資料;row_factory 設成 sqlite3.Row 後,結果可以用欄位名稱取值。
- 輸出結果是先印出 C 75,再印出 A 90,因為條件是分數大於等於 60 並按分數由高到低排序,B 的 55 分被過濾掉了。
題目二:改寫(進階)
def search_student(conn, name):
sql = "SELECT * FROM students WHERE name = '" + name + "'"
cursor = conn.cursor()
cursor.execute(sql)
return cursor.fetchall()
「請修正 search_student 函式:第一,字串拼接 SQL 改成參數化查詢,用問號佔位;第二,加上 row_factory 設成 sqlite3.Row;第三,加例外處理,抓到 sqlite3.Error 就回傳空 list;第四,加上 docstring 和中文註解。」
- 改成用問號佔位符的參數化查詢,SQLite 自動處理輸入轉義,不會被注入。
- 設定 conn.row_factory = sqlite3.Row,之後 fetchall 回傳的每一筆都可以用欄位名稱取值。
- 加上例外處理,資料庫發生錯誤時記錄下來並回傳空 list,不讓程式整個崩潰。
題目三:抓錯(高階)
資料庫的錯誤最難發現,程式不會崩潰,但資料沒存進去、查出來是空的、或已經被注入攻擊,往往完全看不出來。
import sqlite3
conn = sqlite3.connect("app.db")
cur = conn.cursor()
def add_user(name, email):
cur.execute(
"INSERT INTO users VALUES ('"
+ name + "', '" + email + "')" # 問題一
) # 問題二:少了什麼?
def get_user(uid):
cur.execute(
"SELECT * FROM users WHERE id=" + str(uid) # 問題三
)
return cur.fetchone()
# 問題四:conn 在哪裡關閉?
任務:找出這段程式碼的四個問題,並說明各自的危險原因。
- 字串拼接組 SQL,有 SQL Injection 漏洞,應改用問號佔位符做參數化查詢。
- INSERT 後沒有呼叫 conn.commit(),資料不會真正存入資料庫。
- 把 uid 轉成字串再拼接,同樣是 Injection 漏洞,應改成問號佔位符加上一組參數。
- conn 在函式外建立卻沒有關閉,長期運行會耗盡連線資源,應改用 with 陳述式管理連線。
重點整理
| 操作 | SQL 指令(參數化) | 要 commit 嗎 |
|---|---|---|
| 新增 | INSERT INTO t VALUES (?, ?) | 要 |
| 查詢 | SELECT * FROM t WHERE col = ? | 不用 |
| 更新 | UPDATE t SET col = ? WHERE id = ? | 要 |
| 刪除 | DELETE FROM t WHERE id = ? | 要 |
有沒有用問號做參數化查詢,用來防 Injection?有沒有 commit,寫入才會真正生效?有沒有用 with 陳述式,用來防止資源洩漏?
模組六完成清單
- 能用 sqlite3 建立連線、建表、執行 CRUD 四種操作
- 知道所有寫入操作後都需要呼叫 conn.commit()
- 能辨識並修正 SQL Injection,把字串拼接改成參數化查詢
- 能設定 row_factory,改用欄位名稱取值
- 了解 ORM 的概念和 SQLAlchemy 的基本用法
- 完成題目一:說明記憶體資料庫、executemany、輸出結果
- 完成題目二:請 AI 修正 SQL Injection 並加上例外處理
- 完成題目三:找出四個資料庫程式碼的問題
