标题令人困惑,但请允许我解释一下。我有一个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雪松堆栈上托管)
答案 0 :(得分: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基于此返回结果。因为你可能想要使用eager loading,所以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
这允许您在任何基于AR :: B的模型上调用.escape_sql
。我发现这非常有用,但是如果你有其他方法来清理sql,请随意使用它。
对于问题的第二部分 - 除非有多个位置具有相同的名称,否则我只需要Location.find_by_name
将其转换为id以传递到上面。基本上这个:
def self.by_location_name(name)
loc = Location.find_by_name(name)
by_location(loc)
end