railsのmysqlでemojiを使う方法
emojiを使おうと思ったら、色々と問題があるようなので、まとめます。
文字コード問題
まず、emojiはmysqlのutf8では使用できません。 mysqlでemojiを使用するにはutf8mb4にする必要があります。
これは何をしようとしているのかいうと、emojiの文字コードは1文字に4バイト使用します。 通常のutf8では、1文字に対して3バイトまでしか使用できません。 なので、mysqlで保存するためには、文字コードを変更する必要があります。
collation問題
こちらは初期設定のutf8mb4の設定でいくと、寿司ビール(🍣🍺)問題になります。 🍣と🍺が同じものとして扱われます。 こちらを別々に検索できるようにするための設定が必要になります。
これに関連してハハパパ問題というのがあります。
utf8でもきちんと設定しておかないと、はまったりします。
ハハパパ問題(参考)
ハハパパ問題は、「ハハ」「パパ」が同一の値として扱われます。 そんなことがあるのか!って思いましたが、railsのmysqlでは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
以上です。
参考
クラスメソッドを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ってどうやってアップロードするファイルを制限しているんだろ?って調べた時に見つかりました。
ただ、普通にオプションに書いてありました。
Redisについて
Redisについてお話ししたいと思います。
Redisとは
メモリ上にKVS(Key Value Store)を保存できるようにするソフトウェアです。
- インメモリ型データベース
- メモリ上にデータをすべて持ち、ディスクから取り出すよりも高速処理が可能になる
- データを永続化する
- データ構造サーバー
- String型, List型, Set型, SortedSet型, Hash型のデータ構造を持つことができる
公式サイト
データ型について
- 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オブジェクトについてです。
今まで適当にやっていたのですが、少しまとめたいと思います。
- メモリの使用量を削減できる
- メンバーの追加や変更をインスタンスがリアルタイムに認識できる
メモリの使用量を削減できる
こちらはどういうことかを説明します。
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でガリガリ書くのはダメですね。