Sql has\u one关联的Rails 3查询匹配属性是has\u many关联的子集

Sql has\u one关联的Rails 3查询匹配属性是has\u many关联的子集,sql,ruby-on-rails-3,Sql,Ruby On Rails 3,标题令人困惑,但请允许我解释一下。我有一个汽车模型,它有多个具有不同时间戳的数据点。我们几乎总是关注其最新状态的属性。因此,该模型具有多种状态,以及一个可轻松访问其最新状态的状态: class Car < ActiveRecord::Base has_many :statuses, class_name: 'CarStatus', order: "timestamp DESC" has_one :latest_status, class_name: 'CarStatus', ord

标题令人困惑,但请允许我解释一下。我有一个汽车模型,它有多个具有不同时间戳的数据点。我们几乎总是关注其最新状态的属性。因此,该模型具有多种状态,以及一个可轻松访问其最新状态的状态:

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
假设我想要一个(可链接的)范围来查找最新位置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
#car.rb
按位置定义自我id(id)
ids=[]
查找每个(包括::最新状态)do |车|

ids好的。既然你像我一样使用postgres 9.1,我就试试看。首先解决第一个问题(范围按最后状态的位置过滤):

此解决方案利用了PostGres对分析函数的支持,如下所述:

我认为以下内容为您提供了所需的部分内容(当然,替换/插入您感兴趣的“?”位置id):

此查询将返回
car\u id
status\u id
location\u id
,以及时间戳(默认情况下称为created\u 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中):

这允许您在基于AR::B的任何模型上调用
.escape\u sql
。我发现这非常有用,但如果您有其他方法来清理sql,请随意使用它

对于问题的第二部分-除非有多个位置具有相同的名称,否则我只需执行一个
位置。按\u名称查找\u
,将其转换为一个id,以传递到上面的位置。基本上:

def self.by_location_name(name)
 loc = Location.find_by_name(name)
 by_location(loc)
end

我有一种感觉,一个有效的解决方案,你的问题将有点数据库具体。请您将数据库的名称和版本添加到问题中好吗?@TheWalrus good call先生,我使用的是带有Postgres 9.1的Heroku cedar堆栈,在单独的表中保存最新状态和旧状态可能更容易。这样,你仍然有你的历史,但你没有困难查询。有些关联:谢谢你的回答。。我曾短暂地试着让它工作,但没有多大成功,但最近没有太多时间。我会回来提供更多信息。再次感谢我,我真的无法让它工作,可能只是因为我在sql方面很差劲。不管怎样,我最终做的不是依靠时间戳每次查找最新状态,而是在CarStatus上设置一个
latest?
标志,以及一个after_save回调,确保该标志设置为该车的最新状态。所以我的汽车瞄准器现在好多了,只是检查一下那个标志。
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 = ?
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
class ActiveRecord::Base
  def self.escape_sql(clause, *rest)
    self.send(:sanitize_sql_array, rest.empty? ? clause : ([clause] + rest))
  end
end
def self.by_location_name(name)
 loc = Location.find_by_name(name)
 by_location(loc)
end