Django
MySQL
Django2.0

いつも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 らへんに載っている手順通りに設定すれと問題になることはないと思いますが、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 をインストールします。pypiにあるのでpipでパツイチですね。

$ pip install mysqlclient

3. 接続設定を書く

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

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

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

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を使え! | おそらくはそれさえも平凡な日々

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

4. MySQLの設定

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

[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
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." いわれるときの対策 - かみぽわーる

my.cnfに設定を追加

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

[mysqld]
...

innodb_file_format = Barracuda
innodb_file_per_table = 1
innodb_large_prefix

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

CREATE TABLE文に " ROW_FORMAT=DYNAMIC" をつけるようにする

manage.pyに以下の2行を追加します。

#!/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ファイルでもエラーにならずに通るようになりました。