見出し画像

DjangoのModelリレーションシップの基礎を学ぶレシピ

このレシピでは、Djangoのモデルを定義上で必要なモデルのリレーションシップに焦点を当てて、その基礎を学ぶことができるレシピです。

具体的には、以下の3種類のリレーションシップについて、具体的な事例をもとに実装しながら学べるようになっています。


1.事前準備

このレシピでは、実際にモデルの実装を行いながら理解を深めていきます。

そのため、最低限必要なDjango環境の準備を行います。

まず、仮想環境を作成してアクティベートします。

python -m venv env
env\scripts\activate

次に必要なモジュールをインポートします。

pip install django

次にDjangoプロジェクトを作成します。

django-admin startproject config .

また、アプリケーション(app)を作成します。

django-admin startapp app

config\settings.pyの初期カスタマイズを行います。

作成したアプリケーション(app)と今回利用するライブラリを有効にするためINSTALLED_APPSに以下のエントリーを追加します。

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app.apps.AppConfig',   #追加
]

言語とタイムゾーンを日本に変更しておきます。

LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'

以上で事前準備は完了です。

2.データベースの基礎

まず最初に、データベースについてあまり知らない方向けに抑えておきたいデータベースの基礎知識について簡単に解説します。

既にご存知の方は読み飛ばしていただいて問題ありません。

リレーショナルデータベースの基礎

リレーショナルデータベースとは、データを複数の表(テーブル)として管理します。
表と表の間の関係を定義することで、様々なデータの関連性を扱うことができます。

例えば、会社に所属する社員情報を管理するために、以下のような複数の表(テーブル)を用いてデータを管理することができます。

社員テーブルと部署テーブルは、「部署コード」をキーとして各テーブル間のデータの関連性を表現する個ができます。

例えば、社員ID[A0001」の斎藤さんの部署コードは「D01」であり、部署テーブルの部署コード「D01]を見ると部署名が「人事」であることがわかります。

このように、社員と部署の関係性を2つのテーブルを使って表現します。

1対1のリレーション

1対1のリレーションとは、あるデータが必ず他のデータの1つに関連付けられているという関係性を言います。

具体例を挙げると、以下のようなケースです。

  • 特定ユーザとG-mailアドレスの関係(1対1の関係)

  • 書籍と書籍に紐づくISBN番号の関係(1対1の関係)

  • 日本国民一人一人とマンナンバー番号の関係(1対1の関係)

Djangoでは、2つのモデル間で1対1の関係を定義するにはOneToOneFieldを使います。

以下は、日本国民(Personモデルクラス)とマイナンバー(MynumberCardモデルクラス)の間に1対1のリレーションを定義する実装例です。

実際にapp\models.pyに以下のコードを追加しましょう。


from django.db import models

class MynumberCard(models.Model):
    mynumber = models.IntegerField(verbose_name ='マイナンバー', max_length=12)
    def __str__(self):
        return str(self.mynumber)

class Person(models.Model):
    email = models.EmailField(verbose_name ='メールアドレス', null = False, blank=False, unique = True)
    name = models.CharField(verbose_name ='名前', max_length=150, null = False, blank=False)
    mynumber = models.OneToOneField(MynumberCard,verbose_name = 'マイナンバー', on_delete = models.CASCADE)
    def __str__(self):
        return str(self.name)

Personモデルクラス内のmynumberフィールドがOneToOneFieldになっていてMynumberCardモデルクラスと1対1の関係を定義しています。

1対1のリレーションを定義する場合は、以下の形式で定義します。

column_name = models.OneToOneField(<参照先のmodelクラス>, on_delete=<オプション>,<その他のオプション>)

on_deleteオプションは、モデル間のデータの扱い方法を定義するオプションで、以下のようなオプションがあります。

では、実際にマイグレーションを行いテーブルを作成してみましょう。

python manage.py makemigrations
System check identified some issues:
WARNINGS:
app.MynumberCard.mynumber: (fields.W122) 'max_length' is ignored when used with IntegerField.
        HINT: Remove 'max_length' from field
Migrations for 'app':
  app\migrations\0001_initial.py
    - Create model MynumberCard
    - Create model Person
python manage.py migrate
System check identified some issues:
WARNINGS:
app.MynumberCard.mynumber: (fields.W122) 'max_length' is ignored when used with IntegerField.
        HINT: Remove 'max_length' from field
Operations to perform:
  Apply all migrations: admin, app, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying app.0001_initial... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK

DBシェルモードを起動し、実際に生成されたテーブルと定義情報を確認してみましょう。

※データベースはデフォルトのSqliteDBを利用しています。

python manage.py dbshell
SQLite version 3.32.3 2020-06-18 14:00:33
Enter ".help" for usage hints.
sqlite>

上記画面に切り替わったら、まずはテーブル一覧を確認します。

sqlite> .tables
app_mynumbercard            auth_user_groups
app_person                  auth_user_user_permissions
auth_group                  django_admin_log
auth_group_permissions      django_content_type
auth_permission             django_migrations
auth_user                   django_session

先ほど定義したPersonモデルクラスと、MynumberCardモデルクラスで定義したテーブルがそれぞれapp_personテーブル、app_mynumbercardテーブルとして実装されているのが確認できます。

では、どのようなフィールド定義で作成されているか確認します。

sqlite> .schema app_mynumbercard
CREATE TABLE IF NOT EXISTS "app_mynumbercard" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "mynumber" integer NOT NULL);
sqlite> .schema app_person
CREATE TABLE IF NOT EXISTS "app_person" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "email" varchar(150) NOT NULL UNIQUE, "name" varchar(150) NOT NULL, "mynumber_id" bigint NOT NULL UNIQUE REFERENCES "app_mynumbercard" ("id") DEFERRABLE INITIALLY DEFERRED);

schema <テーブル名>で指定したテーブルの定義情報を確認することができます。

上記結果をまとめると、各テーブルのフィールドは以下の様になっています。

次に、実際にデータを登録して1対1のリレーションの挙動を確認してみます。

まず、PersonMynumberCardモデルクラスをadminサイトに登録するためapp\admin.pyに以下のコードを追加します。


from django.contrib import admin
from .models import MynumberCard, Person

class MynumberCardAdmin(admin.ModelAdmin):
    list_display=('pk','mynumber')

class PersonAdmin(admin.ModelAdmin):
    list_display=('pk','email','name','mynumber')

admin.site.register(Person, PersonAdmin)
admin.site.register(MynumberCard, MynumberCardAdmin)

以下のコマンドを実行して管理者ユーザを作成します。
ユーザ名は何でもOKですが、ここではadminユーザを作成しています。

python manage.py createsuperuser
ユーザー名 (leave blank to use 'xxx'): admin
Error: That ユーザー名 is already taken.
ユーザー名 (leave blank to use 'xxx'):
メールアドレス:
Password:
Password (again):
Superuser created successfully.

次に、以下のコマンドを実行して開発サーバを起動後にadminサイト(http://127.0.0.1:8000/admin)にアクセスしましょう。

python manage.py runserver

先ほど作成した管理者ユーザでログオンすると、以下のようにPersonsMynumber cardsが表示されれます。

まず、Mynumber cardsの「追加」ボタンからマイナンバー番号(13桁の数字)を適当に2件ほど登録しましょう。

次に、Personsの「追加」ボタンから1件データを登録します。
ここでは、事前に登録したマイナンバー「134345677543」を選択してPersonテーブルにデータを登録しました。

登録後は以下のような画面になります。

更にもう1件Poersonsにデータを登録してみましょう。
この時、先ほど既に登録済みのマイナンバー「134345677543」を再度選択して保存ボタンを押してみましょう。

すると、下図のように「この マイナンバー を持った Person が既に存在します。」と表示され、マイナンバー(mynumber)と国民一人一人(person)が1対1の関係になっていることが確認できます。

このような1対1の関係性を定義したい場合、DjangoではOneToOneFieldを利用します。

1対多のリレーション

次に、1対多のリレーションについて見ていきましょう。

1 対多のリレーションは、テーブル内の1つのレコードが別のテーブルの2つ以上のレコードと関連付けされているような関係性です。

具体例を挙げると、以下のような関係性が挙げられます。

  • ある本の著者(1)に対して複数の書籍(M)が存在している関係(1対多の関係)

  • ブログの投稿者(1)に対して複数のブログ記事(M)が存在している関係(1対多の関係)

  • ある注文(1)に対して複数の明細データ(M)が存在している関係(1対多の関係)

Djangoで1対多の関係性を定義するにはForeignKeyというフィールドを定義します。

それでは具体的な事例で1対多のモデルを定義してみましょう。

ここでは、カテゴリとブログ記事の関係を1対多のリレーションとしてDjangoのモデルで定義します。

あるブログ記事には必ず1つのカテゴリを割り当てるものとします。

また、カテゴリ視点はあるカテゴリは複数のブログ記事に割り当てあれる可能性があるので、カテゴリとブログ記事の関係は1対多となります。

基本的には、多側のテーブルにForeignKeyフィールドを定義して、1側のテーブルを参照するような構成にします。

それでは実際にモデルを定義していきます。

app\models.pyに以下のコードを追加します。

class Category(models.Model):
    category_name = models.CharField(max_length=100)

    def __str__(self):
        return self.category_name

class Blog(models.Model):
    title = models.CharField("タイトル", max_length=100)
    content = models.TextField("内容")
    postdate = models.DateField(auto_now_add=True)
    category = models.ForeignKey(Category, on_delete = models.PROTECT,verbose_name ="カテゴリ")

    def __str__(self):
        return self.title

カテゴリ(category)クラスにはカテゴリ名フィールド(category_name)の未定義しています。

ブログ記事(Bolog)クラスにはタイトル(title)、内容(content)、投稿日(postdate)を定義しています。

また、categoryフィールドでForeignKeyを使いCategoryテーブルを参照する外部参照の関係性を定義しています。

category = models.ForeignKey(Category, on_delete = models.PROTECT,verbose_name ="カテゴリ")

上記のようにForegnKeyを定義することで1対多の関係を定義することができます。

それでは実際にマイグレーションを行いデータベースにテーブルを作成してみましょう。

python manage.py makemigrations
Migrations for 'app':
  app\migrations\0003_blog_category.py
    - Create model Category
    - Create model Blog

python manage.py migrate
Operations to perform:
  Apply all migrations: admin, app, auth, contenttypes, sessions
Running migrations:
  Applying app.0003_blog_category... OK

各テーブル間の関係性は以下の様になります。

実際にadminサイト上でデータを登録して、1対多の関係性を確認してみましょう。

Category、Blogdモデルクラスをadminサイトに登録するためapp\admin.pyに以下のコードを追加します。

from django.contrib import admin
from .models import MynumberCard, Person, Blog, Category  #修正

class MynumberCardAdmin(admin.ModelAdmin):
    list_display=('pk','mynumber')


class PersonAdmin(admin.ModelAdmin):
    list_display=('pk','email','name','mynumber')


#ここから下を追加
class BlogAdmin(admin.ModelAdmin):
    list_display=('pk','title', 'content', 'postdate', 'category')


class CategoryAdmin(admin.ModelAdmin):
    list_display=('pk','category_name')


#ここまでを追加
admin.site.register(Person, PersonAdmin)
admin.site.register(MynumberCard, MynumberCardAdmin)
admin.site.register(Blog, BlogAdmin)   #追加
admin.site.register(Category, CategoryAdmin)#追加

adminサイトに再度アクセスして、カテゴリ名をいくつか登録しましょう。

以下の例では、3つのカテゴリ名を登録しました。

次に、ブログ記事を登録します。
この例では、カテゴリで「スポーツ」を選択して保存します。

同様に、同じカテゴリ「スポーツ」を選択して別の記事も登録してみましょう。

以下の様に、あるカテゴリ「スポーツ」が複数のブログ記事に割り当てられているという1対多の関係性が確認できました。

多対多のリレーション

最後に多対多のリレーションについて見ていきましょう。

多対多のリレーションは、あるテーブルの複数のレコードが別のテーブルの複数のレコードと関連付けられているような関係性を言います。

具体例を挙げると、以下のような関係性が挙げられます。

  • 複数の人(M)に対して複数のFaceBookグループ(M)が関連づけられている状態(多対多の関係)

  • 複数のレシピ(M)に対して複数の食材(M)が関連付けられている状態(多対多の関係)

  • 複数のブログ(M)に対して複数のタグ(M)が割り当てられている状態(多対多の関係)

もう少し補足すると、ある人は複数のグループに所属しており、あるグループには複数の人が属しているという関係性が多対多です。

ある料理のレシピには複数の食材が使われており、ある食材は複数のレシピで使われているという関係性が多対多です。

Djangoで多対多の関係性を定義するにはMyanyToManyFieldというフィールドを定義します。

それでは具体的な事例で多対多のモデルを定義してみましょう。

ここでは、ブログ記事とタグ関係を多対多のリレーションとしてDjangoのモデルで定義します。

あるブログ記事には複数のタグが付けられ、またあるタグは複数のブログ記事でタグ付けされるという多対多の関係性となります。

それでは実際にモデルを定義していきます。

app\models.pyに以下のコードを追加します。

class Tag(models.Model):
    name = models.CharField('タグ', max_length=50)

    def __str__(self):
        return self.name


class Blog(models.Model):
    title = models.CharField("タイトル", max_length=100)
    content = models.TextField("内容")
    postdate = models.DateField(auto_now_add=True)
    category = models.ForeignKey(Category, on_delete = models.PROTECT,verbose_name ="カテゴリ")
    tag = models.ManyToManyField(Tag, verbose_name='タグ')  #追加

    def __str__(self):
        return self.title

新規にTagモデルクラスを追加しています。

またBlogモデルクラスに以下のtagフィールドを追加しています。
ManyToManyFieldを使い、参照先にTagモデルクラスを指定することで、TagモデルクラスとBlogモデルクラス間で多対多のリレーションを定義します。

tag = models.ManyToManyField(Tag, verbose_name='タグ') 

それでは、マイグレーションを行いデータベースにテーブルを追加しておきます。

python manage.py makemigrations
Migrations for 'app':
  app\migrations\0004_auto_20210812_1322.py
    - Create model Tag
    - Add field tag to blog
python manage.py migrate
Operations to perform:
  Apply all migrations: admin, app, auth, contenttypes, sessions
Running migrations:
  Applying app.0004_auto_20210812_1322... OK

まずは、SqliteDBのテーブル一覧を確認してみましょう。

dbshellコマンドを実行し、「.tables」でテーブル一覧を確認します。

python manage.py dbshell
SQLite version 3.32.3 2020-06-18 14:00:33
Enter ".help" for usage hints.
sqlite> .tables
app_blog                    auth_permission
app_blog_tag                auth_user
app_category                auth_user_groups
app_mynumbercard            auth_user_user_permissions
app_person                  django_admin_log
app_tag                     django_content_type
auth_group                  django_migrations
auth_group_permissions      django_session
sqlite>

先ほど追加したのはTagモデルクラスだけですが、2つのテーブルが追加されていることが確認できます。

以下の2つのテーブルが追加されています。

データベースの世界で多対多の関係を構築するには、中間テーブルを作成して互いのテーブルを外部参照するような構成をとります。

ただし、DjangoではManyToManyFieldを定義すると上記の様に自動で中間テーブル(app_blog_tag)が生成されます。

では、ここで中間テーブル(app_blog_tag)の定義を確認してみましょう。

.schema app_blog_tag
CREATE TABLE IF NOT EXISTS "app_blog_tag" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "blog_id" bigint NOT NULL REFERENCES "app_blog" ("id") DEFERRABLE INITIALLY DEFERRED, "tag_id" bigint NOT NULL REFERENCES "app_tag" ("id") DEFERRABLE INITIALLY DEFERRED);
CREATE UNIQUE INDEX "app_blog_tag_blog_id_tag_id_c2cbc2a3_uniq" ON "app_blog_tag" ("blog_id", "tag_id");
CREATE INDEX "app_blog_tag_blog_id_7a8f0c47" ON "app_blog_tag" ("blog_id");
CREATE INDEX "app_blog_tag_tag_id_e851497a" ON "app_blog_tag" ("tag_id");

色々表示されますが、以下の部分が中間テーブルの列定義情報です。

CREATE TABLE IF NOT EXISTS "app_blog_tag" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "blog_id" bigint NOT NULL REFERENCES "app_blog" ("id") DEFERRABLE INITIALLY DEFERRED, "tag_id" bigint NOT NULL REFERENCES "app_tag" ("id") DEFERRABLE INITIALLY DEFERRED);

blog_idtag_idという列が定義されていることがわかります。
これは、それぞれapp_blogテーブルとapp_tagテーブルを外部参照するようになっています。

全体の構成をまとめると以下の様になっています。

中間テーブル(app_blog_tag)を介すことでそれぞれapp_blogテーブルとapp_tagテーブルの複数のレコード間の関連性を表現することができます。

では、実際にadminサイト上で多対多のデータを登録してみましょう。

先ほど追加したTagモデルクラスをadminサイトに登録するためapp\admin.pyに以下のコードを追加します。

from .models import MynumberCard, Person, Blog, Category, Tag #Tagを追加

#ここから下を追加
class TagAdmin(admin.ModelAdmin):
    list_display=('pk','name')
#ここまでを追加

admin.site.register(Person, PersonAdmin)
admin.site.register(MynumberCard, MynumberCardAdmin)
admin.site.register(Blog, BlogAdmin) 
admin.site.register(Category, CategoryAdmin)
admin.site.register(Tag, TagAdmin)#追加

adminサイトを更新するとTagsが表示されるので、以下の様にいくつかタグを登録します。

次に、Blogsテーブルですでに登録済みのデータをクリックして内容を表示してみましょう。
下図の赤枠通り、タグフィールドが表示され、Ctrlキーを押しながらクリックすることで複数のタグを選択できる状態になっています。

本レシピで解説した1対1,1対多、多対多といったリレーションはWebアプリケーションを開発する上での重要な基礎部分です。

実際にモデルを実装し、どのようなデータ定義になっているか確認しながらよく理解するようにしましょう。

以上でこのレシピは完了です。

主にITテクノロジー系に興味があります。 【現在興味があるもの】 python、Django,統計学、機械学習、ディープラーニングなど。 技術系ブログもやってます。 https://sinyblog.com/