Python

Python 資料庫操作入門:SQLite、CRUD 與防 SQL Injection

Python 操作資料庫最常用內建的 sqlite3 模組:連線、建表、寫入資料都要記得呼叫 conn.commit()才會真正存進去。SQL 一律用問號做參數化查詢,不要用字串拼接,才能擋掉 SQL Injection;需求變複雜時,再考慮用 SQLAlchemy 這類 ORM。
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()
commit 時機

SELECT 不需要 commit;INSERT、UPDATE、DELETE 都需要。AI 有時會忘記 commit,資料看起來沒有存入,看到寫入操作就要確認後面有沒有接 conn.commit()。

參數化查詢:防 SQL Injection

SQL Injection 是讓攻擊者能夠輸入惡意 SQL 指令的漏洞。參數化查詢(用問號佔位符)是唯一正確的防護方式,AI 生成的初版程式常用字串拼接,你必須看得出來並要求修正。

危險寫法:字串拼接 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"...WHERE name='" + 輸入輸入變成可執行指令資料可能被竄改或刪除安全寫法:參數化查詢使用者輸入問號佔位符傳入參數資料庫自動跳脫輸入永遠只是資料
字串拼接讓輸入變成可執行的指令;參數化查詢讓輸入永遠只是資料。
快速辨識危險寫法

SQL 字串裡出現加號做字串拼接、用百分比格式化、或把變數直接塞進 SQL 字串,這三種都是危險寫法,一律改成問號佔位符加上一組參數。

給 AI 的修正提示詞

「請檢查以下 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)  # 直接用屬性存取
ORM 與原生 SQL:何時用哪個

小型專案、一次性查詢,用原生 SQLite 就夠了。需要切換不同資料庫、或處理複雜的物件關係,再用 ORM。記住:ORM 底層仍然是 SQL,懂 SQL 才 debug 得動 ORM 的問題。

AI 常見資料庫陷阱

陷阱一:沒有 commit,資料沒有存入

INSERT 後忘記 commit

寫入指令跑完看起來沒錯,但沒有呼叫 conn.commit(),程式結束後資料就消失了。

cursor.execute("INSERT INTO students VALUES (?, ?)", ("Alice", 92))
# 沒有呼叫 conn.commit,程式結束後資料就消失

陷阱二:不用 with,連線資源洩漏

忘記 conn.close()

連線開了忘記關,長期運行下去會耗盡連線資源。

conn = sqlite3.connect("db.sqlite")
# 中間做了一堆操作
# 忘記 conn.close,長期運行耗盡連線資源
# 改用:with sqlite3.connect("db.sqlite") as conn:

陷阱三:用索引取欄位,可讀性差

row 0、row 2 這種寫法,欄位順序一改就全錯

用數字索引取欄位,看程式碼的人完全看不出來取的是哪一欄,資料表欄位順序一調整就整段壞掉。

row = cursor.fetchone()
print(row[0], row[2])  # 這是哪兩個欄位?
正確寫法:設定 row_factory,用欄位名稱取值

設定一次 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 各自的作用,以及最終輸出結果。

答案拆解
  1. 冒號加 memory 是記憶體資料庫,只存在於程式執行期間,程式結束即消失,常用於測試。
  2. executemany 用同一句 SQL 批次執行多筆資料;row_factory 設成 sqlite3.Row 後,結果可以用欄位名稱取值。
  3. 輸出結果是先印出 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()
給 AI 的提示詞

「請修正 search_student 函式:第一,字串拼接 SQL 改成參數化查詢,用問號佔位;第二,加上 row_factory 設成 sqlite3.Row;第三,加例外處理,抓到 sqlite3.Error 就回傳空 list;第四,加上 docstring 和中文註解。」

答案拆解
  1. 改成用問號佔位符的參數化查詢,SQLite 自動處理輸入轉義,不會被注入。
  2. 設定 conn.row_factory = sqlite3.Row,之後 fetchall 回傳的每一筆都可以用欄位名稱取值。
  3. 加上例外處理,資料庫發生錯誤時記錄下來並回傳空 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 在哪裡關閉?

任務:找出這段程式碼的四個問題,並說明各自的危險原因。

答案拆解:四個問題
  1. 字串拼接組 SQL,有 SQL Injection 漏洞,應改用問號佔位符做參數化查詢。
  2. INSERT 後沒有呼叫 conn.commit(),資料不會真正存入資料庫。
  3. 把 uid 轉成字串再拼接,同樣是 Injection 漏洞,應改成問號佔位符加上一組參數。
  4. 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 並加上例外處理
  • 完成題目三:找出四個資料庫程式碼的問題

延伸學習

把這篇文章分享給需要的人FacebookLINEThreadsX

常見問答

為什麼寫入資料庫後,查不到剛剛新增的資料?
多半是忘記呼叫 conn.commit()。SQLite 的新增、更新、刪除都要 commit 才會真正寫入磁碟;查詢則不需要,這是最容易漏掉的一步。
SQL Injection 是什麼?為什麼一定要用參數化查詢?
SQL Injection 是攻擊者把惡意 SQL 藏在輸入內容裡,讓資料庫執行非預期的指令,嚴重可以整張表被刪掉。只要把變數用問號佔位符傳進 execute,資料庫會自動處理跳脫,就不會被注入。
什麼時候該用 ORM,什麼時候直接寫 SQL 就好?
小型專案或一次性查詢,直接用 sqlite3 就夠了;要切換不同資料庫、或處理比較複雜的物件關聯,再考慮 SQLAlchemy 這類 ORM。ORM 底層仍然是 SQL,懂 SQL 才 debug 得動。
row_factory 是什麼,一定要設定嗎?
設定 conn.row_factory = sqlite3.Row 之後,查詢結果可以用欄位名稱取值,例如 row 的 name 欄位,比用索引取值好讀也不容易對錯欄位,非常建議設定。