カバー画像

[rails] scenicのviewの更新を簡単にするgemを作った

鎌倉と申します。本稿はRuby on Rails Advent Calendar 2023の13日目の記事です。


皆さんはscenic使っていますか?scenicはrailsのデータベースのviewをマイグレーションファイルで管理できるようにするためのgemで、大規模なrailsの開発をやったことがある人なら触れたことがある人も多いんじゃないかと思います。

今関わっているお仕事でもscenicを利用しているのですが、ヘビーに利用しすぎると、ある問題が発生します。

それが「マイグレーションファイルを書くのめんどくさすぎ問題」です (いま名づけました)。

普通にscenicを使っている分には特に困ることはないのですが、viewがviewに依存するような状態が複雑化するとこの問題が発生します。

具体的な例を見てみましょう。例えば、以下のようなviewが存在する状況を考えます。

  • first_results : 何にも依存していない親のview (バージョンは仮に1とする)
  • second_results : first_results に依存するmaterialized view (バージョンは仮に3とする)
  • third_results : first_resultssecond_results に依存するview (バージョンは仮に2とする)

それぞれのSQLは以下のようになります。

-- first_results
SELECT 'foo' AS bar;

-- second_results
SELECT * FROM first_results;

-- third_results
SELECT * FROM first_results UNION SELECT * FROM second_results;

仮にこのとき first_results を更新したくなったとしましょう。ここは通常通り bin/rails g scenic:view result_resultsを使ってマイグレーションファイルを生成します。

$ bin/rails generate scenic:view:cascade first_results
      create db/views/first_results_v02.sql
      create db/migrate/20231213000000_update_first_results_to_version_2.rb

このとき生成されるマイグレーションファイル (つまり db/migrate/20231213000000_update_first_results_to_version_2.rb) の中身は初期状態では以下のような感じです。

class UpdateSearchResultsToVersion2 < ActiveRecord::Migration
  def change
    update_view :first_results, version: 2, revert_to_version: 1
  end
end

このとき、 bin/rails db:migrate を行うとどうなるでしょうか?残念ながらマイグレーションは失敗します。以下のようなエラーが出るからです。

== 20231213000000 UpdateSearchResultsToVersion2: migrating =============
-- update_view(:first_results, {:version=>2, :revert_to_version=>1})
rake aborted!
StandardError: An error has occurred, this and all later migrations canceled:

PG::DependentObjectsStillExist: ERROR:  cannot drop view first_results because other objects depend on it
DETAIL:  materialized view second_results depends on view first_results
view third_results depends on materialized view second_results
HINT:  Use DROP ... CASCADE to drop the dependent objects too.

「いま更新しようとしてるviewを消そうとしたけど、このviewにmaterialized viewが依存して消せない。しかもそのmaterialized viewにも別のviewが依存しとるよ」と言ってきています。そのため、マイグレーションファイルを以下のように書き直す必要があります。

class UpdateSearchResultsToVersion2 < ActiveRecord::Migration
  def change
    drop_view :third_results, revert_to_version: 2
    drop_view :second_results, revert_to_version: 3, materialized: true

    replace_view :first_results, version: 2, revert_to_version: 1

    create_view :second_results, version: 3, materialized: true
    create_view :third_results, version: 2
  end
end

このように「更新しようとしているviewに依存するviewを事前に落として、あとで作り直す」という作業を行わなければなりません。

viewにviewが依存する状況が1段2段程度だったらいいのですが、3段や4段、あるいはもっとたくさんネスト場合を想像してみてください……。依存関係とviewのバージョンを調べて書き出すだけでも結構な負担になってしまいます。

そこで今回リリースしたのが scenic-cascade というgemになります。

このgemは、マイグレーションファイルの生成を bin/rails g scenic:view から bin/rails g scenic:view:cascade に変えるだけで、依存関係が記述済みの(drop_view / create_viewが書かれた状態の) マイグレーションファイルを作ってくれます。便利ですよね?ぜひ使ってみて欲しいです!

注意点として、まだリリースしたばかりなので、以下の問題があることをご了承ください。

  • indexの再作成は未サポートです。仮にindexが存在するmaterialized viewに対して drop_view が行われた場合にはindexが消えてしまうので、 create_view の後に create_index を手で追記する必要があります
  • 現状ではpostgresqlのみサポートしています

というわけで自作したgem scenic-cascade の宣伝でした。初めてのgemへのpushやsteepを使った型検査なども導入できて面白かったです。


余談ですが、scenicには「依存するviewをまとめて落として更新する cascade オプション」の導入が検討されたことがあり、PRまで作成されていました。

少なくともpostgresqlには DROP VIEW ... CASCADE というオプションでこれが実現できるので、この cascade オプションがサポートされれば今回作った scenic-cascadeも不要になるのですが、残念ながらマージされることはありませんでした (コメントによれば、依存関係の解決が正しく行える自信がないみたいな事情のようです)。


14日の記事はalienさんの『数値が三桁区切りで表示される input タグをつくる』です!楽しみに待ちましょう〜