🚀 Day 7:資料庫與模型設計(Model)

5 天營隊第二週 (Day 7 / 總時數 6 Hours) | 告別 JSON 文字檔,擁抱 SQLite 資料庫與 Django ORM 站長後台!

📖 為什麼學 these?——「理財咖啡館打造資料庫與站長後台」關卡冒險故事線

💡 故事情境:告別單一文字檔,建構高效安全的專業資料庫!

雖然 expenses.json 很好用,但如果同時有 100 個人一起記帳,檔案會因為被搶著讀寫而鎖死損毀!今天我們要幫理財咖啡館升級為 **關聯式資料庫 (SQLite)**,使用 Django ORM (Object-Relational Mapping) 在 `models.py` 中設計 Schema,並登入超有質感的內建 Admin 站長後台管理所有資料!

單元名稱 記帳程式關卡角色 為什麼這個單元不可或缺?(技術關聯)
單元 1:從 JSON 到真正的資料庫 🗄️ 升級專業防鎖死資料庫檔案櫃 瞭解關聯式資料庫 (RDBMS) 的好處,說明為什麼多使用者併發寫入時需要 SQLite 來替代 Day 4 的 JSON 檔案。
單元 2:定義 Django Model 📐 資料庫表格建構藍圖 (models.py) models.py 撰寫 Python 類別 (Class),定義品項、金額、分類與時間欄位,對應 Day 2 的 List of Dicts 結構。
單元 3:資料庫遷移 (Migrations) 🏗️ SQL 翻譯官與工程施工 (migrate) 執行 makemigrationsmigrate,將 Python 藍圖翻譯成真正的資料庫表格,對應 Day 4 的防呆建檔。
單元 4:超級管理員與 Admin 後台 👑 網頁版站長後台 (admin.py) 建立 createsuperuser 並註冊 Model,不用寫前端就能登入高質感網頁後台進行完整的網頁 CRUD 操作!
單元 5:ORM 基礎操作 (Shell) 🔮 不用寫 SQL 的 Python 魔法 (Shell) 開啟 manage.py shell,用純 Python 語法(.objects.all(), .filter())對 SQLite 進行資料新增與查詢。
單元 6:資料轉移黑客松 🚚 無痛移民:JSON 自動搬家腳本 撰寫一小段 Python 轉移腳本,自動讀取 Day 4 的 expenses.json,一秒匯入 SQLite 資料庫中!

📌 課程前導與簡介說明

🎓 教師心法與教學策略 (Teacher Only)
  • 營隊定位:從純檔案讀寫過渡到專業資料庫 (ORM)。讓高中生明白不用背 SQL 語法也能掌控 SQLite 資料庫。
  • 高中生心理亮點:當學生登入 127.0.0.1:8000/admin/,看到自己設計的欄位出現在高質感後台,還能親手點擊新增、修改時,會獲得極高成就感!
  • 教學銜接點:不斷對照舊觀念(Model 對應 Day 2 的 Dictionary Keys;ORM 對應 Day 2 的 filter 迴圈;後台 CRUD 對應 Day 4 & Day 5)。
🌟 學生自主學習指引 (Student Self-Study)

歡迎來到 Day 7!今天我們要把記帳資料庫真正建立起來!請閱讀每個單元的 **故事單元說明**,點擊 **延伸閱讀** 展開深層觀念與 Google 搜尋,並善用每區塊的 **AI 協作除錯提示詞** 來練習寫碼!

💻 Day 7 專案總成果:Django Model 定義與 JSON 資料匯入腳本

【教師參考解答】學生於 Day 7 結束時將完成的 Model 與匯入腳本:

1. 記帳資料模型 (`expenses/models.py`):

from django.db import models

class Expense(models.Model):
    """ 記帳模型 (對應 DB 的 Table) """
    item = models.CharField(max_length=100, verbose_name="品項名稱")
    price = models.IntegerField(verbose_name="消費金額")
    category = models.CharField(max_length=50, default="一般", verbose_name="消費分類")
    date = models.DateField(auto_now_add=True, verbose_name="紀錄日期")

    def __str__(self):
        return f"{self.item} - ${self.price} ({self.category})"

    class Meta:
        verbose_name = "消費紀錄"
        verbose_name_plural = "消費紀錄清單"

2. 後台註冊 (`expenses/admin.py`):

from django.contrib import admin
from .models import Expense

@admin.register(Expense)
class ExpenseAdmin(admin.ModelAdmin):
    list_display = ('id', 'item', 'price', 'category', 'date') # 定義列表中顯示的欄位
    list_filter = ('category', 'date')                         # 側邊過濾器
    search_fields = ('item',)                                  # 搜尋列

3. JSON 資料轉移腳本 (`import_json.py` 或在 `manage.py shell` 執行):

import json
from expenses.models import Expense

def migrate_json_to_db():
    try:
        with open("expenses.json", "r", encoding="utf-8") as f:
            data = json.load(f)
            count = 0
            for r in data:
                # 使用 ORM 建立資料列
                Expense.objects.create(
                    item=r.get("item", r.get("name", "未命名")),
                    price=r.get("price", 0),
                    category=r.get("category", "一般")
                )
                count += 1
            print(f"🎉 成功將 {count} 筆舊 JSON 資料匯入 SQLite 資料庫!")
    except FileNotFoundError:
        print("⚠️ 找不到舊的 expenses.json 檔案!")

# 執行函式
migrate_json_to_db()

【學生自主實作框架】請根據今日各單元所學,將空缺的 `____` 填入正確的 Django Model 語法:

# 🎯 Day 7 自主挑戰:請填入 models.py 與 admin.py 空缺的 ____ 關鍵字!

# --- [檔案: expenses/models.py] ---
from django.db import models

class Expense(models.____): # 提示:繼承 Django 的 Model 類別
    item = models.____(max_length=100) # 提示:文字字串欄位 CharField
    price = models.____()              # 提示:整數數字欄位 IntegerField
    category = models.CharField(max_length=50, default="一般")

    def __str__(self):
        return f"{self.item} - ${self.price}"

# --- [檔案: expenses/admin.py] ---
from django.contrib import admin
from .models import ____ # 提示:匯入剛建好的 Model

admin.site.____(Expense) # 提示:註冊 Model 到 admin 後台的函式 register

⏰ 單元 1:從 JSON 到真正的資料庫(09:00 - 10:00)

🗄️ 故事單元說明:升級專業防鎖死資料庫檔案櫃

在 Week 1 我們使用 expenses.json 儲存資料,這就像把帳記在「純文字筆記本」上。但如果有多個使用者同時寫入,文字檔會因為被搶著存取而鎖死損毀!關聯式資料庫 (SQLite) 就像是「帶有智能防鎖鎖頭的防火保險箱」,能確保多線程讀寫的安全(ACID 特性),並且查詢速度快上幾百倍!

💡 深度觀念:為什麼 Web 應用需要關聯式資料庫 (RDBMS)?

JSON 檔案與專業資料庫 (SQLite/PostgreSQL) 的根本差異:

  • ACID 安全特性:原子性 (Atomicity)、一致性 (Consistency)、隔離性 (Isolation)、持久性 (Durability),保證交易不會算錯錢。
  • SQLite 檔案型資料庫:Django 預設的 SQLite 是無需設定伺服器的輕量級 SQL 資料庫,整個資料庫就是一個 db.sqlite3 檔案。

🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):

🤖 AI 自主學習探索提示詞 (Prompt):

探索 Prompt (點擊複製)

請向高中生說明為什麼大型 Web 應用程式不能只用 JSON 檔案儲存資料,而需要採用 SQLite 或 PostgreSQL 關聯式資料庫,並解釋什麼是資料庫的 ACID 特性。

🎓 教師備課手冊 (Teacher Only)
  • 講解 JSON 檔案併發讀寫被鎖死 (Lock) 的局限。
  • 介紹關聯式資料庫 (RDBMS) 與 SQLite 的優勢。
  • 概念連結:Day 4 的 JSON 持久化寫檔進化為資料庫。
💻 觀念對映:Django settings.py 中的預設資料庫設定
# myproject/settings.py 預設使用 SQLite3
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3', # 專案根目錄下的 db.sqlite3 檔案
    }
}
⚠️ 常見陷阱與問題案例
  • 直接用記事本打開 db.sqlite3 看到亂碼db.sqlite3 是二進位 SQLite 資料庫檔案,需透過 Django ORM 或專用工具 (如 SQLite Viewer) 開啟。
🤖 單元 1 常見問題 AI 協作除錯提示詞

我用 VS Code 純文字編輯器打開 db.sqlite3 檔案發現裡面全是亂碼。請告訴我為什麼 SQLite 是二進位檔案,以及該如何在 VS Code 安裝套件來查看裡面的 Table。

⏰ 單元 2:定義 Django Model(10:00 - 11:00)

📐 故事單元說明:資料庫表格建構藍圖 (models.py)

我們要怎麼告訴資料庫「記帳表格有哪些欄位」呢?在 Django 中,我們不需要撰寫複雜的 SQL 指令,只要在 expenses/models.py 撰寫一個 Python 類別 class Expense(models.Model)!文字欄位用 CharField,金額欄位用 IntegerField,時間用 DateField,完美對應 Day 2 的 Dictionary Keys!

💡 深度觀念:Model 欄位型別與魔術方法 `__str__`

Django Model 透過類別屬性定義表格欄位規格:

  • 常用欄位型別CharField(max_length=...) (短字串)、IntegerField() (整數)、DateField(auto_now_add=True) (自動填入當前日期)。
  • 魔術方法 __str__(self):定義物件在被 print() 或在 Admin 後台顯示時的純文字代表字串。

🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):

🤖 AI 自主學習探索提示詞 (Prompt):

探索 Prompt (點擊複製)

請向初學者說明 Django models.py 中 CharField, IntegerField 與 DateField 的使用方法,並解釋為什麼要在 Model 類別裡面定義 __str__() 魔術方法。

🎓 教師備課手冊 (Teacher Only)
  • 編輯 expenses/models.py 定義 Expense 類別。
  • 介紹 CharField, IntegerField, DateField__str__
  • 概念連結:對應 Day 2 List of Dicts 結構 (Keys 變為 Model Fields)。
💻 程式碼段落 (`expenses/models.py`)
from django.db import models

class Expense(models.Model):
    item = models.CharField(max_length=100) # 品項名稱
    price = models.IntegerField()           # 消費金額
    category = models.CharField(max_length=50, default="一般") # 分類
    date = models.DateField(auto_now_add=True) # 自動寫入建立時的日期

    def __str__(self):
        return f"{self.item} - ${self.price}"
⚠️ 常見陷阱與問題案例
  • CharField 漏寫 max_lengthCharField 必須指定 max_length 參數,否則執行遷移時會拋出 TypeError: __init__() missing 1 required positional argument: 'max_length'
🤖 單元 2 常見問題 AI 協作除錯提示詞

我在 models.py 中寫 CharField 時出現了 TypeError: missing 1 required positional argument: 'max_length'。請教我如何正確加上 max_length 參數。

⏰ 單元 3:資料庫遷移 (Migrations)(11:00 - 12:00)

🏗️ 故事單元說明:SQL 翻譯官與工程施工 (migrate)

寫完 models.py 之後,SQLite 資料庫還看不懂 Python 程式碼!我們需要兩道神奇的指令:
1. python manage.py makemigrations:把 Python 類別翻譯成建築施工藍圖(Migration 檔案)
2. python manage.py migrate:按照藍圖,真正跑去 SQLite 資料庫裡面把表格實體建造出來!

💡 深度觀念:什麼是資料庫遷移 (Database Migration)?

Migration 是 Django 追蹤與同步 Model 變更的版控機制:

  • makemigrations:檢查 models.py 的修改,在 expenses/migrations/ 目錄產出如 0001_initial.py 的藍圖檔。
  • migrate:將藍圖轉化為 SQL 語法 (如 `CREATE TABLE`) 並在資料庫執行,完成 Schema 更新。

🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):

🤖 AI 自主學習探索提示詞 (Prompt):

探索 Prompt (點擊複製)

請詳細說明 Django 的 makemigrations 與 migrate 兩者之間的差別,並用『建築施工藍圖』與『實際工地建造』來做比喻。

🎓 教師備課手冊 (Teacher Only)
  • 執行 python manage.py makemigrations
  • 執行 python manage.py migrate
  • 帶學生觀察生成檔 expenses/migrations/0001_initial.pydb.sqlite3
💻 Terminal 指令段落
# 1. 產出資料庫變更藍圖檔 (Migration File)
python manage.py makemigrations

# 2. 正式施工套用至 db.sqlite3 資料庫
python manage.py migrate
⚠️ 常見陷阱與問題案例
  • No changes detected:修改了 `models.py` 執行 `makemigrations` 顯示無變更 $\rightarrow$ 檢查忘記把 App 註冊到 `settings.py` 的 `INSTALLED_APPS` 中。
🤖 單元 3 常見問題 AI 協作除錯提示詞

我在執行 makemigrations 時顯示 No changes detected 但我明明寫好了 models.py。請幫我檢查 settings.py 的 INSTALLED_APPS 是否忘記註冊 App。

⏰ 單元 4:超級管理員與 Admin 後台(13:00 - 14:00)

👑 故事單元說明:網頁版站長後台 (admin.py)

Django 最吸引人的黑科技就是內建了「超級站長管理後台」!我們執行 python manage.py createsuperuser 設定管理員密碼,接著在 expenses/admin.py 加上 admin.site.register(Expense)。前往 http://127.0.0.1:8000/admin/ 登入,不用寫半行 HTML 前端,就能像站長一樣在網頁畫面上對記帳資料進行完整的 CRUD 增刪查改!

💡 深度觀念:Django Admin 自訂後台展示

Django Admin 是生產力極高的開箱即用工具:

  • admin.site.register(Model):將 Model 註冊給 Admin 後台託管。
  • 自訂 ModelAdmin 類別:透過 list_display 控制顯示欄位,透過 list_filter 啟用側邊過濾列,透過 search_fields 啟用搜尋框。

🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):

🤖 AI 自主學習探索提示詞 (Prompt):

探索 Prompt (點擊複製)

請說明如何將自訂的 Django Model 註冊到 admin.py,並示範使用 list_display, search_fields 與 list_filter 來美化站長管理後台列表。

🎓 教師備課手冊 (Teacher Only)
  • 在 Terminal 執行 python manage.py createsuperuser 設定帳密。
  • expenses/admin.py 中註冊 Expense 模型。
  • 帶學生開啟 http://127.0.0.1:8000/admin/ 登入體驗 UI 後台 CRUD!
💻 程式碼段落 (`expenses/admin.py`) 與 Terminal 指令
# 1. Terminal 建立管理員帳號
# python manage.py createsuperuser

# 2. 編輯 expenses/admin.py 註冊 Model
from django.contrib import admin
from .models import Expense

@admin.register(Expense)
class ExpenseAdmin(admin.ModelAdmin):
    list_display = ('id', 'item', 'price', 'category', 'date')
    list_filter = ('category',)
    search_fields = ('item',)
⚠️ 常見陷阱與問題案例
  • 忘記密碼或密碼太簡單:`createsuperuser` 輸入密碼時螢幕不會顯示任何字元(保護機制),直接敲完按 Enter 即可。
🤖 單元 4 常見問題 AI 協作除錯提示詞

我在執行 createsuperuser 輸入密碼時螢幕完全沒有顯示字元。請告訴我這是 Linux/Terminal 的安全特性,以及忘記密碼時如何用 changepassword 指令重置。

⏰ 單元 5:ORM 基礎操作 (Shell)(14:00 - 15:00)

🔮 故事單元說明:不用寫 SQL 的 Python 魔法 (Shell)

我們不用去死背複雜的 SQL 語法 SELECT * FROM ...!Django 提供了強大的 **ORM (Object-Relational Mapping)**,讓你可以直接用 Python 語法對資料庫進行控制!執行 python manage.py shell,輸入 Expense.objects.all() 抓出所有資料,或輸入 Expense.objects.filter(category='餐飲') 進行過濾,體驗 Python 指令直接操作 SQLite 的快感!

💡 深度觀念:什麼是 ORM 與 QuerySet 惰性載入?

ORM 充當了 Python 物件與關聯式資料庫之間的橋樑:

  • 常用 QuerySet 方法.all() (取全部)、.filter(key=val) (條件過濾)、.get(id=1) (取單筆)、.create(...) (新建並寫入)。
  • 惰性求值 (Lazy Evaluation):QuerySet 在建立時不會立刻查詢資料庫,直到你真正需要印出或走訪它時才會執行 SQL 查詢。

🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):

🤖 AI 自主學習探索提示詞 (Prompt):

探索 Prompt (點擊複製)

請向初學者解釋什麼是 Django ORM,並示範在 manage.py shell 中使用 .objects.all(), .filter(), .get() 與 .create() 進行資料庫 CRUD 操作。

🎓 教師備課手冊 (Teacher Only)
  • 在 Terminal 執行 python manage.py shell 進入 Python 互動環境。
  • 示範 ORM CRUD 指令:Expense.objects.create(...)Expense.objects.all().filter(...)
  • 概念連結:對應 Day 2 清單演練與 filter 分類搜尋。
💻 Shell 互動指令段落
# 在 python manage.py shell 內部執行:
from expenses.models import Expense

# 1. [C]reate 新增一筆資料
Expense.objects.create(item="排骨飯", price=110, category="餐飲")

# 2. [R]ead 查詢所有資料 (回傳 QuerySet)
all_data = Expense.objects.all()
print(all_data)

# 3. 條件過濾 (Filter)
food_data = Expense.objects.filter(category="餐飲")
print(food_data)
⚠️ 常見陷阱與問題案例
  • 忘記 import Model:直接在 shell 輸入 `Expense.objects.all()` 跳出 `NameError: name 'Expense' is not defined` $\rightarrow$ 記得先寫 `from expenses.models import Expense`。
🤖 單元 5 常見問題 AI 協作除錯提示詞

我在 manage.py shell 裡面執行 Expense.objects.all() 時跳出 NameError: name 'Expense' is not defined。請提醒我如何先從 expenses.models 匯入 Expense 類別。

⏰ 單元 6:資料轉移黑客松 (JSON 到 DB 自動匯入)(15:00 - 16:00)

🚚 故事單元說明:無痛移民:JSON 自動搬家腳本

前一週大家在 expenses.json 裡面記下的舊帳務資料怎麼辦?難道要手動重新輸入嗎?當然不!在這個黑客松單元中,我們寫一小段 Python 轉移腳本:結合 Day 4 的 json.load() 讀取檔案,並搭配今天剛學的 Expense.objects.create(),一秒鐘自動把所有歷史資料搬家移籍進 SQLite 資料庫裡!

💡 深度觀念:資料轉移與批量寫入效能優化

將外部 JSON/CSV 搬家至 DB 是常見的工程需求:

  • 欄位相容處理 dict.get('key', default):預防舊 JSON 檔案缺欄位造成的 KeyError
  • 批量寫入 bulk_create():一次性送出多筆 SQL `INSERT`,避免在迴圈中做數千次資料庫連線 I/O。

🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):

🤖 AI 自主學習探索提示詞 (Prompt):

探索 Prompt (點擊複製)

我有一份舊的 expenses.json 記帳資料,請教我如何撰寫一段 Python/Django 腳本,讀取 JSON 後自動透過 ORM 的 Expense.objects.create() 寫入 SQLite 資料庫。

🎓 教師備課手冊 (Teacher Only)
  • 輔導學生撰寫轉移腳本(可在 `manage.py shell` 中貼上執行)。
  • 結合 Day 4 的 json.load() 讀檔與今日 ORM Expense.objects.create()
  • 帶學生開 127.0.0.1:8000/admin/ 驗證舊資料成功移民至 SQLite!
💻 轉移腳本程式碼段落
# 轉移腳本關鍵邏輯 (在 manage.py shell 中執行)
import json
from expenses.models import Expense

with open("expenses.json", "r", encoding="utf-8") as f:
    old_data = json.load(f)
    for row in old_data:
        Expense.objects.create(
            item=row.get("item", row.get("name")),
            price=row.get("price", 0),
            category=row.get("category", "一般")
        )
print("✅ 所有歷史 JSON 帳務資料已無痛匯入 SQLite 資料庫!")
⚠️ 常見陷阱與問題案例
  • 舊 JSON 字典 Key 欄位不對應:例如舊檔寫 `"name"` 但 Model 欄位定義為 `item` $\rightarrow$ 善用 row.get("item", row.get("name")) 進行安全轉換。
🤖 單元 6 常見問題 AI 協作除錯提示詞

我在匯入舊 JSON 資料至 Django Model 時跳出了 KeyError: 'item'。請教我如何用 dict.get() 來預防欄位名稱不一致的問題。