はじめに
一度登録されたら、その後あまり更新されないマスター系のデータってありますよね。
普通にDBのテーブルを作成してActiveRecordを使ったり、moduleやclassにデータをハードコーディングして扱う場合もあると思います。
テーブルが不要な場合は ActiveHash というgemを使うと便利で高速ですが、なんらかの理由でDBを使いたい場合もあるかもしれません。
(例えば、Railsから切り離されたレポートシステムなどから同じマスターデータを参照したい場合など)
ということで、ActiveRecord を利用しつつ普段はオンメモリで高速にデータを扱う簡単なライブラリを作成してみました。
実装したもの要約
- マスター系データを扱うModelクラスで、全レコードのオブジェクトをメモ化するメソッド
- メモ化したデータを対象にfind, selectを行うメソッド
- 上記Modelに対しbelongs_toの関係にあるModelから参照する場合も、メモ化されたオブジェクトを利用するAssociation
使い方
インストール
RubyGemsに登録したのでGemfileを使ったりgemコマンドを使ってインストールできます。
https://rubygems.org/gems/ar_memoization
gem 'ar_memoization'
DBスキーマの例
サンプルとして、以下のように都道府県を扱うprefectures
と、お店情報を扱うshops
テーブルを定義しました。
shops
はprefectures
に対する外部キーを持っており、prefectures
はあまり更新されないものとします。
class CreateAllTables < ActiveRecord::Migration[5.0]
def self.up
create_table(:prefectures) do |t|
t.string :name
end
create_table(:shops) do |t|
t.belongs_to :prefecture
t.string :name
end
end
end
マスターデータを定義するModelクラス
全レコードのインスタンスをメモ化したいModelクラス内でArMemoization::PrimaryMethods
モジュールをextendします。
#
# 都道府県を扱うModel
#
class Prefecture < ApplicationRecord
extend ArMemoization::PrimaryMethods
end
追加されるメソッド
- .find_memo(id)
- メモ化したインスタンスから、引数のIDに一致するデータを返す
- IDをキーにしたHashから該当レコードのオブジェクトを返すので爆速
- .detect_memo(&block)
- メモ化したレコードのオブジェクトをブロック引数で渡し、ブロックがtrueを返したデータを1件返す
- select_memos(&block)
- メモ化したレコードのオブジェクトをブロック引数で渡し、ブロックがtrueを返したデータを配列で返す
- all_memos
- メモ化したインスタンスの配列を全て返す
- reload_memos
- メモ化したオブジェクトをDBからリロードする
使い方の例
[[1, "東京"], [2, "大阪"], [3, "名古屋"]].each do |ident, name|
Prefecture.create!(id: ident, name: name)
end
# ID:2 のレコードを取得
prefecture = Prefecture.find_memo(2)
# インスタンスメソッド kanto_area? がtrueを返すオブジェクトを取得
prefecture = Prefecture.detect_memo{|pref| pref.kanto_area? }
prefecture = Prefecture.detect_memo(&:kanto_area?) # syntax sugar
# インスタンスメソッド kanto_area? がtrueを返すオブジェクトの配列を取得
prefectures = Prefecture.select_memos(&:kanto_area?)
実装の詳細はこちら
マスターデータへの外部キーを持つModelクラス
マスターデータのModelに対してbelongs_toの関係を持つModelクラスにはArMemoization::ForeignMethods
をextendします。
また、belongs_to
の代わりにbelongs_to_memoized
を使いAssociationを定義します。
class Shop < ApplicationRecord
extend ArMemoization::ForeignMethods
belongs_to_memoized :prefecture
end
(通常ないと思いますが)belongs_toと同時に利用することも出来ます。
belongs_to :prefecture
belongs_to_memoized :memoized_prefecture, class_name: "Prefecture", foreign_key: "prefecture_id"
belongs_to_memoized
は内部でbelongs_to
を実行した後で、関連名でもあるreaderメソッド(上記例ではShop#prefecture
)をoverrideし、レコードをDBからロードする処理の代わりにメモ化済みのオブジェクトをAssociationとしてセットしています。
Association以外で追加されるメソッド
- .where_memoized(association_name, method_name, &block)
- 関連Modelのメモ化されたオブジェクトを用い、join + where 的な絞り込みを行う
- ActiveRecord::Relation を返す
使い方の例
# Prefectureオブジェクトの中で、kanto_area?がtrueを返すオブジェクトのIDに関連しているShopのRelationを返す
# 第2引数として関連Modelのインスタンスメソッド名か、ブロックを渡す
Shop.where_memoized(:prefecture, :kanto_area?).limit(3)
Shop.where_memoized(:prefecture){|pref| pref.kanto_area? }.limit(3)
SELECT `shops`.* FROM `shops` WHERE `shops`.`prefecture_id` = 1 LIMIT 3
同一の条件文を実装するのに、scopeとインスタンスメソッドの両方でコードを書く必要がなくなるので、自分的にはちょっと良い感じです。
実装の詳細はこちら
今は出来ないけど出来そうなこと
- ID以外のPrimary Key に対応
- has_one, has_many からメモ化済みオブジェクトの取得
- create, saveのコールバックでメモ化したオブジェクトを差し替える
- 開発環境ではメモ化したオブジェクトを適切なタイミング(before_actionなど)で簡単にリロードする仕組み
まとめ
ほとんどの場合はActiveHashを使えば良いと思うので、このライブラリの使いどころがあまり思いつきません。とりあえず「こんなのあったらどうだろう?」な思いつきを形にしてみました。むしろ使い道を教えてください😇
それでは、もっと有意義な時間を過ごすべくデザインパターンの勉強でもしてみましょう。
12日目は @nagata03 さんです!