Django Custom Command 匯入 JSON 資料指南

更新日期: 2025 年 2 月 24 日

在 Django 專案中,當我們需要批量導入外部資料(如 JSON 檔案)到資料庫時,使用 自訂管理指令(Custom Management Commands) 是一個方便的方法。

這可以讓開發者透過 python manage.py your_command 來執行特定的資料處理流程。

本指南將帶你從頭學習 如何建立 Django 自訂命令來匯入 JSON 資料,並結合 EventLocationShowInfo 三個模型進行處理。


了解 Django 的自訂命令

Django 允許我們透過 BaseCommand 來擴展 manage.py,建立自己的管理指令。這些指令通常存放在 your_app/management/commands/ 目錄下,例如:

your_app/
 ├── management/
    ├── __init__.py
    ├── commands/
       ├── __init__.py
       ├── import_json.py  # 自訂指令

import_json.py 內,我們可以定義一個 handle 方法,來執行自訂的資料處理邏輯。


準備 Model(模型)

在本專案中,我們有三個主要模型:

  1. Event(活動)
    • 存儲音樂活動的詳細資訊,包括 UIDtitlecategory 等欄位。
    • startDateendDate 存為 DateField
  2. Location(場地)
    • 存儲活動場地資訊,包含 location(地址)、locationName(場地名稱)。
    • latitudelongitude 存儲地理座標。
  3. ShowInfo(場次資訊)
    • 關聯到 Event,記錄該活動的 演出時間、場地、票價、是否開放售票等資訊

模型已在 models.py 內定義完成,如下:

class Event(models.Model):
    UID = models.CharField(max_length=255, primary_key=True)
    title = models.CharField(max_length=255)
    category = models.CharField(max_length=10)
    startDate = models.DateField()
    endDate = models.DateField()
    hitRate = models.IntegerField(default=0)
class Location(models.Model):
    location = models.CharField(max_length=255)
    locationName = models.CharField(max_length=255)
class ShowInfo(models.Model):
    event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="show_info")
    show_time = models.DateTimeField()
    location = models.ForeignKey(Location, on_delete=models.CASCADE, related_name="show_info")
    onSales = models.CharField(max_length=1, choices=[("Y", "Yes"), ("N", "No")], default="N")

現在,我們來看看 import_json.py 的完整實作,該指令將:

  • 讀取 JSON 檔案
  • 解析資料並存入資料庫
  • 避免重複插入(使用 update_or_create
  • 緩存 Location 以避免重複查詢

解析日期格式

JSON 檔案可能包含不同格式的日期,因此我們定義一個函數來解析:

from datetime import datetime

def parse_custom_datetime(datetime_str):
    """解析多種格式的日期時間"""
    if not datetime_str:
        return None
    
    date_formats = ["%Y/%m/%d %H:%M:%S", "%Y-%m-%d %H:%M:%S", "%Y/%m/%d", "%Y-%m-%d"]
    
    for fmt in date_formats:
        try:
            return datetime.strptime(datetime_str, fmt)
        except ValueError:
            continue
    
    return None

這段程式碼的目的是 嘗試將一個日期時間字串轉換為 Python 的 datetime 物件,並且能夠 支援多種日期格式

如果解析成功,則返回 datetime 物件;如果所有格式都解析失敗,則返回 None

date_formats:定義可接受的日期格式

date_formats = [
    "%Y/%m/%d %H:%M:%S",  # 格式: 2024/02/24 14:30:00
    "%Y-%m-%d %H:%M:%S",  # 格式: 2024-02-24 14:30:00
    "%Y/%m/%d",           # 格式: 2024/02/24
    "%Y-%m-%d"            # 格式: 2024-02-24
]

這是一個 列表(list),存放了不同可能的日期格式,每個格式代表 如何解析日期字串

  • %Y:表示四位數的年份(如 2024)。
  • %m:表示兩位數的月份(如 02)。
  • %d:表示兩位數的日期(如 24)。
  • %H:%M:%S:表示時間部分(24 小時制的時、分、秒)。
  • /-:表示可能的分隔符號。

這四種格式代表:

格式範例
"%Y/%m/%d %H:%M:%S"2024/02/24 14:30:00
"%Y-%m-%d %H:%M:%S"2024-02-24 14:30:00
"%Y/%m/%d"2024/02/24
"%Y-%m-%d"2024-02-24

迴圈遍歷所有格式

for fmt in date_formats:
    try:
        return datetime.strptime(datetime_str, fmt)  # 嘗試將字串解析為 datetime 物件
    except ValueError:
        continue  # 如果格式不匹配,則繼續嘗試下一種格式

這個 for 迴圈會遍歷 date_formats 裡的每個格式 fmt,並 嘗試將輸入的日期字串 datetime_str 解析為 datetime 物件

  • datetime.strptime(datetime_str, fmt)
    • 功能:使用 fmt 格式嘗試解析 datetime_str
    • 成功時:直接返回 datetime 物件。
    • 失敗時(格式不符合):拋出 ValueError,然後 except 處理它,繼續嘗試下一種格式。

示例

假設 datetime_str = "2024/02/24 14:30:00",則:

  1. 第一個格式 "%Y/%m/%d %H:%M:%S" 匹配成功,返回: datetime(2024, 2, 24, 14, 30, 0)
  2. 迴圈結束,程式不再嘗試其他格式(因為 return 會直接結束函數)。

但如果 datetime_str = "2024-02-24",則:

  1. 第一個格式 "%Y/%m/%d %H:%M:%S" 不匹配,拋出 ValueError,跳過。
  2. 第二個格式 "%Y-%m-%d %H:%M:%S" 不匹配,拋出 ValueError,跳過。
  3. 第三個格式 "%Y/%m/%d" 不匹配,拋出 ValueError,跳過。
  4. 第四個格式 "%Y-%m-%d" 匹配成功,返回: datetime(2024, 2, 24)

若所有格式都解析失敗,則返回 None

return None  # 如果所有格式都解析失敗,則返回 None

如果 datetime_str 不符合 date_formats 中的任何格式,則:

  1. 所有 try 都失敗,ValueError 使得迴圈繼續。
  2. 直到迴圈跑完,還是沒找到匹配的格式,函數最後返回 None

讀取 JSON 並存入資料庫

import json
import os
from django.core.management.base import BaseCommand
from django.conf import settings
from music_events.models import Event, Location, ShowInfo

class Command(BaseCommand):
    help = "Import music events from JSON file"

    def handle(self, *args, **kwargs):
        # 設定 JSON 檔案路徑
        file_path = os.path.join(settings.BASE_DIR, "music_events", "data", "music_events.json")

        with open(file_path, encoding="utf-8") as f:
            data = json.load(f)

        event_created, event_updated = 0, 0
        showinfo_created, showinfo_updated = 0, 0
        location_cache = {}

        for item in data:
            self.stdout.write(f"Processing Event UID: {item['UID']}")

            # 檢查活動是否已存在
            event, created = Event.objects.update_or_create(
                UID=item["UID"],
                defaults={
                    "title": item["title"],
                    "category": item["category"],
                    "startDate": parse_custom_datetime(item["startDate"]),
                    "endDate": parse_custom_datetime(item["endDate"]),
                    "hitRate": item["hitRate"],
                }
            )
            if created:
                event_created += 1
            else:
                event_updated += 1

            # 處理場地資訊
            for show in item["showInfo"]:
                location_key = (show["location"], show["locationName"])

                if location_key in location_cache:
                    location = location_cache[location_key]
                else:
                    location, _ = Location.objects.get_or_create(
                        location=show["location"],
                        locationName=show["locationName"]
                    )
                    location_cache[location_key] = location

                # 新增 ShowInfo
                _, show_created = ShowInfo.objects.update_or_create(
                    event=event,
                    show_time=parse_custom_datetime(show["time"]),
                    location=location,
                    defaults={
                        "onSales": show["onSales"],
                        "price": show["price"] or None,
                    }
                )
                if show_created:
                    showinfo_created += 1
                else:
                    showinfo_updated += 1

        self.stdout.write(self.style.SUCCESS(
            f"匯入完成!\n"
            f"Event 新增:{event_created},更新:{event_updated}\n"
            f"ShowInfo 新增:{showinfo_created},更新:{showinfo_updated}"
        ))

引入必要的模組

import json
import os
from django.core.management.base import BaseCommand
from django.conf import settings
from music_events.models import Event, Location, ShowInfo
  • json:用來讀取 JSON 檔案。
  • os:用於處理檔案路徑。
  • BaseCommand:Django 自訂管理指令的基礎類別。
  • settings:Django 設定檔,用來取得專案的 BASE_DIR
  • Event, Location, ShowInfo:導入我們的資料庫模型。

定義 Command 類別

class Command(BaseCommand):
    help = "Import music events from JSON file"
  • Command 類別:繼承 BaseCommand,用來建立 manage.py 可執行的自訂命令。
  • help 屬性:當執行 python manage.py help import_json 時,會顯示 "Import music events from JSON file" 這個說明。

設定 JSON 檔案路徑

file_path = os.path.join(settings.BASE_DIR, "music_events", "data", "music_events.json")

os.path.join():這個函式用於組合多個路徑部分,並確保在不同的作業系統(Windows、Linux、Mac)上都能正確運作。

  • Windows 使用 \(例如 C:\myproject\file.json)。
  • Linux 和 macOS 使用 /(例如 /home/user/myproject/file.json)。
  • os.path.join() 會自動選擇適合的 路徑分隔符號,確保程式能夠正確尋找檔案。

🔹 settings.BASE_DIR:獲取 Django 專案根目錄

settings.BASE_DIR
  • settings.BASE_DIR 是 Django 專案的根目錄,通常是 manage.py 所在的資料夾。
  • 假設 Django 專案結構如下:
my_django_project/
├── manage.py
├── my_django_project/
   ├── settings.py
   ├── urls.py
├── music_events/
   ├── management/
      ├── commands/
         ├── import_json.py
   ├── data/
      ├── music_events.json
  • 那麼 settings.BASE_DIR 的值就是 my_django_project/(即 manage.py 所在的資料夾)。

延伸閱讀:深入理解 Django 的 BASE_DIR

🔹 music_events/data/music_events.json:指定 JSON 檔案的相對路徑

"music_events", "data", "music_events.json"
  • "music_events" → Django 應用程式名稱。
  • "data" → 存放 JSON 檔案的資料夾名稱。
  • "music_events.json" → 目標 JSON 檔案名稱。

在這裡,這些部分被 os.path.join() 合併,生成 完整的檔案路徑

🔹 假設 Django 專案在不同作業系統上的結果

1️⃣ 在 Linux/macOS

如果 settings.BASE_DIR 是:

/home/user/my_django_project

file_path 會變成:

/home/user/my_django_project/music_events/data/music_events.json

這是一個 有效的 UNIX 路徑

2️⃣ 在 Windows

如果 settings.BASE_DIR 是:

C:\Users\user\my_django_project

file_path 會變成:

C:\Users\user\my_django_project\music_events\data\music_events.json

這是一個 有效的 Windows 路徑

🔹 file_path 在程式中的用途

這個變數 file_path 之後被用來開啟 JSON 檔案:

with open(file_path, encoding="utf-8") as f:
    data = json.load(f)

這行程式碼會:

  1. 開啟 music_events.json 檔案(確保它在 music_events/data/ 目錄下)。
  2. json.load(f) 解析 JSON,將其轉換為 Python listdict,方便後續資料處理。

🔹 為什麼不直接寫 "music_events/data/music_events.json"

如果你這樣寫:

file_path = "music_events/data/music_events.json"

這是 一個相對路徑,但:

  • 如果你從 Django 不同的工作目錄 執行指令(例如 manage.py 在不同資料夾下),可能會找不到這個檔案。
  • 在 Windows 和 Linux 之間,路徑格式不同,可能會發生錯誤。

使用 os.path.join(settings.BASE_DIR, ...) 的好處:

跨平台兼容性(Windows、Linux、macOS)。
確保路徑正確,不受執行目錄影響
易於維護和修改(如果 JSON 檔案位置變了,只需修改 file_path 這行程式碼)。


讀取 JSON 檔案

with open(file_path, encoding="utf-8") as f:
    data = json.load(f)
  • open(file_path, encoding="utf-8"):以 UTF-8 讀取 JSON 檔案,確保支援中文等特殊字符。
  • json.load(f):將 JSON 內容解析為 Python list,其中每個 item 是一個活動(Event)。

延伸閱讀:Python open() 函數完整指南

記錄統計數據

event_created, event_updated = 0, 0
showinfo_created, showinfo_updated = 0, 0
location_cache = {}
  • event_created, event_updated:記錄 Event(活動) 的新增 & 更新數量。
  • showinfo_created, showinfo_updated:記錄 ShowInfo(場次) 的新增 & 更新數量。
  • location_cache:用來 快取 Location,避免重複查詢相同的場地,提高效能。

這行程式碼是 Python 的「多重賦值(Multiple Assignment)」特性,也可以叫做 「元組解包(Tuple Unpacking)」

它的作用是 同時給多個變數賦值,但這裡 並沒有使用明確的元組 (),因為 Python 允許省略括號

🔹 多重賦值(Multiple Assignment)

在 Python 中,我們可以 同時給多個變數賦值,例如:

a, b = 10, 20
print(a)  # 10
print(b)  # 20

這相當於:

a = 10
b = 20

但語法更簡潔。

🔹 Python 自動識別元組

在 Python 中,如果用 , 分隔多個值,Python 會自動將它們視為元組

x = 1, 2, 3  # 這其實是一個元組
print(x)  # (1, 2, 3)

這與:

x = (1, 2, 3)

是等價的。因此:

event_created, event_updated = 0, 0

等價於:

(event_created, event_updated) = (0, 0)

但 Python 允許省略 (),寫法更簡潔。

逐筆處理 JSON 資料

for item in data:
    self.stdout.write(f"Processing Event UID: {item['UID']}")
  • 迴圈遍歷 JSON 資料,逐筆處理每個活動。
  • self.stdout.write():讓指令執行時在終端顯示當前處理的活動 UID

匯入 Event 資料

event, created = Event.objects.update_or_create(
    UID=item["UID"],
    defaults={
        "title": item["title"],
        "category": item["category"],
        "startDate": parse_custom_datetime(item["startDate"]),
        "endDate": parse_custom_datetime(item["endDate"]),
        "hitRate": item["hitRate"],
    }
)

import_events.py 中,我們使用 update_or_create() 方法來將 Event 資料從 JSON 匯入至 Django 資料庫。這個方法確保:

  • 若資料已存在,則更新現有的 Event 記錄。
  • 若資料不存在,則新建一筆 Event 記錄。

🔹 匯入 Event 資料

event, created = Event.objects.update_or_create(
    UID=item["UID"],  # 以 UID 作為查找依據
    defaults={
        "title": item["title"],
        "category": item["category"],
        "startDate": parse_custom_datetime(item["startDate"]),
        "endDate": parse_custom_datetime(item["endDate"]),
        "hitRate": item["hitRate"],
    }
)

📌 update_or_create() 的運作方式

  • 第一個參數 (UID=item["UID"])
    • 這是查找條件,Django 會檢查資料庫中是否已存在相同 UIDEvent 記錄。
  • defaults={}
    • UID 存在時,Django 會更新 defaults 內指定的欄位。
    • UID 不存在時,Django 會根據 defaults 的值來新建一筆 Event

📌 這樣的設計能避免重複插入相同的活動數據,確保資料一致性!

延伸閱讀:Django update_or_create() 用法詳解

🔹 解析 startDateendDate

"startDate": parse_custom_datetime(item["startDate"]),
"endDate": parse_custom_datetime(item["endDate"]),
  • 問題:JSON 檔內的 startDateendDate字串格式,例如 "2025/02/19"
  • 解決方案:我們使用 parse_custom_datetime() 方法來解析字串並轉換為 Python datetime 物件,以確保 Django ORM 能正確存入 DateField

記錄 Event 的新增或更新

if created:
    event_created += 1
else:
    event_updated += 1
  • created == True
    • 代表資料是新建的(資料庫內不存在相同 UID)。
    • 計數器 event_created 加 1,統計新增的 Event 數量。
  • created == False
    • 代表資料是更新的(資料庫內已存在相同 UID,僅更新部分欄位)。
    • 計數器 event_updated 加 1,統計被更新的 Event 數量。

📌 這樣的計數方式有助於記錄 JSON 匯入的狀態,方便開發者在終端機上檢查是否有資料被正確處理!

處理 Location(場地)

for show in item["showInfo"]:
    location_key = (show["location"], show["locationName"])

    if location_key in location_cache:
        location = location_cache[location_key]
    else:
        location, _ = Location.objects.get_or_create(
            location=show["location"],
            locationName=show["locationName"]
        )
        location_cache[location_key] = location

理解 for item in data:for show in item["showInfo"]: 的數據結構

1️⃣ for item in data: – 逐一取出活動資訊

data 是包含 多個活動的列表(list),每個 item 代表一場活動的完整資訊。

📌 假設 data 如下

[
    {
        "UID": "event_001",
        "title": "2025 音樂節",
        "category": "1",
        "startDate": "2025/02/19",
        "endDate": "2025/02/19",
        "hitRate": 100,
        "showInfo": [
            {
                "time": "2025/02/19 19:30:00",
                "location": "高雄市鳳山區三多一路1號",
                "locationName": "衛武營國家藝術文化中心表演廳",
                "onSales": "Y",
                "price": "800",
                "latitude": "22.623017",
                "longitude": "120.342434"
            }
        ]
    },
    {
        "UID": "event_002",
        "title": "台北爵士夜",
        "category": "2",
        "startDate": "2025/03/10",
        "endDate": "2025/03/10",
        "hitRate": 50,
        "showInfo": [
            {
                "time": "2025/03/10 20:00:00",
                "location": "台北市信義區松壽路12號",
                "locationName": "台北流行音樂中心",
                "onSales": "N",
                "price": "免費",
                "latitude": "25.033964",
                "longitude": "121.564468"
            }
        ]
    }
]

📌 for item in data: 迴圈運作方式

for item in data:
    print(item)  # 依序輸出每個活動

第一輪(item = data[0]

{
    "UID": "event_001",
    "title": "2025 音樂節",
    "category": "1",
    "startDate": "2025/02/19",
    "endDate": "2025/02/19",
    "hitRate": 100,
    "showInfo": [...]
}

第二輪(item = data[1]

{
    "UID": "event_002",
    "title": "台北爵士夜",
    "category": "2",
    "startDate": "2025/03/10",
    "endDate": "2025/03/10",
    "hitRate": 50,
    "showInfo": [...]
}

📌 結論for item in data: 讓我們可以逐一取出 data 內的每個活動資訊,item 代表一場活動的完整 JSON 物件。

2️⃣ for show in item["showInfo"]: – 解析場次資訊

每場活動可能包含 多個場次,這些資訊存放於 showInfo 陣列內。for show in item["showInfo"]: 讓我們可以 依序取出各場次的詳細資訊

📌 for show in item["showInfo"]: 迴圈運作方式

for show in item["showInfo"]:
    print(show)  # 依序輸出每場演出的資訊

🔹 itemevent_001(2025 音樂節)時

{
    "time": "2025/02/19 19:30:00",
    "location": "高雄市鳳山區三多一路1號",
    "locationName": "衛武營國家藝術文化中心表演廳",
    "onSales": "Y",
    "price": "800",
    "latitude": "22.623017",
    "longitude": "120.342434"
}

🔹 itemevent_002(台北爵士夜)時

{
    "time": "2025/03/10 20:00:00",
    "location": "台北市信義區松壽路12號",
    "locationName": "台北流行音樂中心",
    "onSales": "N",
    "price": "免費",
    "latitude": "25.033964",
    "longitude": "121.564468"
}

📌 結論for show in item["showInfo"]: 讓我們逐一取出 每場活動的場次資訊show 代表單場演出的詳細資訊。

3️⃣ 總結兩個迴圈的數據內容
迴圈數據內容範例輸出
for item in data:取出一場活動的完整資訊{ "UID": "event_001", "title": "2025 音樂節", "showInfo": [...] }
for show in item["showInfo"]:取出該活動的場次資訊{ "time": "2025/02/19 19:30:00", "location": "高雄市鳳山區三多一路1號", "locationName": "衛武營國家藝術文化中心表演廳" }

處理 Location(場地)資訊

當我們解析 showInfo 時,需要考慮場地資訊的 去重(避免重複存入資料庫)。為此,我們使用 location_key 來唯一識別場地。

建立 location_key

location_key = (show["location"], show["locationName"])  # 地址 + 場地名稱作為唯一識別

📌 作用

  • 確保同一場地不會重複存入,例如:
    • ("高雄市鳳山區三多一路1號", "衛武營國家藝術文化中心表演廳")
    • ("台北市信義區松壽路12號", "台北流行音樂中心")

檢查 location_cache 是否已有該場地

if location_key in location_cache:
    location = location_cache[location_key]  # 若已存在,直接使用

📌 作用

  • 減少資料庫查詢次數,提高效能。

若場地不存在,則新增

else:
    result = Location.objects.get_or_create(
        location=show["location"],
        locationName=show["locationName"],
        defaults={
            "latitude": show["latitude"] or None,
            "longitude": show["longitude"] or None,
        }
    )
    location, created = result  # 以 tuple 拆解回傳值
    location_cache[location_key] = location  # 存入快取,避免重複查詢

📌 作用

  • get_or_create() 會回傳一個 tuple (object, created)
    • object:查詢或新建的 Location 物件。
    • created如果物件是新建的,則回傳 True;如果已存在,則回傳 False
get_or_create() 可能的回傳結果

假設 show["location"] = "高雄市鳳山區三多一路1號"show["locationName"] = "衛武營國家藝術文化中心表演廳",則:

result = Location.objects.get_or_create(
    location="高雄市鳳山區三多一路1號",
    locationName="衛武營國家藝術文化中心表演廳",
    defaults={"latitude": "22.623017", "longitude": "120.342434"}
)
print(result)  # 這裡會輸出 tuple

📌 可能的回傳結果

1️⃣ 若場地已存在

(<Location object (ID=1)>, False)  # `False` 表示這個場地已經存在,未新增

2️⃣ 若場地不存在(需新增)

(<Location object (ID=10)>, True)  # `True` 表示新建了一個場地,ID 可能為 10

新增 ShowInfo(場次)

_, show_created = ShowInfo.objects.update_or_create(
    event=event,
    show_time=parse_custom_datetime(show["time"]),
    location=location,
    defaults={
        "onSales": show["onSales"],
        "price": show["price"] or None,
    }
)

🔹 ForeignKey 在 Django ORM 中的運作

在 Django ORM 中,ShowInfo 模型的 event 欄位是一個 外鍵(ForeignKey),這表示它與 Event 模型建立了關聯,而不是一個單純的文字或數字欄位。

1️⃣ 外鍵 (ForeignKey) 的運作方式

ShowInfo 中的 event 欄位設置為 ForeignKey(Event, on_delete=models.CASCADE),表示:

  • 這個欄位的值不會存 Event 物件本身,而是存 Event 的主鍵(Primary Key)。
  • Django 會負責管理這個關聯,讓你可以直接透過 ShowInfo.event 存取對應的 Event 物件,而不只是主鍵值。

舉例來說:

show_info = ShowInfo.objects.get(id=1)  # 取得一筆 ShowInfo 資料
print(show_info.event)  # 輸出: <Event: 五月天演唱會>
print(show_info.event.id)  # 輸出: 123
print(show_info.event.title)  # 輸出: 五月天演唱會

這表示:

  • show_info.event 是一個 Event 物件,不是單純的一個 ID。
  • show_info.event.id 則是該 Event 物件的主鍵值。
2️⃣ Django 允許使用 Python 物件篩選,並自動轉換為主鍵

Django ORM 允許你在查詢時 直接使用 Event 物件作為篩選條件,而不需要手動取得 event.id

例如:

event = Event.objects.get(UID="E123")  # 取得某個活動的 Event 物件
ShowInfo.objects.filter(event=event)  # 直接用物件來查找場次

在這段程式碼中,event 是一個 Event 物件,而 ShowInfo.objects.filter(event=event) 其實會被 Django 轉換成:

SELECT * FROM showinfo WHERE event_id = 123;

其中 event_idEvent 表的主鍵值。

這意味著:

  • Django 會自動從 Event 物件中取得它的主鍵(id)來進行篩選。
  • 不需要手動寫 event_id=event.id,Django 會幫你處理轉換。

這種設計讓 Django ORM 的語法更加直觀且易讀,開發者可以直接使用 Python 物件來操作資料,而不需要手動管理主鍵值的轉換。


🔹 update_or_create() 背後的運作

1️⃣ 取得 event 物件
event, created = Event.objects.update_or_create(
    UID=item["UID"],
    defaults={"title": item["title"], "category": item["category"]}
)

這段程式的作用:

  • 查找 是否已有 UID=item["UID"]Event 物件。
  • 若存在,回傳該 Event 物件 (event),並更新欄位值。
  • 若不存在,創建新的 Event 物件並回傳。
2️⃣ event=event 用於 ShowInfo
_, show_created = ShowInfo.objects.update_or_create(
    event=event,  # event 是上面找到或新建的 Event 物件
    show_time=parse_custom_datetime(show["time"]),
    location=location,
    defaults={"onSales": show["onSales"], "price": show["price"] or None}
)

Django 會:

  1. 查找 ShowInfo 是否已有 相同 event(活動)、show_time(場次時間)、location(地點) 的記錄。
  2. event 是一個 Event 物件,Django 會自動轉換成 event.id 來進行查找
  3. 內部 SQL 會變成:
SELECT * FROM showinfo WHERE event_id = event.id;
  1. 若找到相符的場次,就更新 defaults 裡的欄位
  2. 若找不到,就創建新場次,並將 event_id 設為 event.id

🔹 ForeignKey 為什麼可以用物件來篩選?

Django 允許 ForeignKey 欄位直接使用物件來查找,因為:

  • 資料庫的 ForeignKey 存的其實是 event.id(主鍵值)。
  • Django 會自動將 Event 物件轉換為 event.id 來做查詢。

舉例來說:

ShowInfo.objects.filter(event=event)

Django 會自動轉成:

SELECT * FROM showinfo WHERE event_id = event.id;

這樣我們不用手動寫 event_id=event.id,Django 會自動處理。

🔹 另一種寫法

如果不想寫 event=event,可以明確指定 event.id

ShowInfo.objects.update_or_create(
    event_id=event.id,  # 明確指定外鍵的主鍵值
    show_time="2024-03-10 19:00:00",
    location="台北小巨蛋",
    defaults={"onSales": "Y", "price": "1500"}
)

這樣也會產生相同的 SQL:

SELECT * FROM showinfo WHERE event_id = 'E123' AND show_time = '2024-03-10 19:00:00';

但 Django 允許 event=event 這種 直觀且簡潔的寫法,所以通常不需要手動寫 event_id=event.id

記錄 ShowInfo 的新增或更新

if show_created:
    showinfo_created += 1
else:
    showinfo_updated += 1
  • show_created = True:新場次,showinfo_created +1
  • show_created = False:更新場次,showinfo_updated +1

完成後顯示匯入結果

self.stdout.write(self.style.SUCCESS(
    f"匯入完成!\n"
    f"Event 新增:{event_created},更新:{event_updated}\n"
    f"ShowInfo 新增:{showinfo_created},更新:{showinfo_updated}"
))
  • 顯示統計結果
    • Event 新增 & 更新 的數量。
    • ShowInfo 新增 & 更新 的數量。
  • self.style.SUCCESS()
    • 終端輸出 綠色字體,提示匯入成功。

執行指令

確認 import_json.py 已存放於:

your_app/
 ├── management/
    ├── commands/
       ├── import_json.py

然後,開啟終端機,在 Django 專案根目錄下執行:

python manage.py import_json

如果執行成功,你將看到輸出:

Processing Event UID: 12345
Processing Event UID: 67890
匯入完成!
Event 新增:10,更新:5
ShowInfo 新增:20,更新:10

總結

本指南詳細介紹了:

  1. 如何建立 Django 自訂命令
  2. 如何讀取 JSON 並解析資料
  3. 如何存入資料庫,避免重複插入
  4. 如何執行自訂指令

這樣的 Django 指令對於 批量匯入資料、自動化資料處理 都非常有幫助。現在,你可以根據需求,擴展這個指令,讓你的 Django 專案更強大! 🚀

Similar Posts