こんにちは。DIGGLEエンジニアのzakkyです。最近、長女、次女&三女(双子)がそれぞれ高校、中学校へ入学しました。可愛かったです。
いよいよレポート(多軸分析)の集計処理部分のパフォーマンス向上施策の第3弾となります。
前回までの記事の紹介
diggle.engineer diggle.engineer
今回のお題
今回はRails+ApartmentでRails Wayに乗っかりつつPL/pgSQLを導入した際のあれやこれやをご紹介します。plv8については、今回以上に大変だったので別記事として次回お話しする予定となっています。
書かないこと
以下の内容は含みませんので予めご了承ください
- Rails、Apartmentの導入などの基礎的な部分
- PostgreSQLのプロシージャに関する説明
どれくらい速くなったのか
前回も記載しましたが、以下が今回改善した集計処理部分のLatency推移となります。 リリース後には低下していることが見て取れるかと思います。
大体3倍速程度には改善することができました。
Railsでプロシージャを呼び出すために具体的にどうしたのか
もう3回目ですので、早速本題へ行きます。 Railsからプロシージャを呼び出すためには、本当に色々なハードルがありました。
※プロシージャの導入に至った詳しい経緯を知りたい方は前回までの記事を参照いただけますと幸いです
プロシージャをRails Wayで管理する
大前提となります。今後のメンテナンス性を考え、あくまでRails Wayに乗っかった形でプロシージャを管理していく方法を考えます。
最終的な変更内容については今回やった内容のまとめに記載していますので、結果だけ知りたい方は最下部を参照ください。
プロシージャをRailsの管理下に置く
Railsガイドに記載されている通り、ダンプフォーマットをsqlへ変更することで解決できます。
publicテナントだけをダンプ対象にする
Apartmentを使っていてダンプフォーマットをsqlへ変更した場合、db:migrateを実行すると、structure.sqlに全テナント分のテーブルがダンプされてしまう問題が発生します。
こちらについてはダンプする対象をpublicにすることで回避できます。
db:resetを動くように修正する
schema_search_pathが指定されていてダンプフォーマットをsqlへ変更している場合、以下のissueにある通りdb:resetが動きません。解決策としては、issue中に記載のある通り、structure_dump_flagsを指定することで回避できます。
Apartmentの新規テナント作成時にプロシージャを適用する
以下のREADMEに記載されている通り、use_schemasとuse_sqlを適用することで新規テナント作成時にプロシージャが適用されます。
余談
上記READMEを読んでいると気になる一文があります。
this option doesn't use db/structure.sql, it creates SQL dump by executing pg_dump
ソースコードを追いかけると、確かにstructure.sqlを使わずに、pg_dumpをApartmentの中で実行しています。(次回以降にお話ししますが、ここの辺りで問題が発生してapartmentに対してPRを出すことになりました)
db:migrateでプロシージャを登録する
ここでようやくプロシージャをRailsで登録できるようになります。
シンプルに以下で実行可能です。
def change execute <<~SQL CREATE PROCEDURE sample_procedure() AS $function$ BEGIN -- 処理を書く END; $function$ LANGUAGE plpgsql; SQL end
プロシージャの管理方針を考える
今回作成するプロシージャは弊社の根幹のロジックであり、今後のレポート拡充などやパフォーマンス改善で更に手を加えることが既に見えています。そして、改修の都度migrationを書いていては、どこにどのプロシージャがあるかを確認できず、管理が非常に煩雑になる未来が見えます。
上記などを考慮した結果、弊社ではプロシージャのソースコードを管理しやすいようにプロシージャ用のディレクトリを作成して、その中でプロシージャを管理するようにしました。
このようにmigrationを実行するイメージです。
def change execute File.read('db/procedure/sample_procedure.sql') end
ただ、そうすると、migrationを書き忘れたりlocalのdb:migrateの実行タイミング次第では登録されるプロシージャの内容が個々人で変わったりなど、色々な危険があるのではないかと気付き、最終的には、db:migrateを実行する度にプロシージャの最新ソースを登録するようなrakeを別途作成することとしました。
lib/tasks/procedure.rake
def install_all_procedures Dir.glob(Rails.root.join('db/procedure/*.*')).each do |file| ActiveRecord::Base.connection.execute File.read(file) end end task 'db:procedure' => :environment do puts 'Re-install procedures..' install_all_procedures Apartment::Tenant.each do puts "At Tenant: #{Apartment::Tenant.current}" install_all_procedures end if ActiveRecord::Base.dump_schema_after_migration Rake::Task['db:schema:dump'].invoke end end Rake::Task['apartment:migrate'].enhance do Rake::Task['db:procedure'].invoke end
rakeの中身の説明
簡単にrakeファイルの仕様を解説をします。
まず、Apartmentの仕様として、db:migrateを実行すると内部でapartment:migrateが実行されます。
今回の場合、apartmentのmigrationが終わった後にプロシージャの登録を行いたいので、Rake::Task['apartment:migrate'].enhance
としています。
install_all_procedures周りの処理は全テナント(publicスキーマ含む)に対してdb/procedure/
配下の全ファイルを実行していくだけなので割愛するとして、Rake::Task['db:schema:dump'].invoke
については、上記のプロシージャ登録をstructure.sqlの取得タイミングの後に実行しているため、再度dumpしてあげる必要があり、このような記述となっています。
結果として、db:migrateを実行するとdb/procedure/
配下の全ファイルが常に実行されるようなrakeファイルとなっています。
つまり、db:migrateを行うたびに毎回procedure配下のファイルが実行されてしまうため、各プロシージャの先頭ではDROP-CREATEの形での記述をしてあげる必要があります。
DROP FUNCTION IF EXISTS sample_function; CREATE FUNCTION sample_function() --以下略
登録したプロシージャを呼び出す
プロシージャの呼び出し自体は非常に簡単です。
# procedure ActiveRecord::Base.connection.execute('CALL sample_procedure()') # function ActiveRecord::Base.connection.execute('SELECT sample_function()')
※パラメータを渡す際にはsanitize_sqlなどを行う必要がありますので、ご注意ください。
今回やった内容のまとめ
以下のような形となりました。
application.rb
class Application < Rails::Application config.active_record.schema_format = :sql config.active_record.dump_schemas = 'public' # MEMO: db:resetが動くように調整 # NOTE: https://github.com/rails/rails/issues/38695 ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags = ['--clean', '--if-exists'] end
apartment.rb
Apartment.configure do |config| config.use_schemas = true config.use_sql = true end
We're hiring!
DIGGLEでは、必要に応じてライブラリのソースを読み解いて問題解決に挑む開発メンバーを募集しています!少しでも興味があれば、ぜひ下記採用サイトからエントリーください。
Meetyによるカジュアル面談も行っていますので、この記事の話をもっと聞きたい!という方がいらっしゃいましたら、お気軽にお声がけください。