JSON型カラムをActiveRecord::Storeで使いやすくしてみる

今回はRails アプリケーションの開発でJSON型のカラムを扱った際に ActiveRecord::Store を使うと便利だったのでそのことについて共有します。

実行環境

以下の環境で試しました。 - Ruby 2.7.1 - Rails 6.0.3.2 - Postgres

JSON型について

PostgresqlではJSONデータ型をサポートしています。 https://www.postgresql.jp/document/12/html/datatype-json.html

Railsではcreate_tablejson型のカラムを作ることができます。同様にjsonb型のカラムも作ることができます。 https://railsguides.jp/active_record_postgresql.html#json%E3%81%A8jsonb

例題をつかって試してみる

例として、profile という json 型カラムを持つ users テーブルを定義することにします。

class CreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.json :profile

      t.timestamps
    end
  end
end

以下のモデルも定義します。ひとまず何もない空のクラスにします。

class User < ApplicationRecord
end

この状態で適当なデータを作って、json型カラムの中の値を参照してみます。

user = create(profile: { name: 'Ken', age: 20 })

user.profile
#=> {"name"=>"Ken", "age"=>20}

user.profile['name']
#=> "Ken"

user.name
#=> NoMethodError (undefined method `name' for #<User:0x00007f9f8d20f510>)

最後の行で試した user.name ですが、 nameというカラムは無いため NoMethodError になってしまいます。

store_accessor

そこで、 store_accessor を使ってみます。

api.rubyonrails.org

以下のように User クラスの中に書きます。第一引数にはjson型のカラム名(今回は :profile)、第二引数以降にはkey(今回は :name, :age)を書きます。

class User < ApplicationRecord
  store_accessor :profile, :name, :age
end

すると nameage のGetter・Setterメソッドが追加され、通常のカラムと同様に扱えるようになります。

user.reload.profile
#=> {"name"=>"Ken", "age"=>20}
user.name
#=> "Ken"
user.age
#=> 20

user.age = 30
user.age
#=> 30
user.changed?
#=> true

user.save
#=> User Update
user.reload.profile
#=> {"name"=>"Ken", "age"=>30}

また、設定した属性の一覧は Model.stored_attributesで確認できます。

User.stored_attributes[:profile]
#=> [:name, :age]

バリデーションも行える

通常のカラム同様に使えるようになるので、もちろん バリデーション も設定することができるようになります。

class User < ApplicationRecord
  store_accessor :profile, :name, :age
  
  validates :name, presence: true
end
user.name = ""
user.save!
#=> ActiveRecord::RecordInvalid (Validation failed: Name can't be blank)

accessors を overwrite

Getter・Setter メソッドはsuperを使うことで overwrite できます。 例えばnameが大文字で返すかつ、age=で代入するとIntegerに変換されるようにしてみました。

class User < ApplicationRecord
  store_accessor :profile, :name, :age
  
  validates :name, presence: true
  
  def name
    super.downcase
  end
  
  def age=(number)
    super(number.to_i)
  end
end
user.profile
#=> {"name"=>"Ken", "age"=>20}
user.name
#=> "KEN"

user.age = "30"
user.profile
#=> {"name"=>"Ken", "age"=>30}

参考URL

https://api.rubyonrails.org/classes/ActiveRecord/Store.html