scenic RailsでMaterialized Viewを使う


RailsでPostgreSQLのビュー(View)やマテリアライズド・ビュー(Materialized View)を使うためのGem「thoughtbot/scenic」の紹介です。
thoughtbot謹製ですので、安心して使えそうなのも魅力です!


🍄 PostgreSQLのViewとMaterialized View

PostgreSQLのViewとMaterialized Viewの大まかかな説明は次のとおりです。

View(ビュー)について

  • ViewとはSQLで構成された実態のな仮想的なテーブルのこと。Viewの作成時にSQLを定義する
  • Viewを呼び出すとSQLが即時実行される
  • 最新の情報を常に取得できるが、SQLが複雑だったり、レコードが多いと負荷がかかる

Materialized View(マテリアライズド・ビュー)について

  • 実態を持ったViewのことである。作成するときにSQLの実行結果のテーブルを作る
  • 最新の情報を反映するためにはリフレッシュ処理を行う必要がある
  • SQLが複雑だったり、レコードが多い処理の負荷を下げるのに有効なアプローチである

🗽 PostgreSQLのMaterialized Viewの機能

RailsでPostgreSQLのマテリアライズドビューを使う | 日々雑記』にあるとおり、
PosgreSQLにはリフレッシュ時にSELECTコマンドを発行した時にロックされて応答が遅延しないように、CONCURRENTLY
というパラメータをつけるといいようです!

REFRESH MATERIALIZED VIEW CONCURRENTLY materialized_view;

🏈 Gemのインストール

ではいよいよ、Viewを扱いやすくするGem「thoughtbot/scenic」の紹介です。

Gemfileに以下を追加して、bundle installを実行してください。

# View & Materialized View (GitHubじゃないとちょっと古い気がします)
gem 'scenic', git: 'git@github.com:thoughtbot/scenic.git'

# Fake Data(サンプルデータを作るために使います)
gem 'faker'

🐠 Viewを作成するためのModelを作成

まずは、Viewを作成するための元となるテーブルをScaffoldで作成します。

# 新しい Rails プロジェクトを作成
$ rails new scenic_sample
$ cd scenic_sample

# scaffoldで一通り作成
$ rails g scaffold book title:string author:string price:float

次にサンプルデータを作ります。

# db/seeds.rb
require 'faker'

1000.times do |i|
Book.create(
title: Faker::Name.title,
author: Faker::Name.name,
price: Faker::Commerce.price
)
end

ではテーブルを作って、テストデータを投入します。

# テーブルを作成
$ bundle exec rake db:migrate

# テストデータを投入
$ bundle exec rake db:seed

これでサンプルデータが作成されました。

🐮 いよいよマテリアライズド・ビューを作成

次にマテリアライズド・ビューを作成します。

今回は$50以上のBookをExpensiveBookとして、それを抽出するテーブルにします。

# ExpensiveBookの作成
$ rails generate scenic:model expensive_book --materialized
invoke active_record
create app/models/expensive_book.rb
invoke rspec
create spec/models/expensive_book_spec.rb
invoke factory_girl
create spec/factories/expensive_books.rb
create db/views/expensive_books_v01.sql
create db/migrate/20160106124232_update_expensive_books.rb

🚕 マテリアライズド・ビューのSQL

そんなマテリアライズド・ビューのSQLがこちら。

# db/views/expensive_books_v01.sql
SELECT
*
FROM
books
WHERE
price >= 50.0

あとはリフレッシュ時にCONCURRENTLYを使うために、UNIQ INEXを追加します。

# db/migrate/20160106123247_create_expensive_books.rb
class CreateExpensiveBooks < ActiveRecord::Migration
def change
create_view :expensive_books, materialized: true
# 追加
add_index :expensive_books, :id, unique: true
end
end

ではマテリアライズド・ビューを作成します。

$ bundle exec rake db:migrate
== 20160106123247 CreateExpensiveBooks: migrating =============================
-- create_view(:expensive_books)
-> 0.0272s
== 20160106123247 CreateExpensiveBooks: migrated (0.0272s) ====================

こんな感じのSQLが実行されたようです。

CREATE OR REPLACE VIEW public.expensive_books AS SELECT books.id,
books.title,
books.author,
books.price,
books.created_at,
books.updated_at
FROM books
WHERE (books.price >= (50.0)::double precision);

SQLを変更する場合は、rails g scenic:view expensive_book とすれば変更用のSQLのmigrationファイル生成されます。

🚌 マテリアライズド・ビューのModelファイル

# app/models/expensive_book.rb
class ExpensiveBook < ActiveRecord::Base
# find_by!とかを使う場合はprimary_keyが必要なので
self.primary_key = :id

# このメソッドを実行するとマテリアライズド・ビューが更新される
def self.refresh
Scenic.database.refresh_materialized_view(table_name, concurrently: true)
end

private
# マテリアライズド・ビューは書き込みができないので読み込み専用のViewとして作成
def readonly?
true
end
end

一点GemのREADMEに書いていた注意点として、リフレッシュ時のパラメータにconcurrently: trueとした場合
はlockがかからないけど、PostgreSQL 9.4 以上 && uniq index が必須なので注意してください。

🐯 参考リンク

🖥 VULTRおすすめ

VULTR」はVPSサーバのサービスです。日本にリージョンがあり、最安は512MBで2.5ドル/月($0.004/時間)で借りることができます。4GBメモリでも月20ドルです。 最近はVULTRのヘビーユーザーになので、「ここ」から会員登録してもらえるとサービス開発が捗ります!

📚 おすすめの書籍