カラムの値をハッシュ形式で保存する方法があるらしい、というのは以前聞いた事があったのですが、この度色々調べていたところserialize
やstore
を使えば実現できるという事が分かったので、ちょっと使ってみました。
※Railsのserialize
やstore
をプロダクトに導入するときはデメリットをきちんと理解した上で選択してください。チェリー本で有名な伊藤さんもこれらを安易に導入することについて、こちらで警鐘を鳴らしています。
概要
serialize や store は ActiveRecord の機能の一つです。 text型のカラムに配列やハッシュなど、好きな形式のデータを放り込めます。 テーブルやカラムを追加しなくても自由にデータが保存できる 魔法のような機能 (注:皮肉)です。
引用元:ActiveRecord serialize / store の甘い誘惑を断ち切ろう - Qiita
普通は一つのカラムに対してintやtextといった型を指定するし、DBの正規化といった面でもselialize
やstore
は普通は使われるべきではない。
先述した記事内で伊藤さんが仰っているように、「カラムを増やすのが面倒だから」「楽だから」といった理由でこれらを使った実装をするのはもちろん良くないはずです。
例えば一時的に使用されるデータだったり、ハッシュとしてまとめた状態でログに残しておきたいといった明確な目的がない場合は、当たり前に使用しないべきだろうな、という風に思います。
今回は、それを踏まえた上で使ってみる。
使い方
今回はstore
を使って実装。
実際こういった仕様があるかどうかは置いておいて、ユーザーモデルの中に「updateされる前のユーザー情報」を格納するためのカラムprevious_data
を用意し、その中にハッシュ形式で以前のユーザー情報を格納するようにします。
migration
class Users < ActiveRecord::Migration[5.2] def change create_table :users do |t| t.string :name t.text :email t.integer :phone t.text :previous_data end end end
models/user.rb
class User < ActiveRecord::Base store :previous_data, accessors: %i(pre_name pre_email pre_phone) end
selializeとstoreの違いは、アクセサを定義できるかどうか。
配列っぽく単純な羅列で事足りるならselialize
、キーとバリューといったハッシュ形式で使用したいならstore
を使う。
user = User.find(1) user.previous_data => {"pre_name"=>"fugafuga", "pre_email"=>"fugafuga@gmail.com", "pre_phone"=>"09011112222"} user.previous_data[:pre_name] => "fugafuga" user.pre_name => "fugafuga"
previous_data
カラムに対して、アクセサを書いたpre_name
、pre_email
、pre_phone
がハッシュ形式でちゃんと格納されています。
ハッシュなのでuser.previous_data[:pre_name]
で呼び出せるのは想像できたけど、storeでアクセサを定義しておいたら直接呼び出す事ができるので、user.pre_name
で呼び出しができます。感動。
まあでも、アクセサ書いてるから当たり前か。
...と言うことは、意味合いが同じでも純粋にstring型で用意されているname
カラムと命名が一致してはいけないので注意。
アクセサを書いておけばアプリケーション内で普通のカラムみたいな感じで扱えるので、例えばフォームを作るときとかも以下のようにprevious_data
なしに直感的に書く事ができる。
_form.html.slim
= bootstrap_form_for @user do |f| = f.text_field :name = f.text_field :email = f.text_field :phone = f.text_field :pre_name = f.text_field :pre_email = f.text_field :pre_phone = f.submit
(「変更前のデータ」をフォームから入力させるんかい!というツッコミは置いておいて)
これで送信すると、アクセサに書かれたのカラム入った値はprevious_data
の中にきちんと格納してくれる。
user = User.find(1) => #<User:0x00007fd40e3469a0 id: 1, name: "hogehoge", email: "hogehoge@gmail.com" phone: 09012345678 previous_data: {"pre_name"=>"fugafuga", "pre_email"=>"fugafuga@gmail.com", "pre_phone"=>"09011112222"}, created_at: Fri, 17 Jan 2020 11:36:44 JST +09:00>
こんな感じ。
ビューでもモデル(バリデーションのコールバックとか)でも、もちろんコントローラーでも、どこでも直接userのオブジェクトに対してpre_name
やpre_email
メソッドが使えて、確かに「甘い誘惑」って感じがしました。
注意点
- 変更に弱い
- 検索ができない
- あくまでもtext型
上2つに関しては、先ほどから何度か登場しているこちらで詳しく解説されていますのでそちらをご覧ください。
(私もransackで検索してみようと思いましたが、実際できなかったです。)
私が結構痛いなと思ったのは、previous_data
自体があくまでもtext型なので、intやbooleanを期待している値でも全部textの状態でDBに保存されてしまう事でした。
保存される前にモデル側でキャストする処理を自分で書いてあげれば問題ないのですが、それが結構めんどくさいなと思いました。
と言う事で、メリットはあるけどデメリットも結構あるなぁというのが触ってみた感想でした。