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

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

## 環境
- Ubuntu 16.04 LTS
- Django 2.2
- Python 3.6

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

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

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

```python: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
```

# 使用方法
## 各インスタンス生成～タグ付けまで

```python
# タグの生成
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`を起点にした操作は、変数自体の属性が持つ`all`、`add`、`remove`などの各種メソッドを利用して操作を行うことができる。

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

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

tag_list = a.tags.all()
```

## 記事へタグを追加
タグの追加は`add`メソッドで行うことが可能。

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

a.tags.add(t)
```

## 記事へタグを設定(置き換え)
タグの設定は`set`メソッドで一括して行うことも可能。
※ 不足分の追加ではなく、置き換えとなるので注意。

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

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

## 記事から特定タグを削除
関連付けられた１つのタグを削除したい場合は、削除したいタグのインスタンスを`remove`メソッドで指定することで削除できる。
※ これは記事`Article`とタグ`Tag`の関係が削除されるのみで、タグ`Tag`そのものは削除されない。

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

a.tags.remove(t)
```

## 記事からタグを一括削除
`clear`メソッドを使用すると、関連付けられたタグを一括で削除することも可能。
※ これも記事`Article`とタグ`Tag`の関係が削除されるのみで、タグ`Tag`そのものは削除されない。

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

a.tags.clear()
```

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

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

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

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

```python
# TagのnameがDjangoというタグがつけられた全ての記事(Article)を取得
article_list = Article.objects.filter(tags__name="Django")
```

# タグ(Tag)を起点にした操作
`ManyToManyField`を設定しなかったタグ側にも、逆参照という形で操作を行うこともできる。
対向側には`モデル名_set`という属性が自動的に付加されているので、これを使用して操作を行う。

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

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

article_list = t.article_set.all()
```

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

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

t.article_set.add(a)
```

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

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

t.article_set.remove(a)
```

## 特定のタグを全ての記事から削除
`clear`メソッドでその時、そのタグを付加されている全ての記事から削除することが可能。
この操作に関しては、記事を起点に操作するよりも、タグ起点で行った方がシンプルになると思われる。

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

t.article_set.clear()
```

# 補足: ManyToManyFieldを使用する場合の注意
多対多の関係を実現する`ManyToManyField`を使用するには、紐づけられる双方のインスタンスが主キーを持っている必要がある。
Djangoのモデルでは主キーを明示的に設定しなくても、デフォルトで`id`という主キーとなる変数が追加されますが、これは`create`メソッドでインスタンスを生成するか、`save`メソッドを呼び出してDBへ初回保存された際に値が確定・生成されるため、注意が必要。
主キーが確定しない状態で`add`メソッドを用いて要素の追加を行うとエラーとなってしまう。

```python:エラー例
>>> 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へ保存したくない場合は、トランザクション制御の機能などを利用する必要がある。

```python:トランザクションの実装例
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/