DjangoのmodelでN:Nの関係を作る方法

DjangoのmodelでN:Nの関係を作る方法をご紹介します。
「ManyToManyField」を使用します。

条件

  • Django 3.1.2
  • Python 3.7.0

概要

ここでは、「アカウント:場所 = 1:N 」の関係から、「アカウント:場所 = N:N 」の関係にする手順を示します。

アカウント:場所 = 1:N の場合

models.py

models.pyを以下のように定義します。

from django.contrib.auth.models import AbstractUser
from django.db import models
from django.urls import reverse


class CustomUser(AbstractUser):
    def __str__(self):
        return self.username + ":" + self.email


class Location(models.Model):
    """場所モデル"""

    class Meta:
        db_table = 'location'

    name = models.CharField(verbose_name='地名', max_length=255)
    memo = models.CharField(verbose_name='メモ', max_length=255, default='', blank=True)
    author = models.ForeignKey(
        'nvn.CustomUser',
        on_delete=models.CASCADE,
    )
    created_at = models.DateTimeField(verbose_name='登録日時', auto_now_add=True)
    updated_at = models.DateTimeField(verbose_name='更新日時', auto_now=True)

    def __str__(self):
        return self.name + ', ' + self.memo + ', ' + self.author.username

    @staticmethod
    def get_absolute_url(self):
        return reverse('locations:index')

urls.py

トップ画面に場所情報一覧を表示します。

from django.urls import path
from . import views

app_name = 'nvn'

urlpatterns = [
    # トップ画面
    path('', views.IndexView.as_view(), name='index'),

    # 詳細画面
    path('locations/<int:pk>/', views.DetailView.as_view(), name='detail'),
]

views.py

一覧画面では、対象アカウントに紐づく情報のみ表示するようにします。

import logging
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views import generic
from .models import Location

logger = logging.getLogger('development')


# 一覧画面
class IndexView(LoginRequiredMixin, generic.ListView):
    
    paginate_by = 5
    template_name = 'nvn/index.html'
    model = Location

    def get_queryset(self):
        current_user = self.request.user
        if current_user.is_superuser:  # スーパーユーザの場合、リストにすべてを表示する。
            return Location.objects.all()
        else:  # 一般ユーザは自分のレコードのみ表示する。
            return Location.objects.filter(author=current_user.id)


# 詳細画面
class DetailView(generic.DetailView):
    model = Location
    template_name = 'nvn/detail.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        self.request.session.modified = True  # セッション更新
        context['location_pk'] = self.kwargs['pk']
        return context

テンプレート(index.html)

一覧画面では、ログインしたアカウント名と紐づく場所一覧を表示します。

{% extends 'base.html' %}

{% block content %}

{% if user.is_authenticated %}
    <h1>ユーザ名</h1>
    <p class="user-name">{{ user.username }} でログイン中です。</p>
{% endif %}

<h1>My Locations</h1>

<section class="">
    <ul>
        {% for location in object_list %}
        <li>
            <h2><a href="{% url 'nvn:detail' location.pk %}">{{ location.name }}</a></h2>
            <p>{{ location.memo }}</p>
        </li>
        {% empty %}
        <li class="no-post">
            <p>No location yet.</p>
        {% endfor %}
    </ul>
</section>
<hr>

{% if is_paginated %}
<section class="pagination">
    <ul>
        <li>
            {% if page_obj.has_previous %}
            <a href="?page={{ page_obj.previous_page_number }}">&lt;&lt; Prev</a>
            {% else %}
            &lt;&lt; Prev
            {% endif %}
        </li>
        <li>
            {% if page_obj.has_next %}
            <a href="?page={{ page_obj.next_page_number }}">Next &gt;&gt;</a>
            {% else %}
            Next &gt;&gt;
            {% endif %}
        </li>
    </ul>
</section>
{% endif %}
{% endblock %}

実行結果

DB情報

ユーザー名「test1」と「test2」を用意します。

場所情報を3つ用意し、それぞれ以下のようにアカウントに紐づけます。

  • test1: 東京、北海道
  • test2: 沖縄

画面の表示

各アカウントに紐づけた通りの表示となります。

アカウント:場所 = N:N の場合

models.py

models.pyにおいて、CustomUserとの紐づけを「ManyToManyField」に変更します。

「makemigrations」および「migrate」を実行して、DBに反映します。

from django.contrib.auth.models import AbstractUser
from django.db import models
from django.urls import reverse


class CustomUser(AbstractUser):
    def __str__(self):
        return self.username + ":" + self.email


class Location(models.Model):
    """場所モデル"""

    class Meta:
        db_table = 'location'

    name = models.CharField(verbose_name='地名', max_length=255)
    memo = models.CharField(verbose_name='メモ', max_length=255, default='', blank=True)
    author = models.ManyToManyField(CustomUser)
    created_at = models.DateTimeField(verbose_name='登録日時', auto_now_add=True)
    updated_at = models.DateTimeField(verbose_name='更新日時', auto_now=True)

    def __str__(self):
        return self.name + ', ' + self.memo + ', ' + str(list(self.author.values_list('username', flat=True)))

    @staticmethod
    def get_absolute_url(self):
        return reverse('locations:index')

実行結果

DB情報

アカウントと場所の紐づけ方法を変更したため、DB上の紐づけはリセット(紐づけなし)されています。
N:Nの関係にしたことで、管理画面では以下のように、場所に対してAuthor(アカウント)を複数設定できるようになります。

今回は以下のように紐づけました。

  • test1: 東京、北海道
  • test2: 沖縄、北海道

画面の表示

各アカウントに紐づけた通りの表示となります。

参考

Django公式ドキュメント

https://docs.djangoproject.com/ja/3.1/topics/db/examples/many_to_many/

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です