1

标题令人困惑,但请允许我解释一下。我有一个具有不同时间戳的多个数据点的汽车模型。我们几乎总是关心其最新状态的属性。所以模型 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_idstatus_idlocation_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,请随意使用它。

对于问题的第二部分-除非有多个具有相同名称的位置,否则我只需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 回答