Django Custom Command 匯入 JSON 資料指南
更新日期: 2025 年 2 月 24 日
本文為 資料庫正規化 基本介紹系列文,第 4 篇:
在 Django 專案中,當我們需要批量導入外部資料(如 JSON 檔案)到資料庫時,使用 自訂管理指令(Custom Management Commands) 是一個方便的方法。
這可以讓開發者透過 python manage.py your_command 來執行特定的資料處理流程。
本指南將帶你從頭學習 如何建立 Django 自訂命令來匯入 JSON 資料,並結合 Event、Location 和 ShowInfo 三個模型進行處理。
了解 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(模型)
在本專案中,我們有三個主要模型:
- Event(活動)
- 存儲音樂活動的詳細資訊,包括
UID、title、category等欄位。 startDate和endDate存為DateField。
- 存儲音樂活動的詳細資訊,包括
- Location(場地)
- 存儲活動場地資訊,包含
location(地址)、locationName(場地名稱)。 latitude和longitude存儲地理座標。
- 存儲活動場地資訊,包含
- 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",則:
- 第一個格式
"%Y/%m/%d %H:%M:%S"匹配成功,返回:datetime(2024, 2, 24, 14, 30, 0) - 迴圈結束,程式不再嘗試其他格式(因為
return會直接結束函數)。
但如果 datetime_str = "2024-02-24",則:
- 第一個格式
"%Y/%m/%d %H:%M:%S"不匹配,拋出ValueError,跳過。 - 第二個格式
"%Y-%m-%d %H:%M:%S"不匹配,拋出ValueError,跳過。 - 第三個格式
"%Y/%m/%d"不匹配,拋出ValueError,跳過。 - 第四個格式
"%Y-%m-%d"匹配成功,返回:datetime(2024, 2, 24)
若所有格式都解析失敗,則返回 None
return None # 如果所有格式都解析失敗,則返回 None如果 datetime_str 不符合 date_formats 中的任何格式,則:
- 所有
try都失敗,ValueError使得迴圈繼續。 - 直到迴圈跑完,還是沒找到匹配的格式,函數最後返回
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_DIRsettings.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所在的資料夾)。
🔹 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)這行程式碼會:
- 開啟
music_events.json檔案(確保它在music_events/data/目錄下)。 - 用
json.load(f)解析 JSON,將其轉換為 Pythonlist或dict,方便後續資料處理。
🔹 為什麼不直接寫 "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 內容解析為 Pythonlist,其中每個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 會檢查資料庫中是否已存在相同
UID的Event記錄。
- 這是查找條件,Django 會檢查資料庫中是否已存在相同
defaults={}:- 當
UID存在時,Django 會更新defaults內指定的欄位。 - 當
UID不存在時,Django 會根據defaults的值來新建一筆Event。
- 當
📌 這樣的設計能避免重複插入相同的活動數據,確保資料一致性!
延伸閱讀:Django update_or_create() 用法詳解
🔹 解析 startDate 和 endDate
"startDate": parse_custom_datetime(item["startDate"]),
"endDate": parse_custom_datetime(item["endDate"]),- 問題:JSON 檔內的
startDate和endDate是字串格式,例如"2025/02/19"。 - 解決方案:我們使用
parse_custom_datetime()方法來解析字串並轉換為 Pythondatetime物件,以確保 Django ORM 能正確存入DateField。
記錄 Event 的新增或更新
if created:
event_created += 1
else:
event_updated += 1created == 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) # 依序輸出每場演出的資訊🔹 當 item 為 event_001(2025 音樂節)時
{
"time": "2025/02/19 19:30:00",
"location": "高雄市鳳山區三多一路1號",
"locationName": "衛武營國家藝術文化中心表演廳",
"onSales": "Y",
"price": "800",
"latitude": "22.623017",
"longitude": "120.342434"
}
🔹 當 item 為 event_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_id 是 Event 表的主鍵值。
這意味著:
- 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 會:
- 查找
ShowInfo是否已有 相同event(活動)、show_time(場次時間)、location(地點) 的記錄。 event是一個Event物件,Django 會自動轉換成event.id來進行查找。- 內部 SQL 會變成:
SELECT * FROM showinfo WHERE event_id = event.id;- 若找到相符的場次,就更新
defaults裡的欄位。 - 若找不到,就創建新場次,並將
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
總結
本指南詳細介紹了:
- 如何建立 Django 自訂命令
- 如何讀取 JSON 並解析資料
- 如何存入資料庫,避免重複插入
- 如何執行自訂指令
這樣的 Django 指令對於 批量匯入資料、自動化資料處理 都非常有幫助。現在,你可以根據需求,擴展這個指令,讓你的 Django 專案更強大! 🚀
