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_table
でjson型のカラムを作ることができます。同様に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
を使ってみます。
以下のように User
クラスの中に書きます。第一引数にはjson型のカラム名(今回は :profile
)、第二引数以降にはkey(今回は :name, :age
)を書きます。
class User < ApplicationRecord store_accessor :profile, :name, :age end
すると name
と age
の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}