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