本文為 資料庫正規化 基本介紹系列文,第 3 篇:
- 資料庫正規化(Database Normalization)完整指南:新手必讀
- 資料正規化完整指南:從 JSON 數據到結構化資料
- 從 JSON 到資料庫:使用 Django ORM 建構音樂活動管理系統 👈進度
- Django Custom Command 匯入 JSON 資料指南
- Python datetime 格式化指南:從基礎到實戰解析
- 深入理解 Django 的 BASE_DIR
- Django update_or_create() 用法詳解
在現代應用開發中,數據通常以 JSON(JavaScript Object Notation)格式存儲與傳輸。
然而,當我們需要將這些 JSON 數據存入關聯式資料庫時,如何設計高效且易於維護的資料結構便成為關鍵。
本篇文章將以 音樂活動資訊 JSON 檔案 為例,介紹如何:
- 解析 JSON 結構並轉換為適當的關聯式資料庫設計
- 建立 Django ORM 模型來存放與管理數據
- 使用 Python 腳本 讀取並將 JSON 數據匯入 Django 資料庫
透過這些步驟,你將能夠將外部 JSON 數據無縫整合到 Django 專案,實現高效的資料管理與查詢能力。接下來,讓我們開始探索 JSON 資料的結構與對應的資料庫模型設計!
理解 JSON 結構
我們的 JSON 檔案(music_events.json)包含音樂活動的相關資訊,每個活動有以下屬性:
{
"version": "1.4",
"UID": "669eab3226b3240c380463f1",
"title": "新觀點.新世界~Kimball Gallagher 2024台灣巡迴音樂會",
"category": "1",
"showInfo": [
{
"time": "2025/02/19 19:30:00",
"location": "高雄市鳳山區三多一路1號",
"locationName": "衛武營國家藝術文化中心表演廳",
"onSales": "Y",
"price": "",
"latitude": "22.6230179238508",
"longitude": "120.342434118507",
"endTime": "2025/02/19 21:05:00"
}
],
"webSales": "https://www.opentix.life/program/1811063063948378113",
"startDate": "2025/02/19",
"endDate": "2025/02/19",
"hitRate": 34
}
從這份 JSON 檔案可以看出,每個活動有基本資訊(標題、類別、日期等),並包含 showInfo 陣列,其中記錄了各場次的資訊,如時間、地點、是否開放售票等。
ERD 圖與正規化設計
在建立資料庫之前,我們需要確保數據結構的設計是高效且易於維護的。
正規化(Normalization) 是一種數據庫設計方法,透過將數據拆分成不同的表,減少冗餘、確保一致性,並增強數據的完整性。
在我們的案例中,原始的 JSON 數據包含活動資訊、演出場次以及場地資訊,因此我們將其拆分為三張關聯表,並設計 ERD(Entity-Relationship Diagram,實體關聯圖) 來清楚呈現表之間的關係。
根據 JSON 數據,我們確定了三張主要的關聯表:
活動表:events
該表存放音樂活動的基本資訊,例如活動名稱、分類、主辦單位、活動期間等。
| 欄位名稱 | 資料型別 | 說明 |
|---|---|---|
UID (PK) | VARCHAR(255) | 活動唯一識別碼 |
version | VARCHAR(10) | 數據版本 |
title | VARCHAR(255) | 活動名稱 |
category | INT | 活動分類(類別 ID) |
showUnit | VARCHAR(255) | 演出單位 |
discountInfo | TEXT | 折扣資訊 |
descriptionFilterHtml | TEXT | 活動描述 |
imageUrl | VARCHAR(255) | 活動圖片 URL |
masterUnit | TEXT | 主辦單位(可存 JSON 或正規化) |
subUnit | TEXT | 協辦單位 |
supportUnit | TEXT | 支援單位 |
otherUnit | TEXT | 其他單位 |
webSales | VARCHAR(255) | 購票網址 |
sourceWebPromote | VARCHAR(255) | 宣傳網站 |
sourceWebName | VARCHAR(255) | 來源網站 |
startDate | DATE | 活動開始日期 |
endDate | DATE | 活動結束日期 |
hitRate | INT | 點擊率 |
comment | TEXT | 活動評論 |
editModifyDate | VARCHAR(255) | 最後修改日期 |
場次表:show_info
這張表存放每場活動的具體場次資訊,例如演出時間、場地、票價等,並與 events 表建立關聯。
| 欄位名稱 | 資料型別 | 說明 |
|---|---|---|
showID (PK) | INT AUTO_INCREMENT | 場次唯一識別碼(自動遞增) |
UID (FK) | VARCHAR(255) | 關聯 events 表的活動識別碼 |
show_time | DATETIME | 演出時間 |
endTime | DATETIME | 演出結束時間 |
locationID (FK) | VARCHAR(255) | 關聯 locations 表的場地 ID |
onSales | CHAR(1) | 是否開放售票 (Y/N) |
price | VARCHAR(255) | 票價資訊 |
場地表:locations
該表存放場地資訊,避免 show_info 重複存儲相同場地資訊,提高數據一致性。
| 欄位名稱 | 資料型別 | 說明 |
|---|---|---|
locationID (PK) | VARCHAR(255) | 場地唯一識別碼 |
location | VARCHAR(255) | 具體地址 |
locationName | VARCHAR(255) | 場地名稱 |
latitude | DECIMAL(10,6) | 經度 |
longitude | DECIMAL(10,6) | 緯度 |
ERD 圖(實體關聯圖)
為了更清晰地呈現這些表之間的關係,我們設計了一張 ERD(Entity-Relationship Diagram):
events表中的UID作為主鍵(PK),同時是show_info表中的外鍵(FK)。show_info表中的locationID作為外鍵,關聯locations表中的locationID。locations表存放場地資訊,減少show_info表內的冗餘數據。
關聯關係如下:
erDiagram
EVENTS {
VARCHAR UID PK "活動唯一識別碼"
VARCHAR version "數據版本"
VARCHAR title "活動名稱"
INT category "活動分類"
VARCHAR showUnit "演出單位"
TEXT discountInfo "折扣資訊"
TEXT descriptionFilterHtml "活動描述"
VARCHAR imageUrl "活動圖片"
TEXT masterUnit "主辦單位"
TEXT subUnit "協辦單位"
TEXT supportUnit "支援單位"
TEXT otherUnit "其他單位"
VARCHAR webSales "購票網址"
VARCHAR sourceWebPromote "宣傳網站"
VARCHAR sourceWebName "來源網站"
DATE startDate "活動開始日期"
DATE endDate "活動結束日期"
INT hitRate "點擊率"
TEXT comment "活動評論"
VARCHAR editModifyDate "最後修改日期"
}
SHOW_INFO {
INT showID PK "場次唯一識別碼"
VARCHAR UID FK "關聯到 EVENTS"
DATETIME show_time "演出時間"
DATETIME endTime "演出結束時間"
VARCHAR locationID FK "關聯到 LOCATIONS"
CHAR onSales "是否開放售票 (Y/N)"
VARCHAR price "票價資訊"
}
LOCATIONS {
VARCHAR locationID PK "地點唯一識別碼"
VARCHAR location "具體地址"
VARCHAR locationName "場地名稱"
FLOAT latitude "經度"
FLOAT longitude "緯度"
}
%% 明確標示外鍵與主鍵關係
EVENTS ||--|{ SHOW_INFO : "FK → PK"
LOCATIONS ||--|{ SHOW_INFO : "FK → PK"
Django ORM 建模
在 Django 中,我們可以使用 ORM(Object-Relational Mapping)來設計這些資料表。
根據 models.py 文件,我們可以看到這些對應的 Django Model:
建立資料庫模型
Event Model
class Event(models.Model):
UID = models.CharField(max_length=255, primary_key=True) # 活動唯一識別碼
version = models.CharField(max_length=10) # 數據版本
title = models.CharField(max_length=255) # 活動名稱
category = models.CharField(max_length=10) # 活動分類(原本是 IntegerField,JSON 為字串)
showUnit = models.CharField(max_length=255, blank=True, null=True) # 演出單位
webSales = models.URLField(blank=True, null=True) # 購票網址
startDate = models.DateField() # 活動開始日期
endDate = models.DateField() # 活動結束日期
hitRate = models.IntegerField(default=0) # 點擊率
def __str__(self):
return self.title
這個 Event 模型對應 music_events.json 裡的基本資訊。
Location Model
class Location(models.Model):
locationID = models.AutoField(primary_key=True) # 自動遞增 ID
location = models.CharField(max_length=255) # 具體地址
locationName = models.CharField(max_length=255) # 場地名稱
latitude = models.DecimalField(max_digits=10, decimal_places=6, blank=True, null=True) # 經度
longitude = models.DecimalField(max_digits=10, decimal_places=6, blank=True, null=True) # 緯度
def __str__(self):
return self.locationName
這個 Location 模型用來儲存場地資訊。
ShowInfo Model
class ShowInfo(models.Model):
showID = models.AutoField(primary_key=True) # 場次唯一識別碼,自動遞增
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="show_info") # 關聯到 events 表
show_time = models.DateTimeField() # 演出時間
endTime = models.DateTimeField(blank=True, null=True) # 演出結束時間
location = models.ForeignKey(Location, on_delete=models.CASCADE, related_name="show_info") # 關聯到 locations 表
onSales = models.CharField(max_length=1, choices=[("Y", "Yes"), ("N", "No")], default="N") # 是否開放售票
price = models.CharField(max_length=255, blank=True, null=True) # 票價資訊
def __str__(self):
return f"{self.event.title} - {self.show_time}"
這個 ShowInfo 模型用來存儲各場次資訊,並透過 ForeignKey 連結 Event 和 Location。
關聯類型解析
1. Event → ShowInfo(一對多 One-to-Many)
- 關係類型:
- 父物件(Parent Object):
Event - 子物件(Child Object):
ShowInfo
- 父物件(Parent Object):
- 關聯說明:
- 一個
Event(活動) 可以有多個ShowInfo(場次) - 一個
ShowInfo只能屬於一個Event
- 一個
- Django 定義:
class ShowInfo(models.Model):
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="show_info")- SQL 等價關係:
ALTER TABLE show_info ADD CONSTRAINT fk_event FOREIGN KEY (event_id) REFERENCES events (UID);- 實際案例:
- 活動「五月天演唱會」(
Event) 可能有 3 場不同時間的演出(不同的ShowInfo)。 ShowInfo記錄了該場演出的具體時間、票價等資訊。
- 活動「五月天演唱會」(
2. Location → ShowInfo(一對多 One-to-Many)
- 關係類型:
- 父物件(Parent Object):
Location - 子物件(Child Object):
ShowInfo
- 父物件(Parent Object):
- 關聯說明:
- 一個
Location(場地) 可以有多個ShowInfo(場次) - 一個
ShowInfo只能對應一個Location
- 一個
- Django 定義:
class ShowInfo(models.Model): location = models.ForeignKey(Location, on_delete=models.CASCADE, related_name="show_info")- SQL 等價關係:
ALTER TABLE show_info ADD CONSTRAINT fk_location FOREIGN KEY (location_id) REFERENCES locations (locationID);- 實際案例:
- 高雄巨蛋 (
Location) 可能舉辦五月天演唱會 (ShowInfo) 以及其他活動。
- 高雄巨蛋 (
3. ShowInfo → Event & Location(多對一 Many-to-One)
- 關係類型:
ShowInfo透過 外鍵(ForeignKey)連結Event和Location,形成 多對一(Many-to-One) 關係。- 父物件(Parent Objects):
Event&Location - 子物件(Child Object):
ShowInfo
- 關聯說明:
- 一個
ShowInfo只能關聯到一個Event(活動) - 一個
ShowInfo只能關聯到一個Location(地點)
- 一個
- Django 定義:
class ShowInfo(models.Model):
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="show_info")
location = models.ForeignKey(Location, on_delete=models.CASCADE, related_name="show_info")完整模型關聯圖(父子關係標示)
Event (活動) 1 ----- * ShowInfo (場次) * ----- 1 Location (場地)
↑ ↑
父物件 子物件
關聯解釋
Event(父物件) →ShowInfo(子物件):
一個活動可能有多個場次,但每場次只能屬於一個活動。Location(父物件) →ShowInfo(子物件):
一個場地可以有多場演出,但每場演出只能發生在一個場地內。ShowInfo是Event和Location的子物件,因為它需要依賴這兩者的外鍵來存在。
這樣的設計確保了數據的完整性,使 ShowInfo 可以正確對應到活動 (Event) 和場地 (Location)。
提醒:記得執行 makemigrations 和 migrate
在 Django 中,當我們建立或修改 models.py 內的資料表結構後,一定要執行遷移指令 來將模型同步到資料庫,否則這些設計將不會生效。請確保完成以下步驟:
1️⃣ 建立遷移檔案(生成對應的 SQL 指令):
python manage.py makemigrations這會根據 models.py 的變更,自動生成遷移檔案,例如 0001_initial.py。
2️⃣ 執行遷移(真正套用到資料庫):
python manage.py migrate這會將遷移檔案的變更套用到資料庫,建立 events、show_info、locations 等表。
📌 小提醒:
- 若修改了
models.py,請記得重新執行makemigrations和migrate。 - 可以用
python manage.py showmigrations檢查哪些遷移已經套用。
這樣一來,我們的 Django ORM 模型就能夠順利與資料庫對應,確保接下來的 JSON 資料匯入能夠正常運行!
從 JSON 解析並存入資料庫
接下來,我們可以寫一個 Python 腳本來解析 music_events.json,並將資料存入 Django 資料庫。
import json
from myapp.models import Event, ShowInfo, Location
from datetime import datetime
# 讀取 JSON 文件
with open("music_events.json", "r", encoding="utf-8") as file:
data = json.load(file)
# 解析 JSON 並存入資料庫
for event_data in data:
event, created = Event.objects.get_or_create(
UID=event_data["UID"],
defaults={
"version": event_data["version"],
"title": event_data["title"],
"category": event_data["category"],
"webSales": event_data.get("webSales", ""),
"startDate": datetime.strptime(event_data["startDate"], "%Y/%m/%d").date(),
"endDate": datetime.strptime(event_data["endDate"], "%Y/%m/%d").date(),
"hitRate": event_data.get("hitRate", 0),
},
)
for show in event_data["showInfo"]:
location, _ = Location.objects.get_or_create(
location=show["location"],
locationName=show["locationName"],
latitude=show.get("latitude"),
longitude=show.get("longitude"),
)
ShowInfo.objects.create(
event=event,
show_time=datetime.strptime(show["time"], "%Y/%m/%d %H:%M:%S"),
endTime=datetime.strptime(show["endTime"], "%Y/%m/%d %H:%M:%S"),
location=location,
onSales=show["onSales"],
price=show.get("price", ""),
)
這段程式碼:
- 讀取 JSON 檔案
- 解析
events,並建立Event物件 - 解析
showInfo,建立Location和ShowInfo物件 - 使用
get_or_create()確保不會重複插入相同的資料
結論
透過這篇文章,我們了解了:
- 如何分析 JSON 結構並設計資料庫模型
- 使用 Django ORM 來定義
models.py - 解析 JSON 並存入資料庫的方式
這樣的流程適用於各種 JSON 格式的數據轉換,讓你的應用能夠更好地處理與管理外部數據!