Rails
MySQL
ActiveRecord
emoji

Rails 3.2でiOS5の絵文字を扱う

More than 3 years have passed since last update.

iOS5の絵文字は4バイトのUTF-8で記述されているので、それに対応したPostgreSQLやMySQL 5.5.3以降が必要。

ここではMySQL 5.5を利用する。

MySQLでutf8mb4を扱う

前述した通り、MySQLで4バイトのUTF-8を扱うにはMySQL 5.5.3以上でキャラクタセットにutf8mb4を使う必要がある。

MySQLサーバ

たぶんキャラクタセット関係にはutf8mb4を設定しておいた方がいい。
character_set_systemcharacter_set_filesystem以外はクライアントーサーバ間の通信と保存に使われるので、適切に設定されていないとちょん切られてしまったりすると思う。要検証

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.5.10/share/mysql/charsets/ |
+--------------------------+------------------------------------------------------+

ActiveRecordからの接続

で、mysql2 gemの最新リリースの0.3.11ではutf8mb4に対応していないので、未だに取り込まれていないパッチを自分であてるか、Gemfileで次のようにブランチを指定する(自分はこっちをやってる)。

gem 'mysql2', :git => 'git://github.com/tmtm/mysql2.git', :branch => 'utf8mb4'

database.ymlのencodingcharsetにutf8mb4を指定する。

development:
  adapter: mysql2
  charset: utf8mb4
  encoding: utf8mb4
  collation: utf8mb4_general_ci
  reconnect: false
  database: myapp_development
  pool: 5
  username: root
  password:
  socket: /tmp/mysql.sock

charsetを指定するのは、CREATE DATABASEDEFAULT CHARACTER SETに使われるから(デフォルトはutf8)。なんでencodingに統一されていないんだろう?

ユニークインデックスのバイト数制限

MySQLのユニークインデックスは767byteまでしか使えないという制限があるため、767byteわる4で191文字までしか使えない。

例えばschema_migrations.versionはvarcharでユニークインデックスがはられているが、デフォルトでは255文字が割り当てられているのでマイグレーションを実行したときにエラーが出る。

実際には14文字しか使わないので、varchar(15)に変更する。

$ bundle open activerecordからlib/active_record/connection_adapters/abstract/schema_statements.rbの419行目あたりを書き換える。

schema_migrations_table.column :version, :string, :null => false, :limit => 15

ただ本番のgemを書き換えるのも嫌なので、自分はconfig/initializers/active_record_schema_migrations_version.rbActiveRecord::ConnectionAdapters::SchemaStatements#initialize_schema_migrations_tableをそのままコピーして、該当箇所だけ書き換えている。

またschema_migrations.version以外にも自分でユニークインデックスをはる場合には:lengthオプションで191文字以下がインデックス対象になるようにする必要がある。

intとvarcharで複合インデックスをはる際には:lengthを細かく指定できるので気をつけて。

add_index :entries, [:user_id, :url],
      unique: true, length: { url: (191-4) },
      name: "index_on_feed_entries"

JSON

ActiveRecord::JSON::Encoding.escapeは4バイト以上の文字のエスケープに失敗するので、今はJSON gemを使ってる。

config/initializers/active_support_json_encoding.rbにこんな感じ:

module ActiveSupport
  module JSON
    module Encoding
      class << self
        def escape_with_json_gem(string)
          ::JSON.generate([string])[1..-2]
        end
        alias_method_chain :escape, :json_gem
      end
    end
  end
end

JSON gemを使わなくても自分でちゃんとエスケープしてあげる方法もある。

参考資料

(ちなみに)WebSocket/Node.js

WebSocketにNode.jsを使う場合、JavascriptのエンジンであるV8が4バイトのUTF-8に対応していないので、URLエンコードなどの適当な変換をかけてからデータを渡してあげる。

URLエンコードだとこんな感じ:

# Rails側
encoded = Rack::Utils.escape(mb_string)

// JS側
var mbString = decodeURIComponent(encoded).replace(/\+/g,  " ");

Android, iOS4はどうするの?

  • iOS4からiOS5で絵文字のキャラクタ・セットが変わったけど、iOS5ではiOS4の絵文字を表示できるので、iOS5からの絵文字をすべてiOS4のものに変換してしまうのがいいかもしれない。
    • でもそれもいつまで有効なんだろうなぁ…。
  • Androidは知らん
    • NaverのLINEががんばってる、気がする。