いつもDjangoでMySQL(utf8mb4)を利用するときに行っているDjangoのDATABASE設定

Django と MySQL(CHARACTER SET: utf8mb4) を合わせて使う場合、

- `(1071, 'Specified key was too long; max key length is 767 bytes')`
- `(1709, 'Index column size too large. The maximum column size is 767 bytes.')`

などのエラーが出てしまうことがあります。
utf8の場合は [Databases | Django documentation | Django](https://docs.djangoproject.com/ja/1.11/ref/databases/#mysql-notes) らへんに載っている手順通りに設定すれと問題になることはないと思いますが、utf8mb4を使用するとなると、上記のエラーを回避するために特別な手順が必要になるので、初期セットアップの手順から備忘録的にまとめます。
（初期セットアップとかいいから上記の `...767 bytes` のエラーを回避する方法だけ知りたい方は、5の章の内容だけ見ていただけますと。）

今回記事を書くにあたって検証した環境は以下の通りです。

- Python == 3.6.3
- Django == 2.0.2
- mysqlclient == 1.3.12
- MySQL == 5.6.39


## 1. Django projectを作成する
まずはじめにprojectを開始します。（Djangoはprojectがあり、その中に複数のアプリケーション(app)があるという構成を取ります。）

```
$ pip install django
...

$ django-admin startproject sampleproj  # => これで sampleproj というprojectが作成される
```

startprojectを行った後は以下のようなディレクトリ構成で初期ファイルが作成されます。

```
$ tree .
.
└── sampleproj
    ├── manage.py
    └── sampleproj
        ├── __init__.py
        ├── __pycache__
        │   ├── __init__.cpython-36.pyc
        │   └── settings.cpython-36.pyc
        ├── settings.py
        ├── urls.py
        └── wsgi.py

3 directories, 7 files
```


## 2. mysqlclient をインストール

https://docs.djangoproject.com/ja/2.0/ref/databases/#mysql-db-api-drivers

にもある通り、[mysqlclient](https://pypi.python.org/pypi/mysqlclient) をインストールします。pypiにあるのでpipでパツイチですね。

```
$ pip install mysqlclient
```


## 3. 接続設定を書く
`sampleproj/sampleproj/settings.py` からデータベースの接続設定を行います。ファイルをエディタで開くと

```python
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}
```

というdictが書かれている部分があるので、これを以下のように書き換えます。（あくまでも一例なので、利用環境に合わせて適時書き換えてください。）

```python
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'sampleproj',
        'USER': 'root',
        'PASSWORD': '',
        'HOST': 'localhost',
        'PORT': '3306',
        'OPTIONS': {
            'charset': 'utf8mb4',
            'sql_mode': 'TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY',
        },
    },
}
```

| フィールド       | 説明                                                                          |
|------------------|-------------------------------------------------------------------------------|
| ENGINE           | 利用するデータベース設定。MySQLの場合は "django.db.backends.mysql" を設定する |
| NAME             | 接続するdatabase名。利用環境に合わせて設定する                                |
| USER             | 接続ユーザ。利用環境に合わせて設定する                                        |
| PASSWORD         | 接続ユーザに設定されているパスワード。利用環境に合わせて設定する              |
| HOST             | データベースの接続先(ホスト)。利用環境に合わせて設定する                      |
| PORT             | データベースの接続先(ポート)。利用環境に合わせて設定する                      |
| OPTIONS          | データベース接続時に発行されるパラメータを設定するdict。                      |
| OPTIONS.charset  | ここが重要！接続時に charset=utf8mb4 を発行する                               |
| OPTIONS.sql_mode | SQL modeの設定。https://dev.mysql.com/doc/refman/5.6/en/sql-mode.html         |

※ 本筋とは関係ないですが、`sql_mode`の記述は以下の記事をチェックです。
[ルーク！MySQLではkamipo TRADITIONALを使え！ | おそらくはそれさえも平凡な日々](http://www.songmu.jp/riji/entry/2015-07-08-kamipo-traditional.html)

これでベーシックな設定ができました。


## 4. MySQLの設定
my.cnfを以下のように設定し、utf8mb4の利用を有効にします。（※my.cnfを書き換えたらMySQLの再起動は忘れずに）


```toml
[mysql]
default-character-set=utf8mb4

[mysqld]
character-set-server = utf8mb4
skip-character-set-client-handshake
collation-server = utf8mb4_general_ci
init-connect = SET NAMES utf8mb4
```

my.cnfを設定したらmysql cliからアプリケーション用のdatabaseを作成します。

```
mysql> CREATE DATABASE <NAMEに設定したdb名> CHARACTER SET utf8mb4;
```

確認します。

```
mysql> use <NAMEに設定したdb名>;
Database changed

mysql> show variables like '%char%';
+--------------------------+----------------------------------------------------------+
| Variable_name            | Value                                                    |
+--------------------------+----------------------------------------------------------+
| character_set_client     | utf8mb4                                                  |
| character_set_connection | utf8mb4                                                  |
| character_set_database   | utf8mb4                                                  |
| character_set_filesystem | binary                                                   |
| character_set_results    | utf8mb4                                                  |
| character_set_server     | utf8mb4                                                  |
| character_set_system     | utf8                                                     |
| character_sets_dir       | /usr/local/Cellar/mysql@5.6/5.6.39/share/mysql/charsets/ |
+--------------------------+----------------------------------------------------------+
```

OKですね。


## 5. (1071, 'Specified key was too long; max key length is 767 bytes') を回避する
上記の手順までで例えば以下のModelを作成し、migrationを実行してみます。

- `{sampleapp}/models.py`

```python
from django.db import models


class User(models.Model):

    name = models.CharField(max_length=255, null=False, db_index=True)
```

```
$ cd sampleproj

$ python manage.py makemigrations
Migrations for '{sampleapp}':
  {sampleapp}/migrations/0001_initial.py
    - Create model User

$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, app, auth, contenttypes, sessions
Running migrations:
  Applying app.0001_initial...Traceback (most recent call last):

...

    _mysql.connection.query(self, query)
django.db.utils.OperationalError: (1709, 'Index column size too large. The maximum column size is 767 bytes.')
```

`(1709, 'Index column size too large. The maximum column size is 767 bytes.')`というエラーが出るとともにmigrationが失敗していまいました。

このエラーの回避策はDjangoの公式ではサポートされていませんが、以下の記事を参考に回避する設定を加えることにします。
[MySQL(InnoDB) で "Index column size too large. The maximum column size is 767 bytes." いわれるときの対策 - かみぽわーる](http://blog.kamipo.net/entry/2012/11/13/102024)

### my.cnfに設定を追加
`my.cnf`に以下の3行を追加します。
`innodb_large_prefix`を有効にすることでキープレフィックスの制限を3072バイトまで拡張することができるとのこと。

```toml
[mysqld]
...

innodb_file_format = Barracuda
innodb_file_per_table = 1
innodb_large_prefix
```

ただし、これだけでは`innodb_large_prefix`は有効にはならず、`CREATE TABLE`文に`ROW_FORMAT`に`DYNAMIC`か`COMPRESSED`のどちらかを付けてtableを作成する必要がある。


### CREATE TABLE文に " ROW_FORMAT=DYNAMIC" をつけるようにする
`manage.py`に以下の2行を追加します。

```python
#!/usr/bin/env python
import os
import sys

if __name__ == "__main__":
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sampleproj.settings")

    # この以下の2行を追加する!
    from django.db.backends.mysql.schema import DatabaseSchemaEditor
    DatabaseSchemaEditor.sql_create_table += " ROW_FORMAT=DYNAMIC"

    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)
```

この操作はmigrationで新規テーブルを追加する時に発行する`CREATE TABLE`文の末尾に ` ROW_FORMAT=DYNAMIC` というステートメントを追加するというものです。

再度、migrationを実行してみます。

```
$ python manage.py migrate
Operations to perform:
  Apply all migrations: app
Running migrations:
  Applying app.0001_initial... OK
```

これで同じmigrationファイルでもエラーにならずに通るようになりました。