【Django】階層化、多対多モデルの実装|動的に編集画面を変更する

【Django】階層化、多対多モデルの実装|動的に編集画面を変更する プログラミング
【Django】階層化、多対多モデルの実装|動的に編集画面を変更する
スポンサーリンク

こんにちは、こがたです。

中間テーブルを利用すると多対多のリレーションをつくることができます。

この記事ではDjangoで階層化された情報を多対多でつなげる方法を紹介します。

編集画面にて階層化された情報の表示を変更する方法もあるので、複雑な構造のデータで編集画面を変更したい場合はご覧ください。

対象読者
  • Djangoの基本を理解している
  • 階層化モデルの利用方法をしりたい
  • 編集画面で動的に表示項目を変えたい
  • 中間テーブルを利用したい

まず、どのようなことを実装していくのか説明します。
階層化モデル、多対多のリレーションについて理解が深まると思います。

次に「models」「views」「forms」「templates」のコードを紹介していきます。

実装内容(階層化、多対多の説明)

この記事ではサンプルとして本のモデルと親ジャンルのモデル、ジャンルのモデルを紐づけて、編集ページを実装していきます。

編集画面

編集画面で親ジャンルを変更すると、あわせて紐づいたジャンルのみ設定できるようにしていきます。

情報を紐付け、選択肢をしぼることでユーザビリティを高めています。

モデルのリレーションはこのようになっています。

モデルのリレーション

「書籍」は1つの「親ジャンル」をもつ構造になっています。

「親ジャンル」は複数の「ジャンル」と紐づいています。

「書籍」は紐づいた「親ジャンル」と紐づいている「ジャンル」を複数もちます。

リレーションまとめ
  • 【書籍】1つの親ジャンル、複数のジャンルをもつ(可変)
  • 【親ジャンル】複数のジャンルと紐づいている(固定)

「親ジャンル」と「ジャンル」は階層化の構造となっています。

「書籍」と「ジャンル」は多対多のリレーションがあり、中間テーブルが必要です。

こがた
こがた

多対多、つまり双方が複数の情報をもつ場合は中間テーブルが必要になります

データベースの知識からPythonでできるすべて(Web,機械学習,スプレイピング)まで学べるスクールPyQについては↓↓↓

階層モデル、多対多の編集画面を実装

ここから編集画面を表示するためのコードを説明していきます。
※Djangoの構造についてはここでは説明しないのでご注意ください

モデル

from django.db import models

# 親ジャンル
class ParentGenre(models.Model):
    name = models.CharField(max_length=200, blank=False, null=False)

    def __str__(self):
        return self.name

# ジャンル
class Genre(models.Model):
    name = models.CharField(max_length=200, blank=False, null=False)
    parent_genre = models.ForeignKey(ParentGenre, on_delete=models.SET_NULL, null=True)

    def __str__(self):
        return self.name

# 書籍
class Book(models.Model):
    title = models.CharField(max_length=100)
    parent_genre = models.ForeignKey(ParentGenre, on_delete=models.SET_NULL, null=True)
    genre = models.ManyToManyField(Genre, related_name='book')

    def __str__(self):
        return self.title

「親ジャンル」と「ジャンル」は一対多で紐づきます。
そのため「ジャンル」に親ジャンルとのForeignKeyを設定します。

同様に「書籍」も親ジャンルのForeignKeyを設定します。

こがた
こがた

ここまではどの言語でもある紐付けですね

重要なのは「書籍」と「ジャンル」を紐づけている以下の箇所です。

genre = models.ManyToManyField(Genre, related_name='book')

ManyToManyField」を利用することで、マイグレーションすると自動的に中間テーブルが作成されます
※テーブルにカラムが追加されることはありません

書籍からジャンルを呼び出すときは’book.genre‘のように呼び出すことができます。

一方、ジャンルから書籍を呼び出すときは’genre.book_set‘となります。
そこで「related_name」を設定することで’genre.book‘で呼び出せるようになります。

フォーム

from django import forms

from .models import *


class BookForm(forms.ModelForm):
    parent_genre = forms.ModelChoiceField(
        label = '親ジャンル',
        queryset = ParentGenre.objects,
        required=False
    )
    genre = forms.ModelMultipleChoiceField(
        label = 'ジャンル',
        widget = forms.CheckboxSelectMultiple,
        queryset = Genre.objects.all()
    )

    class Meta:
        model = Book
        fields = ('title', 'parent_genre', 'genre',)
        labels={
           'title':'本のタイトル',
           }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-control'

「書籍」は1つの「親ジャンル」をもちます。
そのため「ModelChoiceField」を使用します。
テンプレートで表示するとセレクトボックスとなります。

「ジャンル」は複数紐づく形になるので「ModelMultipleChoiceField」を使用します。
テンプレートで表示するとチェックボックスとなります。

「親ジャンル」「ジャンル」ともに、固定された全てのデータを編集画面に表示するため「queryset」で全てのデータを取得しています。

ビュー

from django.shortcuts import render
from django.views import generic
from django.shortcuts import resolve_url

from .models import *
from .forms import *

class BookCreateView(generic.CreateView):
    model = Book
    form_class = BookForm
    template_name = "books/book_form.html"

    def get_context_data(self, **kwargs):
        context= generic.CreateView.get_context_data(self, **kwargs)
        parent_genre = ParentGenre.objects.all()
        context.update({
            'parent_genre': parent_genre,
            })
        return context

    def get_success_url(self):
        return resolve_url('books:book_add', pk=self.object.pk)

ここではCreateView(汎用ビュー)を使用します。

テンプレート側でjQueryで表示制限するためにcontextにすべてのジャンルデータを格納しています。

テンプレート

共通パーツ(base.html)と編集画面(book_form.html) を作成していきます。

<!doctype html>
<html lang="ja">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css"
          integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous">
    
    {% load static %}
    <script src="{% static 'items/js/jquery-3.5.1.min.js' %}"></script>
    {% block extrajs %}{% endblock %}

    <title>商品サンプル</title>
</head>
<body>
    <!-- メインコンテント -->
    <div class="container mt-3">
        {% block content %}{% endblock %}
    </div>

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
            integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
            crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.0/umd/popper.min.js"
            integrity="sha384-cs/chFZiN24E4KMATLdqdvsezGxaGsi4hLGOzlXwp5UZB1LY//20VyM2taTB4QvJ"
            crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js"
            integrity="sha384-uefMccjFJAIv6A+rW+L4AHf99KvxDjWSu1z9VI8SKNVmz4sk7buKt/6v9KI65qnm"
            crossorigin="anonymous"></script>
</body>
</html>

共通パーツで、ダウンロードしておいたjQueryを読み込んでいます。

{% block extrajs %}{% endblock %}‘で各画面ごとに設定したいscriptを読み込みます。

詳細は↓↓↓

次に編集画面のテンプレートを作成していきます。

{% extends "books/base.html" %}
{% block content %}

    <form method="post" novalidate>
    {% csrf_token %}

    {{ form }}
    <button type="submit">save</button>
    </form>

{% endblock %}


{% block extrajs %}
    <script>
        $('#id_genre').children().remove();
        const genres = {
            {% for p_genre in parent_genre %}
                '{{ p_genre.pk }}': [
                    {% for genre in p_genre.genre_set.all %}
                        {
                            'pk': '{{ genre.pk }}',
                            'name': '{{ genre.name }}',
                            'checked': {% if genre.pk in checked_ids %}true{% else %}false{% endif %},
                        },
                    {% endfor %}
                ],
            {% endfor %}
        };


        const changeCategory = (select) => {
            // ジャンルの選択欄を空にする。
            $('#id_genre').children().remove();

            // 選択した親ジャンルに紐づくジャンルの一覧を取得する。
            const bigId = $('#id_parent_genre').val();
            const genreList = genres[bigId];

            // ジャンルの選択肢を作成・追加。
            var count = 0;
            for (const genre of genreList) {
                const li = $('<li>');
                const label = $('<label for="id_genre_' + count + '">');
                const input = $('<input>');
                input.attr("type", "checkbox")
                input.attr("name", "genre")
                input.val(genre['pk']);
                input.attr("class", "form-control")
                input.attr("id", "id_genre_"+count++)
                input.attr("checked", genre['checked']);
                label.text(genre['name']);
                label.append(input)
                li.append(label)
                $('#id_genre').append(li);
            }

            // 指定があれば、そのジャンルを選択する
            if (select !== undefined) {
                $('#id_genre').val(select);
            }
        };


        $(document).on('change', '#id_parent_genre', function(){
            changeCategory();
        });


        // 入力値に問題があって再表示された場合、ページ表示時点でジャンルが絞り込まれるようにする
        if ($('#id_parent_genre').val()) {
            const selectedCategory = $('#id_genre').val();
            changeCategory(selectedCategory);
        }

    </script>
{% endblock %}

大事なのは{% block extrajs %}・・・{% endblock %}の部分です。

Djangoで作成したHTMLの要素は「id_フィールド名」というidが設定されます。

親ジャンルが変更されるたびに「changeCategory」が呼び出され、ジャンル項目を作成しなおしています。

また、以下の箇所で編集後にチェックされた状態で表示することができるようになっています。

'checked': {% if genre.pk in checked_ids %}true{% else %}false{% endif %},

views.pyのcontextに「checked_ids」という名前で選択されたジャンルのidリストを渡すことで動作します。

最後に

紹介した階層化、多対多のつながりを理解することができれば、どのような情報構造も実装することができます!

各種リレーションのつなげかたを理解できたらフロントサイドで表示を切り替えればユーザビリティの高いページを表示することもできるでしょう。

1つ1つの用途を理解して適切に利用していきましょう。

最後まで読んでくださり、ありがとうございました!!!

コメント

タイトルとURLをコピーしました