1

Rails 5.0.0.1 ATM を使用していますが、DB リクエストの数を最適化する際に ActiveRecord 関係の問題に遭遇しました。現在、モデル A ('Orders' としましょう)、モデル B ('OrderDispatches')、モデル C ('Person')、モデル D ('PersonVersion') があります。

テーブル 'people' は 'id' と 'hidden' フラグのみで構成され、残りの人物データは 'person_versions' ('name'、'surname'、および学名など、時間の経過とともに変化するもの) に格納されます。

すべての Order には、DB に注文を記録した人に関する「recoming_person_id」があり、すべての OrderDispatch には、注文を配達した人に関する「dispatching_person_id」があります。また、Order と OrderDispatch には作成時間があります。

1 つの注文で複数の発送があります。

したがって、直接的な関係は次のとおりです。

has_many :receiving_person, through: :person, foreign_key: "receiving_person_id", class_name: 'PersonVersion'

しかし、ディスパッチに応じて注文をリストすると、N + 1 の状況に対処する必要があります。なぜなら、(Order/OrderDispatch の作成日に従って) 正確な PersonVersion をすべての receive_person_id および dispatching_person_id に対して見つけるために、別のリクエストを行っているからです。

SELECT *
FROM person_versions
WHERE effective_date_from <= ? AND person_id = ?
ORDER BY effective_date_from
LIMIT 1

初め '?' Order/OrderDispatch の作成日と 2 番目の「?」受発注者IDです。

このクエリを使用して、Order/OrderDispatch 作成時の正確な個人データを取得しています。

生のSQLでサブクエリ(またはOrderが1つのリストにOrderDispatchesを伴うため、サブクエリ)を使用してクエリを作成するのはかなり簡単ですが、ActiveRecordを使用してそれを行う方法がわかりません。

これは私が来た限りであるため、カスタム has_one リレーションを記述しようとしました:

has_one :receiving_person. -> {
    where("person_versions.id = (
        SELECT id 
        FROM person_versions sub_pv1 
        WHERE sub_pv1.date_from <= orders.receive_date 
            AND sub_pv1.person_id = orders.receiving_person_id 
        LIMIT 1)")}, 
    through: :person, class_name: "PersonVersion", primary_key: "person_id", source: :person_version

これを受取人または発送人にのみ使用すると機能します。結合された注文と order_dispatches テーブルに対してこれをeager_loadしようとすると、「person_versions」の1つをエイリアスする必要があり、カスタムwhere句ではそうではありません(エイリアスされるかどうかを予測する方法はなく、両方の方法で使用されます) .

別のアプローチは次のようになります。

has_one :receiving_person, -> {
    where(:id => PersonVersion.where("
        person_versions.date_from <= orders.receive_date 
             AND person_versions.person_id = orders.receiving_person_id").order(date_from: :desc).limit(1)}, 
through: :person, class_name: "PersonVersion", primary_key: "person_id", source: :person_version

サブクエリにあり、シンボル ':id' を使用すると、生の SQL が注文と order_dispatches に結合された person_versions テーブルの正しいエイリアスを取得するため、生の 'person_versions' は問題ありませんが、person_versions.id の 'equals' ではなく 'IN' を取得します。 xx サブクエリと MySQL は、IN/ANY/ALL ステートメントで使用されるサブクエリで LIMIT を実行できないため、ランダムな person_version を取得します。

したがって、TL;DR 'has_many through' を 'has_one' に変換する必要があります。カスタムの 'where' 句を使用して、元のレコード作成よりも日付が古いものの中から最新のレコードを探します。

編集: 簡略化のための別の TL;DR

def receiving_person
    receiving_person_id = self.receiving_person_id
    receive_date = self.receive_date
    PersonVersion.where(:person_id => receiving_person_id, :hidden => 0).where.has{date_from <= receive_date}.order(date_from: :desc, id: :desc).first
end

これを「eager_load」できるように、このメソッドを「has_one」リレーションに変換する必要があります。

4

1 に答える 1

1

ビジネスドメインと競合しているため、スキーマを変更します。再構築すると、n + 1の問題が軽減されます

class Person < ActiveRecord::Base
  has_many :versions, class_name: PersonVersion, dependent: :destroy
  has_one :current_version, class_name: PersonVersion
end

class PersonVersion < ActiveRecord::Base
  belongs_to :person, inverse_of: :versions, 

  default_scope ->{
    order("person_versions.id desc")
  }
end

class Order < ActiveRecord::Base
  has_many :order_dispatches, dependent: :destroy
end

class OrderDispatch < ActiveRecord::Base
  belongs_to :order
  belongs_to :receiving_person_version, class_name: PersonVersion
  has_one :receiving_person, through: :receiving_person_version
end
于 2016-12-07T19:45:01.510 に答える