Posted at

Djangoの多対多関係モデルで簡易タグ機能を作る


はじめに

Djangoの多対多(ManyToMany)関係のモデルを利用して、簡易的なタグ機能を作成してみました。


環境


  • Ubuntu 16.04 LTS

  • Django 2.1

  • Python 3.6


作成したもの

ブログシステムなどでよくある、投稿記事に対してジャンルごとにタグづけが行える機能のようなイメージで、記事に対してタグを付加・削除したり、特定タグのつけられた記事の検索を行うことができるモデルを作ってみました。


モデル

今回、作成したモデルはシンプルにタグを定義するTagと、記事を定義するArticleの2つ。

記事とタグの紐づけはManyToManyFiledを使用することで実現し、これによりDjango側でTag-Article間の多対多の関係を紐づける中間テーブルが自動的に生成される。


models.py

from django.db import models

# タグ
class Tag(models.Model):
name = models.CharField(max_length=32)

def __str__(self):
return self.name

# 記事
class Article(models.Model):
title = models.CharField(max_length=128)
tags = models.ManyToManyField(Tag)

def __str__(self):
return self.title



使用方法


各インスタンス生成~タグ付けまで

# タグの生成

t1 = Tag.objects.create(name="Django")
t2 = Tag.objects.create(name="Python")

# 記事の生成
a = Article.objects.create(title="Djangoでタグ機能を作る")

# 記事にタグを付加
a.tags.add(t1)
a.tags.add(t2)

# 更新
a.save()


記事(Article)を起点にした操作

ManyToManyFieldを設定したArticleを起点にした操作は、変数自体の属性が持つalladdremoveなどの各種メソッドを利用して操作を行うことができる。


記事に付加されたタグの取得

Articleインスタンスに付加されたタグを確認するには、allメソッドで取得できる。

a = Article.objects.get(name="Djangoでタグ機能を作る")

tag_list = a.tags.all()


記事へタグを追加

タグの追加はaddメソッドで行うことが可能。

a = Article.objects.get(name="Djangoでタグ機能を作る")

t = Tag.objects.get(name="Django")

a.tags.add(t)


記事へタグを設定(置き換え)

タグの設定はsetメソッドで一括して行うことも可能。

※ 不足分の追加ではなく、置き換えとなるので注意。

t1 = Tag.objects.get(name="Django")

t2 = Tag.objects.get(name="Python")
a = Article.objects.get(name="Djangoでタグ機能を作る")

a.tags.set([t1, t2])


記事から特定タグを削除

関連付けられた1つのタグを削除したい場合は、削除したいタグのインスタンスをremoveメソッドで指定することで削除できる。

※ これは記事ArticleとタグTagの関係が削除されるのみで、タグTagそのものは削除されない。

a = Article.objects.get(name="Djangoでタグ機能を作る")

t = Tag.objects.get(name="Django")

a.tags.remove(t)


記事からタグを一括削除

clearメソッドを使用すると、関連付けられたタグを一括で削除することも可能。

※ これも記事ArticleとタグTagの関係が削除されるのみで、タグTagそのものは削除されない。

a = Article.objects.get(name="Djangoでタグ機能を作る")

a.tags.clear()


特定のタグが付けられた記事を検索・取得

特定のタグがつけられた記事を検索する場合は、通常の絞り込みと同様にfilterメソッドでTagのインスタンスを条件に指定することで検索できる。

t = Tag.objects.get(name="Django")

article_list = Article.objects.filter(tags=t)

タグのインスタンスを用いて検索することに加えて、次のようなタグTagの持つ属性を利用した検索も可能。

# TagのnameがDjangoというタグがつけられた全ての記事(Article)を取得

article_list = Article.objects.filter(tags__name="Django")


タグ(Tag)を起点にした操作

ManyToManyFieldを設定しなかったタグ側にも、逆参照という形で操作を行うこともできる。

対向側にはモデル名_setという属性が自動的に付加されているので、これを使用して操作を行う。


特定のタグのついた記事の取得

タグを起点に、モデル名_setallメソッドで該当のタグが付加されている全ての記事を取得することが可能。

t = Tag.objects.get(name="Django")

article_list = t.article_set.all()


特定のタグを特定の記事につける

記事にタグをつける場合と同様addメソッドでタグの付加が可能。

a = Article.objects.get(name="Djangoでタグ機能を作る")

t = Tag.objects.get(name="Django")

t.article_set.add(a)


特定のタグを特定の記事から削除

記事にタグをつける場合と同様removeメソッドでタグの削除が可能。

a = Article.objects.get(name="Djangoでタグ機能を作る")

t = Tag.objects.get(name="Django")

t.article_set.remove(a)


特定のタグを全ての記事から削除

clearメソッドでその時、そのタグを付加されている全ての記事から削除することが可能。

この操作に関しては、記事を起点に操作するよりも、タグ起点で行った方がシンプルになると思われる。

t = Tag.objects.get(name="Django")

t.article_set.clear()


補足: ManyToManyFieldを使用する場合の注意

多対多の関係を実現するManyToManyFieldを使用するには、紐づけられる双方のインスタンスが主キーを持っている必要がある。

Djangoのモデルでは主キーを明示的に設定しなくても、デフォルトでidという主キーとなる変数が追加されますが、これはcreateメソッドでインスタンスを生成するか、saveメソッドを呼び出してDBへ初回保存された際に値が確定・生成されるため、注意が必要。

主キーが確定しない状態でaddメソッドを用いて要素の追加を行うとエラーとなってしまう。


エラー例

>>> a = Article(title="Djangoでタグ機能を作る")

>>> a.tags.add(t1)
ValueError: "<Article: Djangoでタグ機能を作る>" needs to have a value for field "id" before this many-to-many relationship can be used.

createで生成するか、ManyToManyFieldへ値を設定する前にsaveを一度呼び出してDBへ保存しておけばエラーとならないのですが、一時的に中途半端な状態でDBへ保存されてしまうことになるため、完全に出来上がるまでDBへ保存したくない場合は、トランザクション制御の機能などを利用する必要がある。


トランザクションの実装例

from django.db import transaction

with transaction.atomic():
a = Article(title="Djangoでタグ機能を作る")
a.save() # ここではDBは変更されない

t = Tag.objects.get(name="Django")
a.tags.add(t)
a.save() # ここではDBは変更されない

# withブロックを抜けた時点で変更内容がDBへCommitされる



参考

多対多 (many-to-many) 関係 | Django documentation | Django

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

データベースのトランザクション | Django documentation | Django

https://docs.djangoproject.com/ja/2.1/topics/db/transactions/