Django 多對多關聯範例


建立時間: 2023年11月7日 08:26
更新時間: 2023年11月8日 01:40

說明

本篇將使用相關文章的情境,實作多對多(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 以逗號區隔,或者使用更複雜的過濾方式篩選相關文章,重點就是不能一次顯示所有文章的資料,否則會造成效能負荷過大。

觀看次數: 567
djangofieldmanytomanyorm
按讚追蹤 Enjoy 軟體 Facebook 粉絲專頁
每週分享資訊技術

一杯咖啡的力量,勝過千言萬語的感謝。

支持我一杯咖啡,讓我繼續創作優質內容,與您分享更多知識與樂趣!