2012-07-16 71 views
1

标题很混乱,但让我解释一下。我有一个车型有多个数据点,具有不同的时间戳。我们几乎总是关心其最新状态的属性。因此模型的has_many状态,具有HAS_ONE一起轻松访问其最新一个:Rails 3 has_one关联的一个查询匹配属性has_many association的子集

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" 

比方说,我想有一个(可链接)范围,找到为1。最新的位置ID的所有汽车目前我有一种复杂的方法:

# 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更快的方法,但不知道如何只得到l每辆车的最佳状态。可能有许多状态记录的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 

这将“迈阿密”,“FL”,“MIA”等相匹配的上面的位置...有没有人有任何建议,我可以如何使这个更简洁/高效?以不同的方式定义我的关联会更好吗?或者,也许它会采取一些SQL忍者技能,我承认没有。

使用Postgres 9.1(托管在Heroku雪松堆栈上)

+0

我有一种感觉,你的问题的有效解决方案将有点数据库特定。你能否把你的数据库的名称和版本添加到问题中? – MrTheWalrus 2012-08-01 17:40:04

+0

@MrTheWalrus良好的调用,我使用Heroku雪松堆栈与Postgres 9.1 – 2012-08-02 01:22:09

+1

将最新状态和旧状态保存在单独的表中可能会更容易。这样你仍然有你的历史,但你没有困难查询。有点相关:http://stackoverflow.com/questions/762405/database-data-versioning – Mischa 2012-08-02 02:41:23

回答

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 

加入将作为一个过滤器,只给你参与子查询的汽车对象。

注意:为了像上面那样引用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 
+0

感谢您的回答..我试图简单地得到这个工作没有太大的成功,但最近没有太多时间。我会回来更多的信息。再次感谢 – 2012-08-08 01:28:48

+0

仅供参考我真的无法得到这个工作,它可能只是我吮吸SQL。无论如何,我最终做的并不是依赖时间戳来找到每次最新的状态,而是在CarStatus上放置一个'latest?'标志和一个after_save回调,以确保该标志设置为最新的状态汽车。所以我的汽车范围现在好多了,只是检查那个标志。 – 2012-08-17 15:03:41