読者です 読者をやめる 読者になる 読者になる

railsのmysqlでemojiを使う方法

emojiを使おうと思ったら、色々と問題があるようなので、まとめます。

文字コード問題

まず、emojiはmysqlのutf8では使用できません。 mysqlでemojiを使用するにはutf8mb4にする必要があります。

これは何をしようとしているのかいうと、emojiの文字コードは1文字に4バイト使用します。 通常のutf8では、1文字に対して3バイトまでしか使用できません。 なので、mysqlで保存するためには、文字コードを変更する必要があります。

collation問題

こちらは初期設定のutf8mb4の設定でいくと、寿司ビール(🍣🍺)問題になります。 🍣と🍺が同じものとして扱われます。 こちらを別々に検索できるようにするための設定が必要になります。

これに関連してハハパパ問題というのがあります。

utf8でもきちんと設定しておかないと、はまったりします。

ハハパパ問題(参考)

ハハパパ問題は、「ハハ」「パパ」が同一の値として扱われます。 そんなことがあるのか!って思いましたが、railsmysqlではdefaultでこの設定になっています。 何も知らないと検索結果おかしい・・・みたいな罠にはまります。 では、どのようなcollationがあるかを見ていきます。

  • utf8_bin
  • utf8_general_ci
  • utf8_unicode_ci

utf8_bin 文字コードが完全に一致するもののみ返す 一番厳しいですね

utf8_general_ci アルファベットの大文字・小文字は区別しない 他は全て区別する

utf8_unicode_ci 大文字小文字/全角半角を区別しない。

「あああ」は「アアア」「ァァァ」「ぁぁぁ」にマッチする。

railsはdefaultでutf8_unicode_ciになっています。 なので、この設定は変更しましょう。

show full columns from topics;でcollationの確認をすることができます。

mysql> show full columns from topics;
+------------------------+--------------+-----------------+------+-----+---------+----------------+---------------------------------+---------+
| Field                  | Type         | Collation       | Null | Key | Default | Extra          | Privileges                      | Comment |
+------------------------+--------------+-----------------+------+-----+---------+----------------+---------------------------------+---------+
| id                     | int(11)      | NULL            | NO   | PRI | NULL    | auto_increment | select,insert,update,references |         |
| title                  | varchar(255) | utf8_unicode_ci | NO   |     | NULL    |                | select,insert,update,references |         |
| body                   | text         | utf8_unicode_ci | NO   |     | NULL    |                | select,insert,update,references |         |
| name                   | varchar(255) | utf8_unicode_ci | NO   |     | NULL    |                | select,insert,update,references |         |
| category_id            | int(11)      | NULL            | NO   | MUL | NULL    |                | select,insert,update,references |         |
| thumbnail_updated_at   | datetime     | NULL            | YES  |     | NULL    |                | select,insert,update,references |         |
| thumbnail_file_size    | int(11)      | NULL            | YES  |     | NULL    |                | select,insert,update,references |         |
| thumbnail_content_type | varchar(255) | utf8_unicode_ci | YES  |     | NULL    |                | select,insert,update,references |         |
| thumbnail_file_name    | varchar(255) | utf8_unicode_ci | YES  |     | NULL    |                | select,insert,update,references |         |
| created_at             | datetime     | NULL            | NO   |     | NULL    |                | select,insert,update,references |         |
| updated_at             | datetime     | NULL            | NO   |     | NULL    |                | select,insert,update,references |         |
+------------------------+--------------+-----------------+------+-----+---------+----------------+---------------------------------+---------+
11 rows in set (0.00 sec)

何もしていないと、無事にutf8_unicode_ciになって、ウワって結果になると思います。

default: &default
  adapter: mysql2
  collation: utf8_general_ci

これは初期設定でやるべきことですね。

もし、運用フェーズで気づいたなら、mysql側で変更しましょう。

index問題

最大のインデックス長が757バイトになっています。 utf8では255文字で、utf8mb4では191文字になります。

解決方法としては、二つあります。

  • インデックス長を増やす
  • インデックス長を減らす

減らす方法はデフォルトの動きではならなくなるので、インデックス長を増やす方が良いです。

mysql側の設定

my.cnfを設定します。

[mysqld]
innodb_file_per_table
innodb_file_format=barracuda
innodb_large_prefix = 1

rails側の設定

database.ymlの設定

default: &default
  adapter: mysql2
  charset: utf8mb4
  encoding: utf8mb4
  collation: utf8mb4_bin
  pool: 5
  timeout: 5000

絵文字を区別したいのであれば、collationの設定をutf8mb4_binにしないといけません。 区別する必要がないのであれば、utf8mb4_general_ciで大丈夫です。

ただし、このままだとmigrationの設定がうまくいかないので、挙動を変更するようにします。

config/initializers/ar_innodb_row_format.rb

ActiveSupport.on_load :active_record do
  module ActiveRecord::ConnectionAdapters

    class AbstractMysqlAdapter
      def create_table_with_innodb_row_format(table_name, options = {})
        table_options = options.merge(:options => 'ENGINE=InnoDB ROW_FORMAT=DYNAMIC')
        create_table_without_innodb_row_format(table_name, table_options) do |td|
          yield td if block_given?
        end
      end
      alias_method_chain :create_table, :innodb_row_format
    end

  end
end

以上です。

参考

qiita.com

qiita.com

クラスメソッドをprivateメソッドにする方法

単純にprivateにするだけはダメです。

class Foo
  def self.bar
    puts 'bar'
  end

  private

  def self.baz
    puts 'baz'
  end
end

Foo.baz
=>baz

という風に実は普通に呼べています。

方法は二種類あります。

  • self内でprivateを呼ぶ
  • private_class_methodを定義する

self内privateを呼ぶ

class Foo
  def self.bar
    puts 'bar'
  end

  class << self
    private

    def baz
      puts 'baz'
    end
  end
end

Foo.baz
NoMethodError: private method `baz' called for Foo:Class
Did you mean?  bar

きちんとprivate化されていますね。

private_class_methodを定義する

class Foo
  def self.bar
    puts 'bar'
  end

  def self.baz
    puts 'baz'
  end

  private_class_method :baz
end

Foo.baz
NoMethodError: private method `baz' called for Foo:Class
Did you mean?  bar

同じ結果になっています。 ただし、後者はメソッドが定義された後に書かないといけなません。

そこは注意が必要ですね。

以上です。

文字列からクラス名にアクセスする方法

constantizeを使用します。

"Topic".constantize
=> Topic(id: integer, title: string, body: text, category_id: integer, thumbnail_updated_at: datetime, thumbnail_file_size: integer, thumbnail_content_type: string, thumbnail_file_name: string, created_at: datetime, updated_at: datetime, name: string)

定義ファイル activesupport-4.2.7.1/lib/active_support/core_ext/string/inflections.rb @ line 65:

def constantize
  ActiveSupport::Inflector.constantize(self)
end

自分自身を返してアクセスしている。

以上です。

nestしたcontent_tagを書く方法と注意点

nestしたcontent_tagを書く場合です。

concatで連結して書くことができます。

content_tag(:div, class: "c-grid__quotation-image") do
  concat(image_tag(url))
  concat(link_to("出典:#{host_name}", url))
end

ただし、こんだけ連結させる場合は、別途renderで書いた方がマシです。

render "foo", url: urlm host_name: host_name

役割をハッキリ決めて、読みやすくしないとダメだ。

file_fieldに拡張子を制限する方法

今までjs側で処理を行おうとしていました。

普通にhtml属性で設定ができます。

= f.file_field :thumbnail,accept: 'image/jpg,image/jpeg,image/png,image/gif'

acceptを使用することで、拡張子が画像系のものしか上げなくできます。

twitterってどうやってアップロードするファイルを制限しているんだろ?って調べた時に見つかりました。

ただ、普通にオプションに書いてありました。

file_field - リファレンス - - Railsドキュメント

Redisについて

Redisについてお話ししたいと思います。

Redisとは

メモリ上にKVS(Key Value Store)を保存できるようにするソフトウェアです。

  • インメモリ型データベース
    • メモリ上にデータをすべて持ち、ディスクから取り出すよりも高速処理が可能になる
  • データを永続化する
  • データ構造サーバー
    • String型, List型, Set型, SortedSet型, Hash型のデータ構造を持つことができる

公式サイト

Redis

データ型について

  • String型
    • 個々の要素を保存
  • List型
    • 順番に並べた複数の要素 ex: 時系列用のデータ
  • Set型
    • 順不同の複数の要素。重複を許さない ex: タグ
  • SortSet型
    • Setの特徴を持ちつつ、個々の要素にScore付けをする ex: ランキング機能
  • Hash型

起動方法

redis-server

# 別のconsole画面を開く
redis-cli

これでコマンドライン上から操作できるようになります。

String型の操作

String型は単純なkey, valueを保存するものになります。

set

key, valueの値を設定する

127.0.0.1:6379> set foo bar

127.0.0.1:6379> keys *
1) "foo"

get

keyを指定して、valueの値を取得します。

127.0.0.1:6379> get foo
=>bar

del

keyを指定して、 key, valueを削除します。

127.0.0.1:6379> del foo
=>1

127.0.0.1:6379> keys *

List型

(l/r)push

lは先頭に追加で、rは末尾に追加されます。

127.0.0.1:6379> rpush number 1
(integer) 1
127.0.0.1:6379> rpush number 2
(integer) 2
127.0.0.1:6379> rpush number 3
(integer) 3
127.0.0.1:6379> rpush number 4
(integer) 4
127.0.0.1:6379> rpush number 5
(integer) 5

(l/r)pop

lは先頭を削除で、rは末尾を削除します。

127.0.0.1:6379> rpop number
"5"
127.0.0.1:6379> lpop number
"1"

lrange

lrangeでList型の値を取得します。 lはList型のlを表しています。

127.0.0.1:6379> lrange number 0(start) -1(end)
1) "2"
2) "3"
3) "4"

lindex

特定の位置の値を取得

127.0.0.1:6379> lindex number 2
"4"
127.0.0.1:6379> lindex number 0
"2"

llen

要素の数を取得

127.0.0.1:6379> llen number
(integer) 3

ltrim

指定した位置以外の値を削除する

127.0.0.1:6379> ltrim number 0 1
OK
127.0.0.1:6379> lrange number 0 -1
1) "2"
2) "3"

Set型

sadd

Set型の値を設定していく

127.0.0.1:6379> sadd member foo bar baz
(integer) 3
127.0.0.1:6379> sadd member bob
(integer) 1
127.0.0.1:6379> sadd member2 abc def ghi
(integer) 3
127.0.0.1:6379> sadd member2 foo bar
(integer) 2

srem

Set型の値を削除する

127.0.0.1:6379> srem member bob
(integer) 1

smembers

Set型の値を取得する

127.0.0.1:6379> smembers member
1) "foo"
2) "bar"
3) "baz"

sunion

和集合

127.0.0.1:6379> sunion member member2
1) "baz"
2) "foo"
3) "bar"
4) "def"
5) "abc"
6) "ghi"

sinter

積集合

127.0.0.1:6379> sinter member member2
1) "foo"
2) "bar"

sdiff

差集合

127.0.0.1:6379> sdiff member member2
1) "baz"

SortSet

zadd

SortSet型の値を設定する

127.0.0.1:6379> zadd score 22 mikami 50 yagami 80 yasuda 21 akashi
(integer) 4
127.0.0.1:6379> zadd score 99 hoge
(integer) 1

zrem

SortSet型の値を削除する

127.0.0.1:6379> zrem score hoge
(integer) 1

zrange

SortSet型の値を取得する(取得方法はList型と同じ)

127.0.0.1:6379> zrange score 0 -1
1) "akashi"
2) "mikami"
3) "yagami"
4) "yasuda"

## revで反対の順番にする
127.0.0.1:6379> zrevrange score 0 -1
1) "yasuda"
2) "yagami"
3) "mikami"
4) "akashi"

zrank

順番を知る

127.0.0.1:6379> zrank score yasuda
(integer) 3

## revで反対の順番にする
127.0.0.1:6379> zrevrank score yasuda
(integer) 0

Hash型

hset

Hash型の値を設定する

127.0.0.1:6379> hset user name mikami
(integer) 1
## hmsetで複数の値を設定
127.0.0.1:6379> hmset user email foo@foo.com score 100
OK

hget

Hash型の設定を取得する

127.0.0.1:6379> hmget user name email score
1) "mikami"
2) "foo@foo.com"
3) "100"

hlen

Hash型の値の数を取得する

127.0.0.1:6379> hlen user
(integer) 3

hkeys

Hash型のkeyを全て取得する

127.0.0.1:6379> hkeys user
1) "name"
2) "email"
3) "score"

hvalues

Hash型のvalueを全て取得する

127.0.0.1:6379> hvals user
1) "mikami"
2) "foo@foo.com"
3) "100"

hgetall

全ての値を取得する

127.0.0.1:6379> hgetall user
1) "name"
2) "mikami"
3) "email"
4) "foo@foo.com"
5) "score"
6) "100"

まとめ

概要と簡単な使い方の説明になりました。 高速に値を取得するので、一部の場所に使用することができます。 基本的にはデータを永続化する仕組みが整っているとはいえ、ストレージとして利用されるのはおかしいと思っています。 expiresで期限を設定できるので、期限を設定して適切な値保持にしましょう。

capybaraのrpsec実行時にブラウザを切り替えるgemを作成しました

普段はwebkitで十分なのですが、たまにchromeで開きたいって時ありますよね?

そんなgemを作成しました。

GitHub - ryosuke-endo/capybara_switch_driver

中身は微妙かもしれないけど、自分で使う分だからいいのかなと思いました。

以上です。

javascriptのprototypeオブジェクトの利点

javascriptのprototypeオブジェクトについてです。

今まで適当にやっていたのですが、少しまとめたいと思います。

  1. メモリの使用量を削減できる
  2. メンバーの追加や変更をインスタンスがリアルタイムに認識できる

メモリの使用量を削減できる

こちらはどういうことかを説明します。

javascriptコンストラクターによるメソッドの追加は、メソッドの数に比例して、無駄なメモリを消費するという問題があります。

var Member = function(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
  this.getName = function() {
    return this.firstName + '' + this.lastName;
  }
}

mem = new Member("foo", "bar");
mem_2 = new Member("foo", "baz");

これが永遠と作成されてしまうと、その度にメモリが消費していきます。

そこでprototypeを使用します。

prototypeはインスタンス化した場合は、元となるオブジェクトに属するprototypeオブジェクトに対して暗黙的に参照を持つようになります。

var Member = function(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

Member.prototype.getName = function() {
  return this.firstName + '' + this.lastName;
}

mem = new Member("foo", "bar");
mem_2 = new Member("foo", "baz");

mem.getName()
=>"foobar"

参照でメソッドを実行しているので、無駄なメモリ確保がなくなります。

メンバーの追加や変更をインスタンスがリアルタイムに認識できる

var Member = function(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

mem = new Member("foo", "bar");
mem_2 = new Member("foo", "baz");

Member.prototype.getName = function() {
  return this.firstName + '' + this.lastName;
}

mem.getName()
=>"foobar"

このように後から追加することができます。

以上です。

database cleanerの設定について

rspecのテスト実行時にDatabaseCleanerを使用しています。

データベースを毎回キレイな状態にしてくれるやつですね。

これの何がいいかというと、テストデータが固定化されるのと、増えないのでテストの実行が遅くならないということです。

ただし、設定によってテストが遅くなることもあります。

きちんとした設定をすることが高速化につながります。

そして、設定について、曖昧だったのでまとめておきます。

transactionとtruncationについて

trancationはDBを毎回削除しています。

truncate文ですね。

transactionはDBをrollbackしています。

手元で実行速度を確認したかったので、試してみました。

truncation

Finished in 11.22 seconds (files took 0.45743 seconds to load)
45 examples, 0 failures, 1 pending

transaction

Finished in 10.94 seconds (files took 0.45952 seconds to load)
45 examples, 0 failures, 1 pending

若干ながら、transactionの方が早いですね。

公式のドキュメントにも、transactionの方が早いって謳ってます。

For the SQL libraries the fastest option will be to use :transaction as transactions are simply rolled back. If you can use this strategy you should.

しかし、少しだけ問題があります。

However, if you wind up needing to use multiple database connections in your tests (i.e. your tests run in a different process than your application) then using this strategy becomes a bit more difficult. You can get around the problem a number of ways.

javascriptのテストを実行するときようなときは注意が必要です。

transactionの問題点

jsのテストを実行する場合は、ブラウザで動いているプロセスから、データベースの値が取得できなくてテストが落ちてしまいます。

transactionの場合は、transaction内でデータを生成しますが、別のスレッドからはデータを取得できないからです。

公式にも書いていますが、下記のようにfeatureテストのみtrancationを使用するようにします。

  config.before(:each, type: :feature) do
    # :rack_test driver's Rack app under test shares database connection
    # with the specs, so continue to use transaction strategy for speed.
    driver_shares_db_connection_with_specs = Capybara.current_driver == :rack_test

    if !driver_shares_db_connection_with_specs
      # Driver is probably for an external browser with an app
      # under test that does *not* share a database connection with the
      # specs, so use truncation strategy.
      DatabaseCleaner.strategy = :truncation
    end
  end

また、下記の部分もfalseに変更します。

  config.use_transactional_fixtures = false

挿入するデータをtransactionするのではなく、truncationするようになります。

ここの常にtruncationする必要はないため、特定の時だけfalseにする設定ができれば、処理が早くなりそうな気がします。

まとめ

DB 別のスレッドからデータ参照
truncation truncate(削除)
transaction rollback ×

適当に設定していたけど、意外に奥が深くて面白いですね。

viewの条件式を減らす

viewにロジックを書きすぎるな!ってことです。

自戒の意味を込めて書きます。

- if area? && action_name == "show"
  - if citys?
     = render "foo"
  - else
     = render "bar"
- else
  = render "bar"

こういうのがあったとします。

viewにこんだけロジックが積まれるとしんどいので、まとめます。

def foo?
  if area? && action_name == "show"
    if citys?
      true
    end
  end
end
- if doo?
  = render "foo"
- else
  = render "bar"

こんな感じになるかと思います。

viewにロジックが散らばると、同一条件で別の場所で表示するときにまた書かないといけなくなります。

ロジックをviewでガリガリ書くのはダメですね。