RailsでActiveRecordのserializeで保存したスナップショットと内部クラスを使ってデータを返す

こんにちは!kossyです!




さて、今回はRailsActiveRecordのserializeで保存したスナップショットと
内部クラスを使ってTemporaryなデータを返す方法をブログに残してみたいと思います。



環境

Ruby 2.6.3
Rails 6.0.3
MacOS Mojave



ActiveRecordのserializeとは

なにはともあれまずは公式ドキュメントをチェック

If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object,
then specify the name of that attribute using this method and it will be handled automatically.
The serialization is done through YAML. If class_name is specified, the serialized object must be of that class on assignment and retrieval.
Otherwise SerializationTypeMismatch will be raised.

Empty objects as {}, in the case of Hash, or [], in the case of Array, will always be persisted as null.

Keep in mind that database adapters handle certain serialization tasks for you.
For instance: json and jsonb types in PostgreSQL will be converted between JSON object/array syntax and Ruby Hash or Array objects transparently.
There is no need to use serialize in this case.

オブジェクトとしてデータベースに保存し、同じオブジェクトとして取得する必要がある属性がある場合は、
このメソッドを使用してその属性の名前を指定すると、自動的に処理されます。シリアライズYAMLを介して行われます。
class_nameが指定されている場合、シリアライズしたオブジェクトは、割り当てと取得時にそのクラスのものでなければなりません。
そうでない場合、SerializationTypeMismatchが発生します。

ハッシュの場合は {} としての空のオブジェクト、または配列の場合は [ ]として空のオブジェクトは常にnullとして永続化されます。

データベースアダプターは特定のシリアル化タスクを処理することに注意してください。
たとえば、PostgreSQLjsonおよびjsonbタイプは、JSONオブジェクト/配列構文とRubyハッシュまたは配列オブジェクトの間で透過的に変換されます。

出典: https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html

DBにJSONやオブジェクトを保存して、Rubyライクに処理ができるという認識で良さそうです。


内部クラスとは

クラスの中に定義されたクラスのことを内部クラスと言います。
内部クラスを利用することで、クラス間の関係がわかりやすくなり、ソースコードがシンプルになり、可読性が増します。
また、使用場所を限定し、その存在を外部から隠したい場合にも使用します。

出典: https://techacademy.jp/magazine/32398

インナークラスや内部クラスとググると、Javaでの実装の記事が多く出てくるので、Rubyではあまりメジャーな実装方法ではないのかもしれません。

本エントリでは、serializeで保存したスナップショットをクラスとして扱うようにして、責務の分離と可読性向上の達成を試みてみます。

サンプル


例として、オンライン書籍ストアアプリがあったとします。

テーブル構成は、

  • purchases(購買履歴)
  • books(書籍)
  • company(出版社)

で、関連は

  • purchaes has_many books & books belongs_to purchase

一回の購買で複数の書籍を買うこともあるため

  • company has_many books & books belongs_to company

この条件の時に、
「買った当時の書籍の出版社名を表示したい」という要件があった場合、
万が一出版社の名前が変わった場合、book belongs_to company の関連が組まれていたら、現在の情報を元に出版社名を取得するようになってしまいます。

これを防ぐために、purchasesテーブルに用意したbooks_info的なスナップショットが保存されたカラムのデータを元に出版社名を算出するようにします。

class Purchase < ApplicationRecord

  serialize :book_info, JSON

  class BookInfo
    attr_reader :company, :count

    def initialize(company:, count:)
      @company = company
      @count = count
    end
  end

end


app/views/purchaes/_purchaes.json.jbuilder

# book_jsonはこの仕様の場合は配列を想定すべきですが、手抜きしています ←

json.extract! purchase, :id, :created_at,
book_info = BookInfo.new(purchase.book_info['company'], purchase.book_info['count'])
json.extract! book_info, :company, :count,

これぐらいの情報量ならpurchasesに履歴データを表すカラムを追加すればいいのでは?と思われるかもしれませんが、
要件が変わってより多くのデータを扱いたくなった場合、際限なくカラムを追加することになるのはちと厳しいです。
より多く扱う必要が出た場合はJSONで保存する項目を増やし、都度attr_readerを生やす形を取ればpurchasesの責務が膨らまずに済みます。