箱のプログラミング日記。

えんじにあ奮闘記

Railsのserialize/storeを試してみた

f:id:y_hakoiri:20191102121704j:plain

カラムの値をハッシュ形式で保存する方法があるらしい、というのは以前聞いた事があったのですが、この度色々調べていたところserializestoreを使えば実現できるという事が分かったので、ちょっと使ってみました。

※Railsのserializestoreをプロダクトに導入するときはデメリットをきちんと理解した上で選択してください。チェリー本で有名な伊藤さんもこれらを安易に導入することについて、こちらで警鐘を鳴らしています。

概要

serialize や store は ActiveRecord の機能の一つです。 text型のカラムに配列やハッシュなど、好きな形式のデータを放り込めます。 テーブルやカラムを追加しなくても自由にデータが保存できる 魔法のような機能 (注:皮肉)です。

引用元:ActiveRecord serialize / store の甘い誘惑を断ち切ろう - Qiita

普通は一つのカラムに対してintやtextといった型を指定するし、DBの正規化といった面でもselializestoreは普通は使われるべきではない。

先述した記事内で伊藤さんが仰っているように、「カラムを増やすのが面倒だから」「楽だから」といった理由でこれらを使った実装をするのはもちろん良くないはずです。

例えば一時的に使用されるデータだったり、ハッシュとしてまとめた状態でログに残しておきたいといった明確な目的がない場合は、当たり前に使用しないべきだろうな、という風に思います。

今回は、それを踏まえた上で使ってみる。

使い方

今回は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_namepre_emailpre_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_namepre_emailメソッドが使えて、確かに「甘い誘惑」って感じがしました。

注意点

  • 変更に弱い
  • 検索ができない
  • あくまでもtext型

上2つに関しては、先ほどから何度か登場しているこちらで詳しく解説されていますのでそちらをご覧ください。

(私もransackで検索してみようと思いましたが、実際できなかったです。)

私が結構痛いなと思ったのは、previous_data自体があくまでもtext型なので、intやbooleanを期待している値でも全部textの状態でDBに保存されてしまう事でした。

保存される前にモデル側でキャストする処理を自分で書いてあげれば問題ないのですが、それが結構めんどくさいなと思いました。

と言う事で、メリットはあるけどデメリットも結構あるなぁというのが触ってみた感想でした。

参考

ActiveRecord serialize / store の甘い誘惑を断ち切ろう - Qiita

Rails4でserializeしてデータをDBに保存させる | EasyRamble