從 JSON 到資料庫:使用 Django ORM 建構音樂活動管理系統

Published February 24, 2025 by 徐培鈞
Python

在現代應用開發中,數據通常以 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

該表存放音樂活動的基本資訊,例如活動名稱、分類、主辦單位、活動期間等。

資料型別VARCHAR(255)
說明活動唯一識別碼
資料型別VARCHAR(10)
說明數據版本
資料型別VARCHAR(255)
說明活動名稱
資料型別INT
說明活動分類(類別 ID)
資料型別VARCHAR(255)
說明演出單位
資料型別TEXT
說明折扣資訊
資料型別TEXT
說明活動描述
資料型別VARCHAR(255)
說明活動圖片 URL
資料型別TEXT
說明主辦單位(可存 JSON 或正規化)
資料型別TEXT
說明協辦單位
資料型別TEXT
說明支援單位
資料型別TEXT
說明其他單位
資料型別VARCHAR(255)
說明購票網址
資料型別VARCHAR(255)
說明宣傳網站
資料型別VARCHAR(255)
說明來源網站
資料型別DATE
說明活動開始日期
資料型別DATE
說明活動結束日期
資料型別INT
說明點擊率
資料型別TEXT
說明活動評論
資料型別VARCHAR(255)
說明最後修改日期

場次表:show_info

這張表存放每場活動的具體場次資訊,例如演出時間、場地、票價等,並與 events 表建立關聯。

資料型別INT AUTO_INCREMENT
說明場次唯一識別碼(自動遞增)
資料型別VARCHAR(255)
說明關聯 events 表的活動識別碼
資料型別DATETIME
說明演出時間
資料型別DATETIME
說明演出結束時間
資料型別VARCHAR(255)
說明關聯 locations 表的場地 ID
資料型別CHAR(1)
說明是否開放售票 (Y/N)
資料型別VARCHAR(255)
說明票價資訊

場地表:locations

該表存放場地資訊,避免 show_info 重複存儲相同場地資訊,提高數據一致性。

資料型別VARCHAR(255)
說明場地唯一識別碼
資料型別VARCHAR(255)
說明具體地址
資料型別VARCHAR(255)
說明場地名稱
資料型別DECIMAL(10,6)
說明經度
資料型別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 連結 EventLocation

關聯類型解析

1. Event → ShowInfo(一對多 One-to-Many)

  • 關係類型
    • 父物件(Parent Object):Event
    • 子物件(Child Object):ShowInfo
  • 關聯說明
    • 一個 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
  • 關聯說明
    • 一個 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)連結 EventLocation,形成 多對一(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(子物件):
    一個場地可以有多場演出,但每場演出只能發生在一個場地內。
  • ShowInfoEventLocation 的子物件,因為它需要依賴這兩者的外鍵來存在。

這樣的設計確保了數據的完整性,使 ShowInfo 可以正確對應到活動 (Event) 和場地 (Location)。

提醒:記得執行 makemigrationsmigrate

在 Django 中,當我們建立或修改 models.py 內的資料表結構後,一定要執行遷移指令 來將模型同步到資料庫,否則這些設計將不會生效。請確保完成以下步驟:

1️⃣ 建立遷移檔案(生成對應的 SQL 指令):

python manage.py makemigrations

這會根據 models.py 的變更,自動生成遷移檔案,例如 0001_initial.py

2️⃣ 執行遷移(真正套用到資料庫):

python manage.py migrate

這會將遷移檔案的變更套用到資料庫,建立 eventsshow_infolocations 等表。

📌 小提醒

  • 若修改了 models.py,請記得重新執行 makemigrationsmigrate
  • 可以用 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", ""),
        )

這段程式碼:

  1. 讀取 JSON 檔案
  2. 解析 events,並建立 Event 物件
  3. 解析 showInfo,建立 LocationShowInfo 物件
  4. 使用 get_or_create() 確保不會重複插入相同的資料

結論

透過這篇文章,我們了解了:

  1. 如何分析 JSON 結構並設計資料庫模型
  2. 使用 Django ORM 來定義 models.py
  3. 解析 JSON 並存入資料庫的方式

這樣的流程適用於各種 JSON 格式的數據轉換,讓你的應用能夠更好地處理與管理外部數據!