1

タイトルがややこしいですが、説明させてください。タイムスタンプが異なる複数のデータポイントを持つ Car モデルがあります。ほとんどの場合、最新のステータスの属性に関心があります。そのため、モデルには has_many のステータスがあり、最新のものに簡単にアクセスするための has_one があります。

class Car < ActiveRecord::Base
  has_many :statuses, class_name: 'CarStatus', order: "timestamp DESC"
  has_one :latest_status, class_name: 'CarStatus', order: "timestamp DESC"

  delegate :location, :timestamp, to: 'latest_status', prefix: 'latest', allow_nil: true

  # ...
end

ステータスが保持するものを理解するには:

loc = Car.first.latest_location   # Location object (id = 1 for example)
loc.name                          # "Miami, FL"

最新のロケーション ID が 1 のすべての車を検索する (連鎖可能な) スコープが必要だとします。現在、一種の複雑な方法があります。

# car.rb
def self.by_location_id(id)
  ids = []
  find_each(include: :latest_status) do |car|
    ids << car.id if car.latest_status.try(:location_id) == id.to_i
  end
  where("id in (?)", ids)
end

SQL を使用してこれを行うより迅速な方法があるかもしれませんが、各車の最新のステータスのみを取得する方法はわかりません。location_id が 1 のステータス レコードが多数存在する可能性がありますが、それがその車の最新の場所でない場合は、含めるべきではありません。

さらに難しくするには... 別のレベルを追加して、場所の名前でスコープできるようにしましょう。名前にアクセスできるように、位置オブジェクトとともにステータスをプリロードするこのメソッドがあります。

def by_location_name(loc)
  ids = []
  find_each(include: {latest_status: :location}) do |car|
    ids << car.id if car.latest_location.try(:name) =~ /#{loc}/i
  end
  where("id in (?)", ids)
end

これは、上記の場所と「miami」、「fl」、「MIA」などと一致します...これをより簡潔/効率的にする方法について何か提案はありますか? アソシエーションを別の方法で定義したほうがよいでしょうか? あるいは、SQL忍者のスキルが必要になるかもしれませんが、それは私には確かにありません.

Postgres 9.1 の使用 (Heroku cedar スタックでホスト)

4

1 に答える 1

2

わかった。私と同じようにpostgres 9.1を使用しているので、これを試してみます。最初の問題に最初に取り組む (最後のステータスの場所でフィルタリングする範囲):

このソリューションは、ここで説明されているように、分析関数に対する PostGres のサポートを利用します: http://explainextended.com/2009/11/26/postgresql-selecting-records-holding-group-wise-maximum/

以下は、必要なものの一部を提供すると思います(当然、「?」に興味のある場所IDを置き換え/補間します):

select * 
from (
  select cars.id as car_id, statuses.id as status_id, statuses.location_id, statuses.created_at, row_number() over (partition by statuses.id order by statuses.created_at) as rn 
  from cars join statuses on cars.id = statuses.car_id
) q
where rn = 1 and location_id = ?

このクエリは、 、 、 、およびタイムスタンプを返しますcar_id(status_idデフォルトlocation_idでは created_at と呼ばれますが、他の名前の方が使いやすい場合は別名にすることもできます)。

次に、これに基づいて結果を返すように Rails を説得します。おそらくこれで熱心な読み込みを使用したいので、find_by_sql はほとんどありません。.joinsただし、サブクエリに参加するために使用する、私が発見したトリックがあります。おおよそ次のようになります。

def self.by_location(loc)
  joins(
    self.escape_sql('join (
    select * 
    from (
      select cars.id as car_id, statuses.id as status_id, statuses.location_id, statuses.created_at, row_number() over (partition by statuses.id order by statuses.created_at) as rn 
      from cars join statuses on cars.id = statuses.car_id
    ) q
    where rn = 1 and location_id = ?
    ) as subquery on subquery.car_id = cars.id order by subquery.created_at desc', loc)
  )
end

Join はフィルターとして機能し、サブクエリに関係する Car オブジェクトのみを提供します。

注: 上記のように escape_sql を参照するには、ActiveRecord::Base を少し変更する必要があります。これをアプリの初期化子に追加することでこれを行います (これは app/config/initializers/active_record.rb に配置します):

class ActiveRecord::Base
  def self.escape_sql(clause, *rest)
    self.send(:sanitize_sql_array, rest.empty? ? clause : ([clause] + rest))
  end
end

.escape_sqlこれにより、AR::B に基づく任意のモデルを呼び出すことができます。これは非常に便利だと思いますが、SQL をサニタイズする他の方法がある場合は、代わりにそれを使用してください。

質問の 2 番目の部分については、同じ名前の場所が複数ある場合を除きLocation.find_by_name、上記に渡す ID に変換するだけです。基本的にこれ:

def self.by_location_name(name)
 loc = Location.find_by_name(name)
 by_location(loc)
end
于 2012-08-02T17:39:45.323 に答える