Python3一覧

Django で MySQL を使った絵文字対応を実装する

Django で MySQL を使った絵文字対応を実装します。

MySQL の utf8 には少し問題があり、スマホなどの絵文字を保存することができません。

絵文字を保存するためには、文字コードを utf8 ではなく utf8mb4 にする必要があります。

絵文字に対応するために utf8mb4 にすると、今度はインデックス 767 バイトという問題が出てくる可能性があります。

今回は、この辺のお話をしていきたいと思います。

実装イメージ

バージョン

  • CentOS 7.4.1708
  • Python 3.6.4
  • Django 2.0.3
  • mysqlclient 1.3.12
  • MySQL 5.7

インストール

まず、環境を作っていきます。

Python

$ sudo yum install -y https://centos7.iuscommunity.org/ius-release.rpm
$ yum install -y python36u python36u-libs python36u-devel python36u-pip

Python をインストールします。私の環境は、CentOS 7 ですので、Python 2.7 が入っていますが、Python の 3 系を使いたいので、別途インストールします。

CentOS 7 で Python3 を使う方法として、SCL がありますが、コマンドやパスが複雑になるので、私は、IUS リポジトリの Python を使います。

Django

$ sudo pip install django

Django をインストールします。

MySQL

MySQL は、5.7 を利用します。インストール方法を Qiita のこちら を参照してください。

Python を私と同じように IUS リポジトリでインストールした場合、MySQL リポジトリと衝突します。

先に ius リポジトリを disable にしてください。

# yum-config-manager --disable ius
# yum install mysql mysql-devel mysql-server mysql-utilities

mysqlclient

$ sudo pip install mysqlclient

Python MySQL クライアント mysqlclient をインストールします。

問題点

MySQL utf8 の問題

mysql> show character set like 'utf8%';
+---------+---------------+--------------------+--------+
| Charset | Description | Default collation | Maxlen |
+---------+---------------+--------------------+--------+
| utf8 | UTF-8 Unicode | utf8_general_ci | 3 |
| utf8mb4 | UTF-8 Unicode | utf8mb4_general_ci | 4 |
+---------+---------------+--------------------+--------+
2 rows in set (0.00 sec)

mysql>

冒頭でも言いましたが、MySQL の UTF-8 は少し問題があります。

MySQL で show character set を実行して、Maxlen を確認すると、utf8 は 3 となります。これは、3 バイトという意味です。

しかし、スマホの絵文字は 4 バイト必要になります。文字コードが utf8 では、絵文字を保存することができません。

これが問題です。

なので、絵文字を保存するためには、utf8 の 4 バイト文字コード版の utf8mb4 を使う必要があります。

こちらの記事が非常に丁寧でわかりやすいです。

MySQL InnoDB インデックス 767 バイトの問題

uft8mb4 でめでたし、めでたしと思っていると、次は MySQL InnoDB インデックス長の問題が発生することがあります。

MySQL の InnoDB は、デフォルトの最大インデックス長が 767 バイトになっています。文字コードが utf8 の場合、1文字 3 バイトなので、255 文字までインデックスをつけることができますが、utf8mb4 は 1文字 4 バイトなので、191 文字に減ってしまいます。

innodb_large_prefix オプション

この問題を回避するために、innodb_large_prefix オプションというものがあります。これを利用すると、最大インデックス長が 767 バイトから 3072 バイトまで拡張されます。

公式のドキュメントで確認できませんでしたが、MySQL 5.7 からデフォルトが 3072 バイトになっているらしく、実際に私の環境でも、デフォルトの状態で varchar(255) にインデックスをつけることができました。


mysql> alter table tbl1 add index (col1);
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> show create table tbl1;
+-------+-------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+-------+-------------------------------------------------------------------------------------------------------------------------+
| tbl1 | CREATE TABLE `tbl1` (
`col1` varchar(255) DEFAULT NULL,
KEY `col1` (`col1`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 |
+-------+-------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql>

実装

それでは、実装の方に入っていきたいと思います。まずは、MySQL の設定からです。

MySQL

my.cnf

[mysqld]
innodb_large_prefix
innodb_file_per_table
innodb_file_format=barracuda

上記でも説明しましたが、MySQL 5.6 以下の InnoDB で utf8mb4 文字コードを使用しようとする場合、192 文字以上にインデックスをつけることができません。

DB サーバが本稼働する前に、innodb_large_prefix を上記のように有効にして、この問題を回避しておくことをお勧めします。

settings.py

ALLOWED_HOSTS = ['*']
INSTALLED_APPS = [
    ...
    'app.apps.AppConfig'
]
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'emoji',
        'USER': 'root',
        'PASSWORD': 'Your Password',
        'OPTIONS': {
            'charset': 'utf8mb4',
        }
    }
}

settings.py です。Django から MySQL の utf8mb4 を扱うために、DATABASES の OPTIONS で、’charset’: ‘utf8mb4’ を定義します。

models.py

from django.db import models

class Emoji(models.Model):
    emoji = models.CharField(max_length=255)

    class Meta:
        indexes = [
            models.Index(fields=['emoji']),
        ]

models.py です。Django で DB マイグレーションする際、MySQL の文字コードとインデックス長について意識しましょう。

views.py

from django.shortcuts import render
from .models import Emoji

def index(request):
    emoji = Emoji.objects.all()[0].emoji
    return render(request, 'app/index.html', {'emoji': emoji})

絵文字の表示を確認するためのサンプル views.py です。酷い実装ですみません。。。

index.html

<html>
  <head>
    <title>Django で MySQL を使った絵文字対応を実装する</title>
  </head>
  <body>
    <h1>Django で MySQL を使った絵文字対応を実装する</h1>
    <p>{{ emoji }}</p>
  </body>
</html>

urls.py

from django.urls import path
from app.views import index
urlpatterns = [
    path('', index),
]

動作確認

DB マイグレーション

$ mysql -u root -p -e 'create database emoji default charset utf8mb4'
Enter password:
$ python3.6 manage.py makemigrations
Migrations for 'app':
  app/migrations/0001_initial.py
    - Create model Emoji
    - Create index app_emoji_emoji_19f508_idx on field(s) emoji of model emoji
$ python3.6 manage.py migrate
Operations to perform:
  Apply all migrations: admin, app, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying app.0001_initial... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying sessions.0001_initial... OK

動作確認するために、DB の準備をします。

絵文字インサート

フォームを用意して、スマホから絵文字を POST するのが少し大変なので、Mac から絵文字をインサートしました。

上記のコマンドを実行すると、最初は、以下のエラーが発生しました。

ERROR 1366 (HY000) at line 1: Incorrect string value: '\xF0\x9F\x98\x84' for column 'emoji' at row 1

なので、my.cnf に以下を追加し、mysql クライアントの文字コードを utf8mb4 に設定しています。

[mysql]
default-character-set=utf8mb4

runserver

$ python3.6 manage.py runserver 0.0.0.0:8000

Django デバッグサーバの runserver を実行し、ブラウザでアクセスすると、インサートした絵文字を確認することができると思います。

まとめ

Django で MySQL を使った絵文字対応を実装しました。

この MySQL utf8 問題ですが、数年前に、あるスマホ向けサービスを運用していて気がつきました。

この問題に気づいた時は、テーブル数が数十、レコード数がそれぞれ数万レコードあり、対応するためには、大規模なメンテナンスが必要になるため、導入を見送った経験をしました。

最近では、この MySQL uff8 問題の記事やベストプラクティスがたくさん増えてきた気がします。

皆さんも、MySQL を使う場合は、文字コード utf8 には十分に気をつけましょう!