LoginSignup
12
0

More than 3 years have passed since last update.

ActiveRecordを使いつつDBのデータをメモ化して扱ってみる

Last updated at Posted at 2019-12-10

はじめに

一度登録されたら、その後あまり更新されないマスター系のデータってありますよね。
普通に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

Gemfile
gem 'ar_memoization'

DBスキーマの例

サンプルとして、以下のように都道府県を扱うprefecturesと、お店情報を扱うshopsテーブルを定義しました。
shopsprefecturesに対する外部キーを持っており、prefecturesはあまり更新されないものとします。

db/create_tables.rb
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します。

app/models/prefecture.rb
#
# 都道府県を扱う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を定義します。

app/models/shop.rb
class Shop < ApplicationRecord
  extend ArMemoization::ForeignMethods
  belongs_to_memoized :prefecture
end

(通常ないと思いますが)belongs_toと同時に利用することも出来ます。

app/models/shop.rb
  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 さんです!

12
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
0