RDBMSのVIEWを使ってRailsのデータアクセスをいい感じにする【銀座Rails#10】|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社エンジニアの「?」を「!」に。開発2019.06.21RDBMSのVIEWを使ってRailsのデータアクセスをいい感じにする【銀座Rails#10】morimorihogeシェアツイートブックマークLINEmorimorihogeです。@リンクアンドモチベーションにて発表させていただきましたので、その内容をまとめたいと思います。※当日は時間が足りなくて端折ってしまう部分もあるかと思うので、その補遺としての意味合いもあります注1:本記事では分かりやすさのためにTABLEやVIEWなどのSQL予約語は大文字で記載していきます。11環境で検証していますおさらい:VIEWについて本記事におけるVIEWはRDBMSにおけるVIEWの話で、ActionViewではありません。VIEWについて使ったことがない人もいるかなと思うので、最初に軽く解説します。VIEWは一言で言ってしまえばSELECT文の実行結果に名前を付けて、TABLEと同じようにアクセスできるものです。以下のようなproductsテーブルとデータがあったとします。[query]構文を使います。products;こうして作られたVIEWは、通常のTABLEと同じようにSELECTすることができます。rows)VIEWの機能は基本的にはこれだけです。VIEWはSQL標準機能としてSQL89から存在しており、一般的なRDBMSであれば使えます。PostgreSQLであれば\dでTABLEやSEQUENCEと一緒に一覧できます。Mapper)であるActiveRecordでは、実は接続しているDBに既に作られたVIEWであれば何も考えずに参照することができます。systemで管理する場合はGemの導入が必要です#save系メソッドなどの更新系については可能なVIEWと不可能なVIEWがあり、更新可能なカラムもVIEWの定義によって変わります。VIEWのドキュメントが詳しいです。※他のRDBMSでも基本的な条件は近いと思いますが、実装依存もあると思うので実際に使う場合は利用しているRDBMSのドキュメントを参照してください。ここでは、VIEWはSELECT文を実行する観点においてTABLEと同一視することができるということを抑えてくれればOKです。AciveRecordでVIEWをいい感じに使うでは、実際に具体的な事例を用いてVIEWを使った事例を紹介していきます。説明のためにusersテーブルを定義します。一般的なメールアドレス+パスワードでログインするようなECサイトのようなものを想像してください。endend要件1:有効なユーザーだけを取得したいusers.confirmed_atでメール認証を行うことが読み取れますが、逆に言えばメール認証されていないユーザーはまだ有効なユーザーとして扱いたくないわけです。NULLなユーザーだけを取得したいとします。Wayに従うのであれば、scopeを利用するのが妥当でしょう。}endここではconfimedscopeを定義しました。chainを使って実装すれば良いわけですね。chainはアプリケーションが複雑になっていくと以下のような問題が発生しがちです。Railsのdefault_scopeは使うな、絶対(翻訳)では、VIEWを使った実装を見てみます。confirmed_usersというconfirmedなユーザーだけを取得するVIEWを作成します。ApplicationRecordendあとは、データを取り出すときにUserモデルではなくConfirmedUserモデルから取得すればOKです。ConfirmedUser.allこのように、scope代わりのfilterとしてVIEWを使うのは、以下のような特徴があります。引数付きscopeなど、複雑なアプリケーションロジックが必要なscopeは置き換えが難しい次行きましょう。要件2:公開情報用のカラムを追加し、ユーザーごとにどの情報を公開できるか設定できるようにしたいSNS系のサービスではよくあるやつですね。ここでは、以下の3つのカラムを定義し、それぞれis_public_#{FIELD_NAME}というboolean値を持ち、それがtrueであれば公開OKとします。FacebookのID追加のmigrationは以下のようになりました。公開用データはテーブル分けた方がいいんじゃないかとか、正規化してもいいんじゃない?とかそういう話もあると思いますが、ここでは一旦置いておきます。Wayに乗るのであれば、以下のようなケースが考えられます。書いていくとても事故りやすいのでオススメしません。品質は無視して作り捨てたいプロトタイピングなど以外ではやらない方が良いでしょう。ModelをViewに渡すときにModel本体ではなく、Decorator(またはPresenter)系のGemを利用してラップされたものを渡すDraperやActiveDecoratorを使う想定になります。プロジェクト全体でこうしたGemが利用されていれば「Modelを直接Viewに渡さない」というルールが徹底されているので問題ないと思いますが、使われていないプロジェクトで新たに導入したり、Decoratorを使ったことのないメンバーが加入したりするとDecoratorを通し忘れて生のModelを渡してしまうといったミスが発生する危険性があります。すると見えてしまうため、重要な個人情報などはアプリケーションログやエラーメッセージも含めて注意しないと、思わぬところから非公開のつもりだったカラムが見えてしまう、という可能性はあります。VIEWを使って解決してみます。VIEWを作成します。confirmed_users;突然CASE文が出てきましたが、これによって各カラムが「is_public_#{FIELD_NAME}がtrueなら#{FIELD_NAME}の内容を表示、falseならNULLを返す」ようにしています。することで、DBからデータを取ってきた時点で既にマスクされたデータを作ることができます。Modelを作成すればOKです。ApplicationRecordend取り出す際は何も考えず出せば良いですね。%></dd></dl>View側は実際には設定されてなければ非表示にしたりなどするかと思いますが、少なくとも表示・非表示のロジックをViewにもControllerにも書かずにマスクできています。このように、特定用途向けにデータを加工したVIEWを使う方法には、以下の特徴があります。加工前の生データはRails側のメモリにすら乗らないので、アプリケーションコードのミスで漏れるということが(参照するModelを間違えない限りは)ない。大規模CMSなどでデータ管理用のRailsアプリとフロント用のRailsアプリが分離している場合などに特にうまく働きます(フロントアプリには表示して良いVIEWしか見せないことができる)。Wayに則った名前に変換して利用することができる堅い業務システムなどではD01SHIMEIのようなとても分かりにくい名前がついていることがありますが、ASでリネームしてやることで、Railsの世界ではまともな名前を使うことができる。同様のことがしたければalias_attributeでもできるので、DB(SQL)レベルで名前を隠蔽したいかどうかはプロジェクトによって検討の余地がある。Administrator)化する)さて、最後にかなり強力なもの(だと僕が思っている)を紹介します。要件3:関連を含めた複雑なテーブル構造の中で、様々なテーブルの項目を横断して検索したい今度はusersテーブルの外に目を向けます。実はこのサイトはECサイトで、以下のようなテーブルを持っているとします。)さらにそれらの中間テーブルを含めて以下のような図になっているとします。したテーブル構造の中で「ユーザーごと、商品ごとに何をどれだけ買ったのか集計したい」「高額商品を買ってくれているユーザーを一覧したい」「ユーザー・カテゴリごとの商品販売数を一覧したい」などを要望されたとしましょう。Wayでは複数テーブルをまたいだ条件を記述するのには#joinsや#mergeを駆使して集計していくことになるのではないかと思います。乱暴に一旦丸っとメモリに展開して#eachや#selectで頑張るということはできないわけではありませんが、とても重いですしかなり早い段階で破綻することが多いです。Objectを作り始めたり集計用のクラスを作ったりし始めるのは割とあるあるなのではないでしょうか。まとめ他にも、検索・ソート・絞り込みではransackなんかが使えるとお手軽なのですが、複雑なJOINやJOIN先のテーブルで絞り込みを利用するにはransackable_scopesなどを使ってゴリゴリカスタマイズしないといけなくなったりします。お手軽ではなくなりとても辛い。ここでVIEWの出番です。ここのキモはActiveRecordで単純に集計しやすいVIEWを作ることです。以下のようなVIEWを作ります。products.id;VIEW定義だとわかりにくいので、適当なデータを突っ込んでSELECTしたものが以下です。rows)ユーザーごと、商品ごとに販売数・総額を出し、タグやカテゴリはARRAY型の中に入れたデータになっています({}で囲まれた部分)。VIEWから「高い商品」カテゴリに属する商品の売上をユーザー別に集計して昇順に取り出すには、以下のようにします。VIEWにしかアクセスしないので、whereやgroupなどがとてもシンプルになっています。いうわけで、多数のテーブルをまとめた検索用VIEWを使うことには以下の特徴があります。利点VIEWさえできていれば、内部は複雑なクエリでもRailsエンジニアは通常のテーブルと同じように検索・参照できるつぎはぎ開発で複雑怪奇なテーブル構造になってしまったようなケースでは特に有効1テーブルを前提として作られているGemはそのまま使えるので、Railsのエコシステムを使うことができる純粋にデータアクセス部分の開発難易度が下がるので、開発経験が浅めのエンジニアでも開発を進められるようになる取得したオブジェクトはAR::Relationなので、ActiveRecord拡張のGemがそのまま使えるAR::Base.executeなどの生SQLから取得したデータと違い、普通のActiveRecordの書き方で利用できる欠点VIEWを作るためにそれなりにSQLに長けたエンジニアが必要になるVIEWの定義によっては重いクエリになるので、DBサーバーの負荷が想定外に増えることがある以上、3ケースに分けて代表的と思われるユースケースを紹介してみました。実際にVIEWを使った実装を進めていく中で気を付けないといけない点をまとめます。定義済みのVIEWを参照する分には追加Gemは不要ですが、VIEWの作成や更新をmigration管理したい場合には標準ではできません。Scenicを使うことでVIEWもmigration管理できるようになるため、実質必須になると思います。Scenicの扱い自体はそれほど難しくないので、公式READMEを読めばわかると思います。*で記述されたVIEWはVIEWを作成した瞬間のカラムリストに展開されるので、元テーブルのカラムが変更された場合はVIEWも更新する必要があります。VIEW作成文は生SQLなので、Railsのデータ変換層を通らない具体的にはVIEW定義内で時刻系ロジックを記述する場合にTimezoneの扱いに注意が必要です。Railsの世界ではTimeオブジェクトを渡すように作る限り問題ありませんが、VIEW定義するときだけはRailsの世界から離れてRDBMSの目線で考える必要があります。アプリケーションロジックがVIEWとRailsに分散するこれが一番問題になると思います。Wayならscopeで実装するようなものもVIEW定義で書くことができるため、乱用するとVIEWとアプリケーション側にロジックが分散します。プロジェクトの開発責任者は何をVIEWに出して何をRails側のロジックコードとして書くべきなのかを考えながらバランスを取っていく必要があるでしょう。参照専用のVIEWであればreadonlyにすべきScenicのREADMEにも記載がありますが、以下のようにreadonly?メソッドがtrueを返すように定義してやると、誤った#saveを防止できます。Moriまとめここに書ききれなかったユースケースもたくさんありますが、VIEWを知らない人に知ってもらうにはこれくらいのボリュームが良いかなと思うので、ここまでにしたいと思います。本記事の反響があれば続きを書こうと思いますので、Twitterやはてブ等でコメントいただければ幸いです。Storageを使ってはいけない(翻訳)#Tags#会社紹介#自社サービス#勉強会#Rails#PHP#デザイン#実績#週刊RailsウオッチCONTACTお気軽にご相談ください。(翻訳)CONTACTTechRachoでは、パートナーシップをご検討いただける方からのご連絡をお待ちしております。お気軽にご意見・ご相談ください。