見る方法:OrmsとSQL見解


この記事では、データベース、Ruby、Railsがどのように動作するかを一般的に理解していますActiveRecord データベース関係をRailsアプリケーションのコンテキストでRubyオブジェクトにマップするライブラリ.チェックアウトexample repository アクションでそれらを見るには!(注: JavaScriptの例を設定しましたhere 興味があるのであれば、SQLビューのファーストクラスのサポートを持っているJMS ORMを見つけていません.

凝集悪化


Webアプリケーションは、しばしば「要約」または集計されたデータを表示します.そして、それはいくつかのデータベーステーブルの向こう側から情報を得て、計算をするかもしれません.ORM(Object Relational Mapping)データベースモデリングライブラリは、一度に1つのテーブルから読むように設計されています.このパターンは何トンものユースケース(勝利のための基本的なCRUD)に有用ですが、我々はデータを集約しようとするとき、複雑なアプリケーションコードと/または高価なDB質問のリスクを実行します.SQLビューでは、データ集約ロジックをデータベースにプッシュしながら、クエリの数を減らすことができます.
たとえば-あなたが獣医を助ける彼らのクライアントと通信するアプリケーションを作成するために雇われている想像してください.このようなワークフローは、サーバーエンドポイントを作成した場合、これに慣れているかもしれません.
user = User.includes(:pets).find(1)

render json: {
  user: {
    id: user.id,
    name: "#{user.first_name} #{user.last_name}"
  },
  pets: user.pets.map do |pet|
    {
      id: pet.id,
      name: pet.name
    }
  end
}

上記のコードは非常に簡単です:それはユーザーをロードし、彼らのペットをロードし、フロントエンドが期待する形式でデータをシリアル化します.私たちは2つの質問をしています、しかし、彼らは簡単です(そして、うまくいけば)速く、それは妥当な予想です.
早送りは数週間、我々は新しい要件を持っている.ユーザーは、彼らはあなたと登録しているペットを見ることができますが、彼らはまた、“一目で”次の予定であることを知りたい!したがって、エンドポイントを更新します.
user = User
  .includes(:pets)
  .find(1)

# Pre-load the user's pets' appointments that are scheduled for the future
upcoming_appointments = user
  .appointments
  .order(date: :asc)  
  .where('date >= ?', Time.current)

render json: {
  user: {
    id: user.id,
    name: "#{user.first_name} #{user.last_name}"
  },
  pets: user.pets.map do |pet|
    # Use Array#find to return the first of this pet's appointments
    next_appointment_date = upcoming_appointments.find do |appt|
      appt.pet_id == pet.id
    end

    {
      id: pet.id,
      name: pet.name,
      next_appointment_date: next_appointment_date
    }
  end
}
正直なところ、これはまだ悪くない.我々は別のクエリを追加しました-予定のためにこの時間-いくつかの順序とフィルタリングロジックで.我々はまた、それぞれの所有者のペットのための最初の約束を引き出すために我々の連載ステップでいくつかのループロジックに追加している.それは少しuntidyですが、ねえ、それは動作します.
個人的には、このコードのすべてをアプリケーションコードでエンコードするのは好きではありません.それは私に乱雑に感じ、それは非常に簡単に何かに忍び込むように簡単です.例えば、代わりにこれを実行してみましょう.
render json: {
  user: {
    id: user.id,
    name: "#{user.first_name} #{user.last_name}"
  },
  pets: user.pets.map do |pet|
    next_appointment_date = user
      .appointments
      .order(date: :asc)
      .find_by('date >= ?', Time.current)
      &.date

    {
      id: pet.id,
      name: pet.name,
      next_appointment_date: next_appointment_date
    }
  end
}
🙀ああ!誰かが私たちのシリアライズステップにクエリをスナックし、これは最初に(任意のユーザーあたり1つまたは2つのペットを扱っているとき)、任意のアラームベルを鳴らない可能性がありますが、何が起こるときに地元の動物シェルターはあなたのクライアントの1つと100人以上のペットに登録されますか?ページ負荷あたり100問以上.
これは明らかに工夫された例ですが、私はN + 1クエリがサービスオブジェクトの中に隠れていて、ファイルを別々に隠している、より複雑なケースに遭遇しました.個人的には、この種のロジックをデータベースに入力するのが好きです.それはSQLビューが来るところです!

SQLビューとは


その最も基本的なSQLビューの私の精神モデルは“テーブルのように動作するデータベース内の保存されたクエリです.”があるa lot more to it それよりも、この単純な理解は、長い道のりを得ることができます.例えば、私のデータベースで次の文を実行したならば
CREATE VIEW silly_users AS
  SELECT 
    id, 
    first_name,
    first_name || ' Mc' || first_name || 'erson' AS silly_name
  FROM users;
More on the || concatenation operator
このビューから結果を検索するには、テーブルに対して同じ構文を使用します.
# SELECT * FROM silly_users;
  id   | first_name |       silly_name       
------------+------------+------------------------
     1 | Melissa    | Melissa McMelissaerson
     2 | Colleen    | Colleen McColleenerson
     3 | Vince      | Vince McVinceerson
     4 | David      | David McDaviderson
     5 | Dennis     | Dennis McDenniserson
...etc
このビューは、テーブルのようにかなり動作するので、それは本当に素敵なORMと遊ぶことができます.我々は、ビューバックモデルを作成することができます!
silly_user = SillyUser.find(1)
silly_user.silly_name // => 'Melissa McMelissaerson'
Rubyを使用しているなら、私はScenic gem by ThoughtBot , ActiveRecord ORMを使用するプロジェクトには、ビューのバージョン管理やその他の便利な機能が含まれます.

新しい展望


我々のアプリケーションコードでそれのために質問する代わりに、我々が望むデータをつかむために、データベース表示を書いてみましょう(more here on using DISTINCT ON with GROUP BY ):
-- Grabs one record per pet, returning the earliest future appointment date 
CREATE VIEW pets_with_upcoming_appointments AS
SELECT DISTINCT ON (pets.id)
  pets.id AS id,
  users.id AS user_id,
  pets.name AS name,
  MIN(appointments.date) AS next_appointment_date
FROM users
INNER JOIN pets
  ON user_id = users.id
LEFT JOIN appointments
  ON pets.id = pet_id
  AND appointments.date >= CURRENT_DATE
GROUP BY (
  users.id,
  pets.id,
  pets.name
);

すごい!現在、このビューから読み取ることができます.
# SELECT * FROM pets_with_upcoming_appointments;

 user_id | pet_id | pet_name |  next_appointment_date   
--------------+--------+----------+-----------------------
       1 |      1 | Flannery |   2018-11-22 13:00:00
       2 |      2 | Porkchop |   2018-11-22 16:30:00
       2 |      3 | Gravy    |   2018-12-01 09:00:00
       3 |      4 | Magnus   | 
       4 |      5 | Huey     |   2018-12-15 10:45:00
       4 |      6 | Duey     |   2018-12-15 10:45:00
       4 |      7 | Louie    |   2018-12-15 10:45:00

# SELECT * FROM pets_with_upcoming_appointments WHERE user_id = 1;

 user_id | pet_id | pet_name |  next_appointment_date   
--------------+--------+----------+-----------------------
       1 |      1 | Flannery |   2018-11-22 13:00:00
ここでビューを設定したので、以前に述べた景色の宝石を使ってマイグレーションを作成し、データベースにバックアップされたORMモデルにフックします.
class PetWithUpcomingAppointment < ActiveRecord::Base
  self.table_name = 'pets_with_upcoming_appointments'
end
我々の見解があるのでuser_id フィールドでは、ユーザモデルでのリレーションをフックするのは些細なことです.
class User < ActiveRecord::Base
  has_many :pets
  # wooooot
  has_many :pet_with_upcoming_appointments
  has_many :appointments, through: :pets
end
さて、アプリケーションコードをフェッチしてデータをクリーンアップできます.
user = User
  .includes(:pet_with_upcoming_appointments)
  .find(params[:id])

render json: {
  user: {
    id: user.id,
    name: "#{user.first_name} #{user.last_name}"
  },
  pets: user.pet_with_upcoming_appointments.map do |pet|
    {
      id: pet.id,
      name: pet.name,
      next_appointment_date: pet.next_appointment_date
    }
  end
}
悪くない!私たちは、2つの質問とコントローラの順序/日付比較論理に戻っています💪 さらに良いことに、我々はクエリロジックを複製することなく、我々のアプリケーションの他の部分でこのモデルを再利用することができます.これは、より複雑な集約を実行し、別のテーブルからデータを取得するときに本当に輝き始める.

SQLビューの落とし穴



私が明らかにファンである間、あなたがあなたのアプリケーションで意見を考え始めるとき、あなたが注意しなければならない若干のものがあります:

熱狂的な啓示


これは、“私はハンマーを持って、すべての爪の別の面です!”問題あなたが最初に彼らと一緒に遊んで起動すると、自分自身をあまりにも多くのロジックを押して見つけることができます(それは離れて抽象化され、見つけるのは難しい)データベース.あなたは、あなたのアプリケーションが提供するものを変更する必要があるたびに、新しいビューを更新または作成する必要があるので、移行の多くを実行する必要があります.😬 あなたが何かをあなたが見ているものに変える前に、あなたが得ている利益がトレードオフの価値があるならば、あなた自身に尋ねてください.

カスケードビューの多すぎるレベル


ビューは一般的にテーブルからの結果で構成されていますが、別のビューからデータをプルするビューを作成することもできます.これは良いアイデアのように思えるかもしれません.しかし、実際には、私はそれがより困難なすべてのレベルでビューを更新することを見てきました.また、1つのビューの変化が離れていくつかのレベルからデータにプルする別の1つに影響を与える方法を把握しようとして壁を自分自身を駆動することができます😵 これは本当に最初の点と並行して問題である.

伝搬非効率


SQLクエリのプロファイリングに慣れているならEXPLAIN/EXPLAIN ANALYZE 問題を見つけるLEFT JOIN appointments 上のビューでは、あなたは😂), これはそれらのスキルを使用する良い時間です!If indexes , hash joins , and sequential scans あなたの頭を回転させる、あなたは非常にゆっくりと一度あなたのデータベース内のより多くのレコードを取得を開始実行して一見単純なクエリで終わる可能性があります.あなたが慎重でないならば、非効率的な質問はあなたのアプリの機能のトンを支持することに終わることができます.そして、全体的に低迷したパフォーマンスに至ります.少なくとも、それを使用しているプロセスの速度にあるビューを導入する影響をチェックしてください.

今何?


あなたが興味を持っているならば、我々がより多くを学ぶために、私が推薦する若干の資源(そして、私はこのポストを通して参照された関連)です!長く生き、繁栄し、データベースではなく恐怖を🖖
  • Example Repo
  • What is a Relational Database View?
  • How to Use Views
  • Ordering Within a SQL Group By Clause
  • Depesz's tool for explaining EXPLAIN output
  • Intro to Indexes
  • Hash Joins
  • Sequential Scans
  • Index Scans