建立Relational Database的時候常常會用到One-To-Many Relation,一般都是以Foreign Key的形式儲存。
Django功能十分齊全,但它的model inheritance設計令所有功能都非常"implicit",一些功能如果不知道要到哪裡define的話根本無從入手。
所以這篇就來紀錄一下如何用Django的ORM處理One-To-Many的Model吧
(如果你不知道甚麼是ORM:What is ORM)
I. 建立One-To-Many Model
假設有以下model:
from django.db import models
class Restaurant(models.Model):
name = CharField(max_length=200)
class Food(models.Model):
restaurant = ForeignKey(Restaurant, on_delete=models.CASCADE)
name = CharField(max_length=200)
(不知道如何define model的可先看Django Tutorial)
每個Food
都有個Restaurant
的ForeignKey
,代表它屬於某個Restaurant
,每個Restaurant
可以有多於一個Food
。
II. 取得一個Parent的所有Child
方法一:Query
如果想取得Restaurant的所有Food,以官方教學中有提及的方法:
restaurant = Restaurant.objects.get(id=1) # 取得一間restaurant
Food.objects.filter(restaurant__id=restaurant.id)
# <QuerySet [<Food: 叉燒飯>, <Food: 燒鴨飯>]>
其實就是用restaurant的資料query一次food。
方法二:Child Set
然後我發現原來官方在某個角落寫著,可以用restaurant.food_set
來達到相同效果:
restaurant = Restaurant.objects.get(id=1) # 取得一間restaurant
restaurant.food_set.all()
# <QuerySet [<Food: 叉燒飯>, <Food: 燒鴨飯>]>
food_set
這名稱來自Food
這個class,Django會把它變lowercase然後加上_set
,神奇吧。
這就是為甚麼我不喜歡Django,明明一直都存在於model之中,但如果我沒找到這一小段文檔,可能一世也不會知道它的存在
關於Manager
Restaurant.objects
是一個Manager
,用來管理對restaurant的query,詳情可參照Manager Reference。- 同時
restaurant.food_set
也是一個Manager
,但它管理對「屬於這間restaurant的food」的query
自訂field name
如果想用restaurant.foods
instead of restaurant.food_set
,可以在ForeignKey field裡加入related_name
:
class Food(models.Model):
restaurant = ForeignKey(Restaurant, on_delete=models.CASCADE, related_name='foods') # 這裡
name = CharField(max_length=200)
# ...
restaurant.foods.all()
III. 取得所有Parent連帶它們的Child
(等同SQL裡的Join table)
目標
[
{
"name": "restaurant_A",
"foods": [
{
"name": "叉燒飯"
}, {
"name": "燒鴨飯"
},
...
]
},
...
]
這裡示範如何取得所有Restaurant連帶它們的Food:
方法一:For-loop
先取得Restaurants,再找出屬於每個Restaurant的Food:
restaurants = Restaurant.objects.all().values() # values()把QuerySet裡的所有Restaurant objects變成dict
for restaurant in restaurants:
food_queryset = Food.objects.filter(restaurant__id=restaurant['id']).values()
restaurant['food'] = list(food_queryset) # list()把QuerySet變成list
restaurants = list(restaurants) # list()把QuerySet變成list
方法二:REST Framework Serializer
Django REST Framework是Django一個非常熱門的工具,它可以幫你處理這些常見process,讓你不用自己寫for-loop。
(只是我覺得又再abstract了一層後,已經不知道自己的code怎麼運作了。)
class FoodSerializer(serializers.ModelSerializer):
class Meta:
model = Food
fields = '__all__'
class RestaurantSerializer(serializers.ModelSerializer):
foods = FoodSerializer(many=True, source='food_set') # 加入這行後,它會自動幫你找到所有related childs
class Meta:
model = Restaurant
fields = '__all__'
queryset = Restaurant.objects.all()
serializer = RestaurantSerializer(queryset, many=True)
serializer.data # list of restaurant_dicts with their food_dicts
Performance Issue (N+1 Problem)
在處理Database relations時,常常會遇上performance的瓶頸,而問題九成都是來自著名的N+1 Problem。
N+1意思就是touch了database N+1次,例如以上兩個做法,首先是找出所有N間restaurants(touch 1次),然後逐間找一次相關的Food(touch N次),加起來就是N+1了。
在現今電腦架構中,動用實體硬碟(尤其是HDD)是非常昂貴(expensive)的,無論從速度還是機器折舊角度,都應盡量減少這種operation。
解決方法
Django的Manager
採用Lazy loading,所以未動用到的data是不會主動拿出來的,就像是這裡每間restaurant的foods。
我們可以使用prefetch_related()
來逼django在找restaurants的時候也找出所有相關的foods,這樣動用實體硬碟的次數就只需一次了。
例如以上serializer的做法改良版:
queryset = Restaurant.objects.prefetch_related('food_set').all() # 使用queryset前先prefetch所有相關food
serializer = RestaurantSerializer(queryset, many=True)