Django 多対多の関係先モデルのフィールドでソートする

# はじめに
- この記事は、よりDjangoらしくてスマートな解決方法を求めて試行錯誤した記録です。
    - 試行錯誤の末、なんとか成果は得られたのですが、これで良いものか確証はありません。
    - よりスマートな、あるいは、Djangoらしい解法をご存じでしたら、是非コメントをお寄せください。
- この記事では、DjangoとSQLのチュートリアルを済ませた方を読者として想定しています。

#### 環境
- Python 3.8.8
- Django 3.1.7
- SQLite 3.28.0
- 以下の記事で構築された環境です。
    - [VSCodeでPython・Django + JupyterLabでPython (Windows)](https://qiita.com/tetr4lab/items/5fabb091ec4bbc1d2983) (Qiita)

# 課題
- 多対多の関係を持つ二つのモデルに対して、一方のインスタンス一覧を他方のフィールドでソートします。
    - 多対多の関係のために、間に中間テーブルのモデルが入ります。
- この記事では、「[多対多関係の実装方法](#多対多関係モデルの実装)」と、「[関係先モデルのフィールドをキーにしたソート方法](#書籍リストを著者名でソートする)」に焦点を当てて手法を探ります。

## 題材
- 「著者」と「著書」を登録できるモデルを用意して、「書籍の一覧」ビューを作ります。
    - 似た題材で、よく目にするのは、「タグ」と「記事」でしょうか。

### モデル
- 著者
    - 著者情報のテーブルです。
    - フィールド: [ ID, 名前, メモ, ]
- 書籍
    - 書籍情報のテーブルです。
    - フィールド: [ ID, 書名, 発行日, 出版社, 叢書, 価格, 評価, メモ, ]
- 書籍_著者 (ジャンクション)
    - 書籍と著書に、多対多の関係を与える中間テーブルです。
        - つまり、著者には複数の著作があり、書籍は複数の著者を持つ可能性があります。
    - フィールド: [ ID, 書籍のID, 著者のID, ]

### ビュー
- 書籍の一覧
    - 自テーブルのフィールドと、関係先の著者名を連結した文字列をキーとして、任意にソートできるようにしたいです。
        - 適切なモデルを構築すること、および、後者のソートをよりスマートに実現することが、この記事の課題です。
- その他省略

# 多対多関係モデルの実装
## ForeignKey を使って、(自前で)中間テーブルを作る
- 実は、`ManyToManyField`を知らなかったので、最初にこの方法を試しました。

```py:exlibris/models.py
from django.core import validators
from django.db import models

class Author(models.Model):
    """著者"""
    name = models.CharField(max_length=255)
    add_name = models.CharField(max_length=255, blank=True, verbose_name='additional name')
    memo = models.TextField(blank=True)
    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["name", "add_name"],
                name="author_unique"
            ),
        ]
    def __str__(self):
        return self.name

class Book(models.Model):
    """書籍"""
    title = models.CharField(max_length=255)
    pub_date = models.DateField(null=True, blank=True, verbose_name='date published')
    publisher = models.CharField(max_length=255, blank=True)
    series = models.CharField(max_length=255, blank=True)
    price = models.IntegerField(null=True, blank=True)
    evaluation = models.IntegerField(null=True, blank=True, validators=[validators.MinValueValidator(0), validators.MaxValueValidator(100)])
    memo = models.TextField(blank=True)
    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["title", "publisher", "series"],
                name="book_unique"
            ),
        ]
    def __str__(self):
        return self.title

class author_book(models.Model):
    """junction table for Author <-> Book"""
    author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name='book')
    book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='author')
    def __str__(self):
        return f"{self.author.name} ⇔ {self.book.title}"
```

- この方法の場合は、Adminサイトの`Author`、`Book`どちら側でも、明示的に指定することで相手側をインライン編集できます。
    - この場合でも、中間テーブルをAdminサイトで管理する必要は生じません。

```py:exlibris/admin.py
from django.contrib import admin
from .models import Author, Book, author_book

class AuthorInline(admin.TabularInline):
    model = author_book
    extra = 0
    min_num = 1

class BookInline(admin.TabularInline):
    model = author_book
    extra = 0

class AuthorAdmin(admin.ModelAdmin):
    inlines = [BookInline]

class BookAdmin(admin.ModelAdmin):
    inlines = [AuthorInline]

admin.site.register(Author, AuthorAdmin)
admin.site.register(Book, BookAdmin)
```

## ForeignKey で中間テーブルを作った上で、ManyToManyField も使う
- `ManyToManyField`で`through`を使うことで、Djangoに中間テーブルを作らせず自前のモデルを使わせます。
    - 中間テーブルの`ForeignKey`に逆参照名(`related_name`)を定義しようとすると怒られます。
- 残念ながら、これらの`ManyToManyField`は、Adminサイトでモデルのフィールドとしては扱えず、インライン編集するためには、結局、インラインクラスの定義が必要になります。
    - 結果的に、前述の中間テーブル側で逆参照名を設定する代わりに、それぞれのモデルで`ManyToManyField`を設定しているだけに見えます。

```py:exlibris/models.py
from django.core import validators
from django.db import models

class Author(models.Model):
    """著者"""
    name = models.CharField(max_length=255)
    add_name = models.CharField(max_length=255, blank=True, verbose_name='additional name')
    books = models.ManyToManyField(Book, through='author_book')
    memo = models.TextField(blank=True)
    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["name", "add_name"],
                name="author_unique"
            ),
        ]
    def __str__(self):
        return self.name

class Book(models.Model):
    """書籍"""
    title = models.CharField(max_length=255)
    authors = models.ManyToManyField(Author, through='author_book')
    pub_date = models.DateField(null=True, blank=True, verbose_name='date published')
    publisher = models.CharField(max_length=255, blank=True)
    series = models.CharField(max_length=255, blank=True)
    price = models.IntegerField(null=True, blank=True)
    evaluation = models.IntegerField(null=True, blank=True, validators=[validators.MinValueValidator(0), validators.MaxValueValidator(100)])
    memo = models.TextField(blank=True)
    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["title", "publisher", "series"],
                name="book_unique"
            ),
        ]
    def __str__(self):
        return self.title

class author_book(models.Model):
    """junction table for Author <-> Book"""
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    book = models.ForeignKey(Book, on_delete=models.CASCADE)
    def __str__(self):
        return f"{self.author.name} ⇔ {self.book.title}"
```

## ManyToManyField を使って、片側からのみ設定する
- 一方のモデルで`ManyToManyField`を逆参照(`related_name`)付きで定義し、他方からは逆参照を使います。

```py:exlibris/models.py
from django.core import validators
from django.db import models

class Author(models.Model):
    """著者"""
    name = models.CharField(max_length=255)
    add_name = models.CharField(max_length=255, blank=True, verbose_name='additional name')
    memo = models.TextField(blank=True)
    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["name", "add_name"],
                name="author_unique"
            ),
        ]
    def __str__(self):
        return self.name

class Book(models.Model):
    """書籍"""
    title = models.CharField(max_length=255)
    authors = models.ManyToManyField(Author, related_name='books', related_query_name='book')
    pub_date = models.DateField(null=True, blank=True, verbose_name='date published')
    publisher = models.CharField(max_length=255, blank=True)
    series = models.CharField(max_length=255, blank=True)
    price = models.IntegerField(null=True, blank=True)
    evaluation = models.IntegerField(null=True, blank=True, validators=[validators.MinValueValidator(0), validators.MaxValueValidator(100)])
    memo = models.TextField(blank=True)
    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["title", "publisher", "series"],
                name="book_unique"
            ),
        ]
    def __str__(self):
        return self.title
```

- この方法では、例えば以下のようにすることで、Adminサイトの`Author`側でも`Book`をインライン編集することができます。

```py/exlibris/admin.py
from django.contrib import admin
from .models import Author, Book

class BookInline(admin.TabularInline):
    model = Book.authors.through
    extra = 0

class AuthorAdmin(admin.ModelAdmin):
    inlines = [BookInline]

admin.site.register(Author, AuthorAdmin)
admin.site.register(Book)
```

- この`Book.authors.through`については、公式ドキュメントの「[ManyToManyField.through](https://docs.djangoproject.com/ja/3.1/ref/models/fields/#django.db.models.ManyToManyField.through)」の項の最後に記載があります。

## ManyToManyField を使って、両側から設定する
- 著者と書籍の両方のモデルで`ManyToManyField`を使います。
    - 先に定義するモデル(`Author`)で中間テーブルを生成して、後から定義するモデル(`Book`)では`through=Author.books.through`とすることで、`Author`が生成したモデルに接続することが可能です。
    - 文字列でモデルを指定(`through='author_books'`)して、遅延評価する(後から定義する`Book`側で中間テーブルを生成する)ことも可能です。

```py:exlibris/models.py
from django.core import validators
from django.db import models

class Author(models.Model):
    """著者"""
    name = models.CharField(max_length=255)
    add_name = models.CharField(max_length=255, blank=True, verbose_name='additional name')
    memo = models.TextField(blank=True)
    books = models.ManyToManyField('Book', blank=True)
    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["name", "add_name"],
                name="author_unique"
            ),
        ]
    def __str__(self):
        return self.name

class Book(models.Model):
    """書籍"""
    title = models.CharField(max_length=255)
    authors = models.ManyToManyField(Author, through=Author.books.through)
    pub_date = models.DateField(null=True, blank=True, verbose_name='date published')
    publisher = models.CharField(max_length=255, blank=True)
    series = models.CharField(max_length=255, blank=True)
    price = models.IntegerField(null=True, blank=True)
    evaluation = models.IntegerField(null=True, blank=True, validators=[validators.MinValueValidator(0), validators.MaxValueValidator(100)])
    memo = models.TextField(blank=True)
    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["title", "publisher", "series"],
                name="book_unique"
            ),
        ]
    def __str__(self):
        return self.title
```

- モデル`Book`で、逆側で定義された中間テーブル(`'author_books'`)に`through`で接続すると、なぜだか、使われないデフォルト名のテーブル(`'book_authors'`)も生成されてしまいます。
    - 前述した「[自前で中間テーブルを作った上で ManyToManyField も使う](#自前で中間テーブルを作った上で%20ManyToManyField%20も使う)」方法だと、同じような`through`設定でもデフォルト名のテーブルは作られたりしないのに、この辺りの法則はよく分かっていません。
- ともかく、この方法だと、次のようにするだけで、Adminサイトの`Author`、`Book`どちら側でも、ナチュラルにインラインで関係を編集できます。
    - ここまで挙げた中では、この方法が良さそうです。

```py:exlibris/admin.py
from django.contrib import admin
from .models import Author, Book

admin.site.register(Author)
admin.site.register(Book)
```

# 書籍リストを著者名でソートする
## 取り敢えず動いたコード
- 書籍の一覧を著者名でソートしたかったのですが、`GROUP By`の制御が思うようにならず、面倒になって、生のSQLを書きました。
- 以下のコードで、やりたいことは実現できています。
    - `sortkey`が`'-?author'`のとき、つまり、URL `exlibris/book/author/`と`~/-author/`に対する挙動が争点になります。

### モデル
```py:exlibris/models.py
from django.core import validators
from django.db import models

class Author(models.Model):
    """著者"""
    name = models.CharField(max_length=255)
    add_name = models.CharField(max_length=255, blank=True, verbose_name='additional name')
    memo = models.TextField(blank=True)
    books = models.ManyToManyField('Book', blank=True)
    @property
    def book_names(self):
        """著書一覧"""
        return ','.join([a.title for a in self.books.all()])
    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields = [name, add_name,],
                name = 'author_unique'
            ),
        ]
    def __str__(self):
        return self.name

class Book(models.Model):
    """書籍"""
    title = models.CharField(max_length=255)
    authors = models.ManyToManyField(Author, through=Author.books.through)
    pub_date = models.DateField(null=True, blank=True, verbose_name='date published')
    publisher = models.CharField(max_length=255, blank=True)
    series = models.CharField(max_length=255, blank=True)
    price = models.IntegerField(null=True, blank=True)
    evaluation = models.IntegerField(null=True, blank=True, validators=[validators.MinValueValidator(0), validators.MaxValueValidator(100)])
    memo = models.TextField(blank=True)
    @property
    def author_names(self):
        """執筆者一覧"""
        return ','.join([a.name for a in self.authors.all()])
    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields = [title, publisher, series,],
                name = 'book_unique'
            ),
        ]
    def __str__(self):
        return self.title
```

### URLディスパッチャ
```py:exlibris/urls.py
from django.urls import path
from . import views

app_name = 'exlibris'
urlpatterns = [
    path('', views.BookListView.as_view(), name='index'),
    path('book/', views.BookListView.as_view(), name='books'),
    path('book/<str:sortkey>/', views.BookListView.as_view(), name='sortedbooks'),
    # 省略
]
```

### ビュー
```py:exlibris/views.py
from django.views import generic
from .models import Author, Book

class BookListView(generic.ListView):
    def get_queryset(self):
        objects = Book.objects
        sortkey = self.kwargs.get('sortkey')
        sign, key = ('-', sortkey[1:]) if sortkey and sortkey[0] == '-' else ('', sortkey)
        if key == 'author':
            queryset = objects.raw(f"""
SELECT "exlibris_book"."id", "exlibris_book"."title", "exlibris_book"."pub_date", "exlibris_book"."publisher", "exlibris_book"."series", "exlibris_book"."price", "exlibris_book"."evaluation", "exlibris_book"."memo", group_concat("exlibris_author"."name") as "names"
  FROM "exlibris_book"
  LEFT OUTER JOIN "exlibris_author_book" ON ("exlibris_book"."id" = "exlibris_author_book"."book_id")
  LEFT OUTER JOIN "exlibris_author" ON ("exlibris_author_book"."author_id" = "exlibris_author"."id")
  GROUP BY "exlibris_book"."id"
  ORDER BY "names" {('DESC' if sign else 'ASC')};""")
        else:
            queryset = objects.order_by(sortkey if key in [field.name for field in Book._meta.get_fields()] else '-pub_date')
        #print(queryset.query)
        return queryset
# 以降省略
```

- URLから抽出したソートキーをフィールド名と照合して、一致するものがなければ、デフォルトキーに差し替えます。
    - ソートキー`'author'`は別扱いで、生SQLを使います。


## 試行錯誤
### ソートキー
- 最初は、単純に、ソートキーに`authors__name`と指定してみました。

```py:exlibris/views.py
queryset = objects.order_by(sign+'authors__name')
```

```sql:queryset
SELECT "exlibris_book"."id", "exlibris_book"."title", "exlibris_book"."pub_date", "exlibris_book"."publisher", "exlibris_book"."series", "exlibris_book"."price", "exlibris_book"."evaluation", "exlibris_book"."memo"
  FROM "exlibris_book"
  LEFT OUTER JOIN "exlibris_author_books" ON ("exlibris_book"."id" = "exlibris_author_books"."book_id")
  LEFT OUTER JOIN "exlibris_author" ON ("exlibris_author_books"."author_id" = "exlibris_author"."id")
  ORDER BY "exlibris_author"."name" ASC
```

- この方法だと、まず、書籍×著者のリストが生成されますが、Djangoは、本来の行数(書籍のレコード数)で出力を制限するようです。
    - 例えば、書籍が5冊あり、内2冊が2人の共著で、残り3冊のは著者1人だとすると、(2×2+3=)7行が生成されますが、末尾の2行は出力されません。
- `ORDER BY`の前に`GROUP BY "exlibris_book"."id"`と挿入できれば、少なくとも行を失うことはないのですが…。

### distinct(*fields)
- SQLite3では、行全体の重複をまとめられるだけで、フィールドの指定が使えないらしく、`NotSupportedError`になります。
    - `DISTINCT ON fields is not supported by this database backend`

### annotate
- 使えるかなと思ったのですが、サポートされている集計関数が`Count`、`Min`、`Max`、`Avg`だけで、`group_concat`に相当するものがないようです。
- 以下のように、著者の人数でソートすることはできますが…。

```py:exlibris/views.py
from django.db.models import Count
queryset = objects.annotate(Count('author')).order_by('author__count')
```

```sql:queryset
SELECT "exlibris_book"."id", "exlibris_book"."title", "exlibris_book"."pub_date", "exlibris_book"."publisher", "exlibris_book"."series", "exlibris_book"."price", "exlibris_book"."evaluation", "exlibris_book"."memo", COUNT("exlibris_author_books"."author_id") AS "author__count"
  FROM "exlibris_book"
  LEFT OUTER JOIN "exlibris_author_books" ON ("exlibris_book"."id" = "exlibris_author_books"."book_id")
  GROUP BY "exlibris_book"."id", "exlibris_book"."title", "exlibris_book"."pub_date", "exlibris_book"."publisher", "exlibris_book"."series", "exlibris_book"."price", "exlibris_book"."evaluation", "exlibris_book"."memo"
  ORDER BY "author__count" ASC
```

- なるほど、`annotate`を使うと`GROUP BY`を使ってくれるのですね。

## 独自に Aggregate 関数を作る
- 無ければ作ってしまえ…、というか、必要に応じて作る仕組みになっていました。
- キー`author`に対して、`group_concat`で集めた著者名の連結文字列をカラムに加え、さらにソートキーに使います。

```py:exlibris/views.py
from django.views import generic
from .models import Author, Book
from django.db.models import Aggregate

class GroupConcat(Aggregate):
    function = 'GROUP_CONCAT'

class BookListView(generic.ListView):
    def get_queryset(self):
        objects = Book.objects
        sortkey = self.kwargs.get('sortkey')
        sign, key = ('-', sortkey[1:]) if sortkey and sortkey[0] == '-' else ('', sortkey)
        if key == 'author':
            queryset = objects.annotate(names=GroupConcat('authors__name')).order_by(sign+'names')
        else:
            queryset = objects.order_by(sortkey if key in [field.name for field in Book._meta.get_fields()] else '-pub_date')
        #print(queryset.query)
        return queryset
# 以降省略
```

```sql:queryset
SELECT "exlibris_book"."id", "exlibris_book"."title", "exlibris_book"."pub_date", "exlibris_book"."publisher", "exlibris_book"."series", "exlibris_book"."price", "exlibris_book"."evaluation", "exlibris_book"."memo", GROUP_CONCAT("exlibris_author"."name") AS "names"
  FROM "exlibris_book"
  LEFT OUTER JOIN "exlibris_author_books" ON ("exlibris_book"."id" = "exlibris_author_books"."book_id")
  LEFT OUTER JOIN "exlibris_author" ON ("exlibris_author_books"."author_id" = "exlibris_author"."id")
  GROUP BY "exlibris_book"."id", "exlibris_book"."title", "exlibris_book"."pub_date", "exlibris_book"."publisher", "exlibris_book"."series", "exlibris_book"."price", "exlibris_book"."evaluation", "exlibris_book"."memo"
  ORDER BY "names" ASC
```

- この方法は良さそうです。

#### 独自にFuncで関数を作る
- 実は、`Aggregate`の前に`Func`を試したのですが、こちらは、通常のDB関数向けで、集計関数に使うのは間違っています。
    - `GROUP BY`は生成されないし、無理に作ろうとすると集計したカラムまでそこに押し込んでくるので、大変なことになります。
- 前述のように`Aggregate`で作ると、ちゃんと解っているらしくて、`GROUP BY`を挟みつつ、無茶はしてこないようです。

## DBでViewを作る
- DBで`CREATE VIEW`すれば、先のモデルでは`@property`を付けてPython側で実装していた`authors`フィールドを、DBのカラムとして実装可能です。
    - そうすれば、自テーブルのカラムとして、単純にソートキーに使えます。

### DBの準備
- まず、アプリに、空のマイグレーションファイルを生成します。

```text:terminal
...>py manage.py makemigrations exlibris --empty
Migrations for 'exlibris':
  exlibris\migrations\0019_auto_20210404_1824.py
```

- すると、テンプレ状態のマイグレーションファイルができるので、以下のような感じで書き加えます。
- `exlibris_bookview`というビューを作ります。
    - 既存のテーブルと同名のビューを作ると、Djangoが混乱するので、そういうチャレンジはやらぬが吉です。
    - ロールバックできるように、ビューを`DROP`するSQLも書きます。

```py:exlibris/migrations/0019_auto_20210404_1824.py
# Generated by Django 3.1.7 on 2021-04-04 09:24

from django.db import migrations

class Migration(migrations.Migration):

    dependencies = [
        ('exlibris', '0018_auto_20210404_0325'),
    ]

    create_sql = """
CREATE VIEW IF NOT EXISTS "exlibris_bookview" ("id", "title", "author", "pub_date", "publisher", "series", "price", "evaluation", "memo")
  AS SELECT "exlibris_book"."id", "exlibris_book"."title", group_concat("exlibris_author"."name") as "author", "exlibris_book"."pub_date", "exlibris_book"."publisher", "exlibris_book"."series", "exlibris_book"."price", "exlibris_book"."evaluation", "exlibris_book"."memo"
  FROM "exlibris_book"
  LEFT OUTER JOIN "exlibris_author_book" ON ("exlibris_book"."id" = "exlibris_author_book"."book_id")
  LEFT OUTER JOIN "exlibris_author" ON ("exlibris_author_book"."author_id" = "exlibris_author"."id")
  GROUP BY "exlibris_book"."id";
"""

    drop_sql = """DROP VIEW IF EXISTS "exlibris_bookview";"""

    operations = [
        migrations.RunSQL(create_sql, drop_sql),
    ]
```

- マイグレートを実施します。

```text:terminal
...>py manage.py migrate
```

### モデルの生成
- モデルに以下のクラスを追加して、`Meta`クラスで管理対象から外し(`managed = False`)ます。
    - このクラスは、DB側の制約で、読み出しにしか使えません。

```py:exlibris/models.py
class Bookview(models.Model):
    """書籍 (読み出し専用)"""
    id = models.AutoField(primary_key=True, editable=False)
    title = models.CharField(max_length=255, editable=False)
    author = models.TextField(editable=False)
    pub_date = models.DateField(null=True, blank=True, verbose_name='date published', editable=False)
    publisher = models.CharField(max_length=255, blank=True, editable=False)
    series = models.CharField(max_length=255, blank=True, editable=False)
    price = models.IntegerField(null=True, blank=True, editable=False)
    evaluation = models.IntegerField(null=True, blank=True, validators=[validators.MinValueValidator(0), validators.MaxValueValidator(100)], editable=False)
    memo = models.TextField(blank=True, editable=False)
    class Meta:
        managed = False
    def __str__(self):
        return self.title
```

### ビューの生成
- あとは、ビューを以下のようにしてやれば、モデル`Book`用のテンプレ(`book_list.html`)が使い回せるはずです。

```py:exlibris/views.py
from django.views import generic
from .models import Author, Book, Bookview

class BookListView(generic.ListView):
    template_name = 'exlibris/book_list.html' # テンプレ名を上書き
    def get_queryset(self):
        objects = Bookview.objects
        sortkey = self.kwargs.get('sortkey')
        sign, key = ('-', sortkey[1:]) if sortkey and sortkey[0] == '-' else ('', sortkey)
        queryset = objects.order_by(sortkey if key in [field.name for field in Book._meta.get_fields()] else '-pub_date')
        #print(queryset.query)
        return queryset
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['book_list'] = context.pop('bookview_list', None) # コンテキスト変数のキー名を変更
        return context
```

### 結果
- `sortkey = 'author'`に対して、以下が得られます。

```sql:queryset
SELECT "exlibris_bookview"."id", "exlibris_bookview"."title", "exlibris_bookview"."author", "exlibris_bookview"."pub_date", "exlibris_bookview"."publisher", "exlibris_bookview"."series", "exlibris_bookview"."price", "exlibris_bookview"."evaluation", "exlibris_bookview"."memo"
  FROM "exlibris_bookview"
  ORDER BY "exlibris_bookview"."author" ASC
 ```

- このやり方は、きちんと動作しますし、ビューがシンプルで分かり易いのですが、モデルが読み書きで二系統になるのでややこしいですね。
- この例のようなシンプルな題材ではなく、より複雑なモデルを作りたい場合には選択肢になるかも知れません。
    - とはいっても、普段使いするビューに書くか、普段は見えないマイグレーションに書くか、という違いだけで、生のSQLを書いていることに変わりないのですが。

# おわりに
- 執筆者は、Python、Djangoともに初学者レベルですので、誤りもあるかと思います。
    - お気づきの際は、是非コメントや編集リクエストにてご指摘ください。
    - あるいは、「それでも解らない」、「自分はこう捉えている」などといった、ご意見、ご感想も歓迎いたします。

#### 大雑把なまとめ
- 多対多の関係を作るときは、一方のモデルで暗に中間テーブルを生成させ、他方からも同じテーブルに接続して使うのが便利
- 多対多の関係先のフィールドでソートする際に、DBがサポートする集計関数がDjango側に用意されていなければ、自分で定義して使うのが便利
- 直にSQLを書けば何でもできる