7

Rails3 アプリのスコープに基づいてアソシエーションを熱心にロードしようとしましたが、解決策が見つかりませんでした。

私のアプリには次のモデルがあります:

class Project
 has_many :entries
 has_many :to_dos

class ToDo
 has_may :entries
 has_many :tasks
 belongs_to :project

class Task
 has_many :entries
 belongs_to :to_do

class Entry
belongs_to :project
belongs_to :to_do
belongs_to :task

# options format: {:from_date=>(Date.today-1.week), :to_date=>(Date.today+1.week), :user_id=>60}
scope :filtered_list, lambda { |options|
  condition = options[:user_id].nil? ? "true" : "user_id = #{options[:user_id]}"
  condition += options[:from_date].nil? ? "" : " AND entry_date >= '#{options[:from_date]}'"
  condition += options[:to_date].nil? ? "" : " AND entry_date <= '#{options[:to_date]}'"
  where(condition)
}

また、projects#index には、ユーザーのすべてのプロジェクトを取得する次のコードがあります。

@projects = current_user.projects.includes(:entries, :to_dos =>[:entries, :tasks => :entries])

ユーザーのすべてのプロジェクトを取得し、関連付けを熱心に読み込みます。したがって、プロジェクト内のすべてのエントリを取得するために次のループを実行すると、新しいクエリは発生しません。

def all_entries(options)
  entries = self.entries
  self.to_dos.each do |d|
    entries += d.entries
    d.tasks.each do |t|
      entries += t.entries
    end
  end
end

この熱心な読み込みはすべてのエントリを取得するため、実際に必要な量よりもデータが多すぎます。そのため、熱心にロードされたエントリにいくつかの条件を適用しようとしましたが、解決策が見つかりませんでした。私は次のようなものを探していました:

@projects = current_user.projects.includes(:entries.filtered_list(options), :to_dos =>[:entries.filtered_list(options), :tasks => :entries.filtered_list(options)])

いくつかの条件を満たすエントリのみが読み込まれるようにします。

熱心な読み込みでスコープを使用することはできませんか? スコーピングと一緒にeagerloadingを使用するのを手伝ってください。

4

1 に答える 1

1

私の知る限り、このような含まれる関連付けにスコープを適用することはできません。ただし、一括読み込みクエリにのみ適用する条件を指定できます。そのため、少しリファクタリングすることで、スコープで現在定義している条件のみを作成するメソッドを作成できます。

def self.filter_by(options)
  condition = options[:user_id].nil? ? "true" : "entries.user_id = #{options[:user_id]}"
  condition += options[:from_date].nil? ? "" : " AND entries.entry_date >= '#{options[:from_date]}'"
  condition += options[:to_date].nil? ? "" : " AND entries.entry_date <= '#{options[:to_date]}'
  condition
end

またはもう少しルビー風:

def self.filter_by(options)
  conditions = []
  conditions << "entries.user_id = #{options[:user_id]}" unless options[:user_id].nil?
  conditions << "entries.entry_date >= '#{options[:from_date]}'" unless options[:from_date].nil?
  conditions << "entries.entry_date <= '#{options[:to_date]}'" unless options[:to_date].nil?
  conditions.join(" AND ")
end

次に、そのメソッドを熱心な読み込みにチェーンします。

@projects = current_user.projects.includes(:entries, :to_dos =>[:entries, :tasks => :entries].where(Entry.filter_by(options))

また、独立して必要な場合は、スコープで再利用します。

scope :filtered_list, lambda { |options| where(Entry.filter_by(options)) }

免責事項:これは実際のモデル定義でテストされていませんが、私が横たわっていたかなり同等のものでうまく機能します。

また、フィルター オプションが最終的にクライアント側から提供される場合、条件は SQL インジェクションに対して脆弱であることに注意してください。

Rails はバックグラウンドで JOIN を使用して関連データをロードするため、注意が必要です。これは良いこと (クエリ数が少ない) かもしれませんし、悪いこと (インデックス作成が最適でない場合) かもしれません。それがおそらくガイドがこれを言っている理由です:

Active Record では、結合と同じように熱心に読み込まれた関連付けに条件を指定できますが、代わりに結合を使用することをお勧めします。

于 2011-08-05T03:45:48.073 に答える