後端
整體後端大致工作流程為
- ORM 模型設計
- serializer 開發
- View 開發
- URL 路由配置
- API 測試與驗證(Postman / DRF)
ER Model
✅ Product 與其他表的關聯:
- ProductImage(產品圖片) → 一對多 (One-to-Many)
- Specification(產品規格) → 一對多 (One-to-Many)
- ProductTag(產品標籤關聯表) → 多對多 (Many-to-Many)
- Tag(標籤) → 透過 ProductTag 建立多對多關係
2️⃣ ProductImage(產品圖片表)
ProductImage 和 Product → 一對多 (One-to-Many)
- 一個產品可以有多張圖片
- 每張圖片只屬於一個產品
- is_primary 用來標記主圖
3️⃣ Specification(產品規格表)
Specification 和 Product → 一對多 (One-to-Many)
- 一個產品可以有多個規格(例如顏色、容量等)
- 每個規格只對應到一個產品
4️⃣ Tag(標籤表)
Tag 和 Product → 多對多 (Many-to-Many)(透過 ProductTag 來關聯)
📌 關聯: ProductTag 建立產品與標籤的多對多關係
- 一個產品可以有多個標籤
- 一個標籤也可以對應到多個產品
- 需要透過 ProductTag 作為關聯表來管理多對多關係
5️⃣ ProductTag(產品標籤關聯表)
ProductTag 和 Product、Tag → 多對多 (Many-to-Many)
為何需要拆分成 ProductTag 及 Tag 兩張資料表?
Product 與 ProductTag 資料表是多對多關係,而這多對多需要透過 ProductTag 來建立關聯,才能讓 Product 與 Tag 之間建立多對多關係。
錯誤範例
如果我們不使用 tags 表,而是直接在 product_tags 存放標籤文字,資料表可能會長這樣:
d | product_id | tag |
---|---|---|
1 | 101 | 有機 |
2 | 101 | 純素 |
3 | 102 | 有機 |
4 | 103 | 環保 |
5 | 104 | 有機 |
這樣的問題是:
- 標籤文字會重複出現(例如 有機 出現了 3 次)
- 難以變更標籤名稱——如果我們要將所有 有機 改成 有機認證,需要更新所有相關記錄,這在大規模應用時效率低下
- 不方便統計與管理——例如,我們想知道有哪些產品用了 有機 這個標籤,查詢可能會變慢,因為它不是 id 參照
正確範例
使用 tags
表 + product_tags
關聯表,將標籤名稱存到 tags 表,然後讓 product_tags 只存對應的 tag_id:
✅ Tags 表(只存 唯一標籤):
id | name |
---|---|
1 | 有機 |
2 | 純素 |
3 | 環保 |
✅ Product_tags 表(存產品與標籤的關係):
id | product_id | tag_id |
---|---|---|
1 | 101 | 1 |
2 | 101 | 2 |
3 | 102 | 1 |
4 | 103 | 3 |
5 | 104 | 1 |
優點:
- 標籤不會重複存放,有機 只會出現在 tags 表 一次,但可以透過 product_tags 關聯到多個產品。
- 修改標籤更容易,如果我們想把 有機 改成 有機認證,只需要在 tags 表修改一筆記錄,而不是更新所有 product_tags 的行。
- 查詢效率更高,可以用 JOIN 查詢特定標籤的所有產品,而不需要搜尋 text 欄位,這在大數據場景下是更高效的做法。
注意在設計欄位名稱時,不需要將 ForeignKey 欄位的名稱改為 product_id
和 tag_id
,直接使用 product 和 tag 會比較 Pythonic,並符合 Django 的命名習慣。
當使用 ForeignKey 時,Django 就會自動會在資料庫內部建立 product_id 和 tag_id 欄位。
例如:
1 | class ProductTag(models.Model): |
Django 會自動在資料庫中創建:
1 | CREATE TABLE product_tag ( |
為何 Image 欄位不直接放在 Product 資料表中?
首先重點:關聯式資料庫的一個基本原則是 「一個欄位應該只存一個值」
錯誤範例
傳統最法會直接 Product 資料表中加入 image 欄位,若有多張圖再用逗號區隔,但這樣的做法有以下問題:
- 考慮到一個產品可能除了主圖外還有多張說明、副圖,如果直接在 Product 資料表中加入 image 欄位,並用逗號分隔(千萬不要),這種做法在關聯式資料庫 (RDBMS) 中會帶來很多問題!
- 難以查詢、篩選、排序或過濾單張圖片,如果你想取得第一張圖片,就得手動切割字串
- 更新不方便,如果要刪除其中一張圖片,得重新存整個字串
正確範例
把所有的圖片都放到 ProductImage,然後增加 is_primary 欄位來標記主圖。 這樣可以:
✅ Product 表(產品表)
id | name | category | created_at | updated_at |
---|---|---|---|---|
1 | 電視 | 3C | 2025-03-10 12:00:00 | 2025-03-10 12:30:00 |
2 | 水壺 | 日常用品 | 2025-03-10 12:10:00 | 2025-03-10 12:40:00 |
✅ ProductImage 表(產品圖片表)
id | product_id | image_url | is_primary | order |
---|---|---|---|---|
1 | 1 | https://example.com/image1.jpg | TRUE | 0 |
2 | 1 | https://example.com/image2.jpg | FALSE | 1 |
3 | 1 | https://example.com/image3.jpg | FALSE | 2 |
4 | 2 | https://example.com/image4.jpg | TRUE | 0 |
5 | 2 | https://example.com/image5.jpg | FALSE | 1 |
這樣設計才能確保圖片關聯性、提高查詢效率、方便 API 回傳 JSON、未來可以支援更多圖片類型(輪播圖、縮圖、主圖)
ORM 模型
最終的 ORM 模型如下:
1 | # api/products/models.py |
ProductTag
為建立 Product
和 Tag
之間的多對多關係中間表,負責儲存產品ID和標籤ID之間的關聯
cor-header 設定
開始之前,需先設定 CORS Header,讓前端可以存取後端 API
serializer 設計
1 | # api/products/serializers.py |
view 設計
使用 DRF 的 ListAPIView
來實作產品列表的 API,並且使用 ProductSerializer
來序列化資料
1 | # api/products/views.py |
url 路由
1 | # api/products/urls.py |
而此路由檔案則會存放所有不同功能的路由
1 | # backend/urls.py |
註冊 DRF
若沒在 INSTALLED_APPS
中註冊 DRF,會發現有 TemplateDoesNotExist at /api/products/
的錯誤
1 | # settings.py |
此時再去訪問 http://127.0.0.1:8000/api/products/
才能看到 DRF 的 API 界面
前端
要改的地方有大約有以下幾個:
axiosInstance API 請求路由更新
從原本的 dummydata 暫時改成 Django 的預設路由,等後續申請網站網域的時再更改
1 | // api/axiosInstance.ts |
型別定義更新
由於原本的型別定義是根據 DummyData 設計的,現在要改成根據 Django API 的回傳結果來設計
1 | // types/products.ts |
更新產品 API 請求路由
型別 interface
的名稱就從原本的 ProductListResponse
改成 Product
,並且將路由改成 Django API 的路由
1 | // api/products.ts |
hook 更新
更新 useProducts hook
,確保它返回正確的型別
1 | // hooks/useProducts.ts |
前端頁面更新
此處是寫法差異最多的地方,因為原本的 dummydata 的 API 規格為:
dummydata 的 API 規格是直接在 products
陣列中放置所有產品
1 | [ |
而自己寫的 Django API 則是將產品放在陣列中,並以數字作為 key
1 | [ |
API 規格差異比對圖:
dummydata API 的寫法(舊)
1 | // pages/product-page.tsx |
Django API 的寫法(新)
主要差別就在由於 API 回傳的資料結構不同,所以 data 也從原本的 data?.products
改成 data?.map
,並且要確保 data
是陣列才進行 map
1 | // pages/product-page.tsx |
主要變更如下:
- 資料結構變更:
- 原本:data?.products.map((product: Product)
- 現在:data?.map((product)
- 移除了.products 的引用,因為新 API 直接返回產品陣列
- 屬性名稱變更:
- 原本:product.title → 現在:product.name
- 原本:product.thumbnail → 現在:product.images[0].image_url
- 圖片處理優化:
- 加了圖片是否存在的檢查:product.images && product.images.length > 0 && product.images[0].image_url
- 確保即使沒有圖片資訊也不會出錯
成果
若您覺得這篇文章對您有幫助,歡迎分享出去讓更多人看到⊂◉‿◉つ~
留言版