Django 多對多關聯範例
分類
說明
本篇將使用相關文章的情境,實作多對多(manytomany)關聯資料表,並且實作 admin, form, template 的使用範例。
Model
models.py
from django.db.models import ManyToManyField
class Article(Model):
"""文章"""
# 相關文章
related_articles = ManyToManyField("self", blank=True, symmetrical=False)
ManyToManyField() 參數說明
self
設定關聯自身,一篇文章有多篇相關文章。blank
設定可留空。symmetrical
設定非對稱,也就是 A 相關文章有 B,但不代表 B 一定有相關文章 A。
Admin
admin.py
from django.contrib.admin import ModelAdmin
from models import Article
class ArticleAdmin(ModelAdmin):
"""文章"""
filter_horizontal = ("related_articles",)
list_display = (
"display_related_articles",
)
def display_related_articles(self, article: Article) -> str:
"""顯示相關文章
Args:
article (Article): 主文章
Returns:
str: 相關文章標題(編號)
"""
# 相關文章
articles: list[Article] = article.related_articles.all()
return "\n".join(
[
f"{related_article.title}({related_article.id})"
for related_article in articles
]
)
def formfield_for_manytomany(self, db_field, request, **kwargs):
if db_field.name == "related_articles":
article_id = request.resolver_match.kwargs.get("object_id")
if article_id:
kwargs["queryset"] = Article.objects.exclude(pk=article_id)
return super().formfield_for_manytomany(db_field, request, **kwargs)
display_related_articles.short_description = "Related Articles"
site.register(Article, ArticleAdmin)
filter_horizontal
使用可以過濾的操作介面,UI 畫面較為豐富。list_display
使用自定義的display_related_articles()
方法。display_related_articles()
自定義相關文章顯示的資料,manytomany 欄位無法直接使用。formfield_for_manytomany()
用來過濾相關文章自身 id。short_description
顯示欄位的名稱。
Form
forms.py
from django.forms import ModelForm
from models import Article
class ArticleForm(ModelForm):
"""文章表單"""
class Meta:
model = Article
fields = [
"related_articles",
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 過濾自身 Id
self.fields["related_articles"].queryset = Article.objects.exclude(
id=self.instance.id
)
Views
Form
在 view 中一如往常使用 form 即可。
views.py
from forms import ArticleForm
from models import Article
article: Article = Article.objects.get(id=1)
article_form = ArticleForm(instance=article)
# POST request
if request.method == "POST":
article_form = ArticleForm(request.POST, request.FILES, instance=article)
取得相關文章
取得 article id=1 的相關文章範例。
views.py
from models import Article
article = Article.objects.get(id=1)
related_articles = article.related_articles.all()
Template
Form
表單的樣板 manytomany 欄位預設使用的是 <select multiple>
但沒有搜尋框,用起來可能會不太方便,而我也沒有找到內建有其他 widget 的替代方案。
為了讓選擇欄位有搜尋框,我選擇使用 bootstrap-select,這是基於 bootstrap 前端框架的套件,在安裝 bootstrap-select 之前需要安裝 Bootstrap 和 jQuery。
選擇你想要的安裝方式安裝 bootstrap-select 之後,按照原本 form template 的方式渲染表單,大概如下,詳情請參考 Django Form。
{{ form.as_p }}
然後使用 bootstrap-select 套件修改相關文章的 select 元素,語法大概如下。
document.addEventListener('DOMContentLoaded', () => {
// 相關文章使用 bootstrap-select plugin
$('#id_related_articles').selectpicker({
// 搜尋框
liveSearch: true,
// bootstrap button style
style: 'btn-info'
})
})
若沒有發生任何問題,你會看到相關文章欄位已經套用 bootstrap-select。
article object
假設渲染到 template 有包含 Article Model 的資料,假設叫做 article
。
你可以這樣使用相關文章。
{% for related_article in article.related_articles.all %}
{% endfor %}
結論
在文章數不多的情況下,這樣的 UI 操作不會有什麼問題,但如果文章成千上萬篇的話,可能就需要修改一下設定相關文章的方式,像是使用一個文字欄位輸入文章 id 以逗號區隔,或者使用更複雜的過濾方式篩選相關文章,重點就是不能一次顯示所有文章的資料,否則會造成效能負荷過大。
一杯咖啡的力量,勝過千言萬語的感謝。
支持我一杯咖啡,讓我繼續創作優質內容,與您分享更多知識與樂趣!