django/MySQL で Unicode 6.0 絵文字を使う

# 目的
[django](https://www.djangoproject.com/) から [Unicode 6.0 で追加された絵文字](http://ja.wikipedia.org/wiki/Unicode6.0%E3%81%AE%E6%90%BA%E5%B8%AF%E9%9B%BB%E8%A9%B1%E3%81%AE%E7%B5%B5%E6%96%87%E5%AD%97%E3%81%AE%E4%B8%80%E8%A6%A7) を使いたい。

# 動作環境
* Python 3.2.0 以上 (3.4.3 で動作確認)
* Django 1.4.0 以上？ (1.7.0, 1.8.1 で動作確認)
* [mysqlclient](https://pypi.python.org/pypi/mysqlclient/) (1.3.3, 1.3.6 で動作確認)
* MySQL 5.5.14 以上 (5.5.40, 5.6.16 で動作確認)


## Python

Unicode 6.0 絵文字を使うためには、Python 3.2.0 以上が必要になる。

## django

django の対応バージョンについては、調べ切れていない。
1.7.0, 1.8.1 で動作したから良しとする。

## mysqlclient

Python 3.x 系列に対応した [mysqlclient](https://pypi.python.org/pypi/mysqlclient/) を使用する。

## MySQL

キープレフィックスの制限(後述)を3072バイトまで拡張できる MySQL 5.5.14 以上 を使用する。


# 設定

## MySQL

MySQL に絵文字を格納するためには、データベース作成時の文字コードを utf8mb4 にする。

```sql
CREATE DATABASE cmtestdb CHARACTER SET utf8mb4;
```

MySQL(InnoDB) では、ひとつのカラムのキープレフィックスの最大値が 767 バイトという制限がある。
utf8mb4 を指定した場合、 767byte÷4で191文字までしか使えない。
django のミドルウェアが生成するテーブルに191文字より大きいカラムが含まれているため下記のエラーが発生する。

```
Specified key was too long; max key length is 767 bytes
```

このエラーを回避するためには、my.cnf に下記の設定をする。

```text:my.cnf
innodb_file_format = Barracuda
innodb_file_per_table = 1
innodb_large_prefix
```

innodb_large_prefix を指定することで、キープレフィックスの最大値を 3072 バイトまで拡張できる。


## mysqlclient

普通にインストールする。

```sh
pip install mysqlclient
```

## django

### データベース定義
django から MySQL に接続する際、 utf8mb4 の文字コードを指定する。

```python3:settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'DBNAME',
        'USER': 'DBUSER',
        'PASSWORD': 'DBPASSWORD',
        'HOST': 'DBHOST',
        'PORT': '3306',
        'OPTIONS': {
            'charset': 'utf8mb4',
        },
    },
}
```

### "CREATE TABLE" 文の拡張

innodb_large_prefix の指定を有効にするためには、"CREATE TABLE" SQL文で ```ROW_FORMAT=DYNAMIC``` もしくは ```ROW_FORMAT=COMPRESSED``` を指定する。
```python manage.py migrade``` を実行すると "CREATE TABLE" SQL文を発行する。
django 1.7 の実装では、"CREATE TABLE" SQL文の拡張ポイントが見つからなかった。
そこで、django 1.7 オリジナルの "CREATE TABLE" SQL文 生成部分に動的にパッチを当てる形で ```ROW_FORMAT=DYNAMIC``` を追加する。

```python3:manage.py
import os
import sys

def patch_mysql_sql_create_model(original):
    """
    :param :class:`django.db.backends.creation.BaseDatabaseCreation` original: BaseDatabaseCreation
    :return: BaseDatabaseCreation
    :rtype: :class:`django.db.backends.creation.BaseDatabaseCreation`
    """

    def revised(self, model, style, known_models=set()):
        """
        :class:`django.db.backends.creation.BaseDatabaseCreation` #sql_create_model の処理に対して下記の処理を追加する。
        * SQL 文の末尾に 'ROW_FORMAT=DYNAMIC;' を追加する。

        追加条件は、下記になる。
        * データベースが MySQL
        * SQL 文の先頭が、'CREATE TABLE'
        """
        fullname = self.__module__ + "." + self.__class__.__name__
        if fullname == 'django.db.backends.mysql.creation.DatabaseCreation':
            original_output, pending_references = original(self, model, style, known_models)

            final_output = []
            for sql in original_output:
                if sql.startswith('CREATE TABLE') is False:
                    continue

                if sql.endswith(';'):
                    sql = sql[:-1]

                sql += 'ROW_FORMAT=DYNAMIC'
                final_output.append(sql)

            return final_output, pending_references
        else:
            return original(self, model, style, known_models)

    return revised

if __name__ == "__main__":
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.local")

    if len(sys.argv) > 1 and sys.argv[1] == 'migrate':
        import django

        if django.VERSION >= (1, 8):
            from django.db.backends.base.creation import BaseDatabaseCreation
        else:
            from django.db.backends.creation import BaseDatabaseCreation
        BaseDatabaseCreation.sql_create_model = patch_mysql_sql_create_model(BaseDatabaseCreation.sql_create_model)

        from django.db.backends.mysql.schema import DatabaseSchemaEditor
        DatabaseSchemaEditor.sql_create_table += ' ROW_FORMAT=DYNAMIC'

    from django.core.management import execute_from_command_line

    execute_from_command_line(sys.argv)
```

上記の設定を行うと、 ```python manage.py migrate``` を実行したときに "CREATE TABLE" SQL文 の末尾に ```ROW_FORMAT=DYNAMIC``` が追加される。


# 参照資料

* [Use utf8mb4 encoding with MySQL 5.5](https://code.djangoproject.com/ticket/18392)
* [MySQL(InnoDB) で "Index column size too large. The maximum column size is 767 bytes." いわれるときの対策](http://blog.kamipo.net/entry/2012/11/13/102024)