2014-01-06 65 views
25

我遇到了一個奇怪的問題,創建一個範圍和使用first發現者。看起來好像在作用域中使用first作爲查詢的一部分將使其返回所有結果,如果未找到結果。如果找到任何結果,它將正確返回第一個結果。Rails範圍返回全部而不是零

我已經建立了非常簡單的測試來證明這一點:

class Activity::MediaGroup < ActiveRecord::Base 
    scope :test_fail, -> { where('1 = 0').first } 
    scope :test_pass, -> { where('1 = 1').first } 
end 

請注意這個測試,我已經設置好條件匹配的記錄或沒有。實際上,我是根據實際情況進行查詢,並得到相同的奇怪行爲。

以下是失敗範圍的結果。正如你所看到的,它使正確的查詢,其中有沒有結果,所以這則所有匹配的記錄,並返回的查詢代替:

irb(main):001:0> Activity::MediaGroup.test_fail 
    Activity::MediaGroup Load (0.0ms) SELECT "activity_media_groups".* FROM "activity_media_groups" WHERE (1 = 0) ORDER BY "activity_media_groups"."id" ASC LIMIT 1 
    Activity::MediaGroup Load (0.0ms) SELECT "activity_media_groups".* FROM "activity_media_groups" 
=> #<ActiveRecord::Relation [#<Activity::MediaGroup id: 1, created_at: "2014-01-06 01:00:06", updated_at: "2014-01-06 01:00:06", user_id: 1>, #<Activity::MediaGroup id: 2, created_at: "2014-01-06 01:11:06", updated_at: "2014-01-06 01:11:06", user_id: 1>, #<Activity::MediaGroup id: 3, created_at: "2014-01-06 01:26:41", updated_at: "2014-01-06 01:26:41", user_id: 1>, #<Activity::MediaGroup id: 4, created_at: "2014-01-06 01:28:58", updated_at: "2014-01-06 01:28:58", user_id: 1>]> 

其他範圍按預期運行:

irb(main):002:0> Activity::MediaGroup.test_pass 
    Activity::MediaGroup Load (1.0ms) SELECT "activity_media_groups".* FROM "activity_media_groups" WHERE (1 = 1) ORDER BY "activity_media_groups"."id" ASC LIMIT 1 
=> #<Activity::MediaGroup id: 1, created_at: "2014-01-06 01:00:06", updated_at: "2014-01-06 01:00:06", user_id: 1> 

如果我在範圍之外執行相同的邏輯,我會得到預期的結果:

irb(main):003:0> Activity::MediaGroup.where('1=0').first 
    Activity::MediaGroup Load (0.0ms) SELECT "activity_media_groups".* FROM "activity_media_groups" WHERE (1=0) ORDER BY "activity_media_groups"."id" ASC LIMIT 1 
=> nil 

我在這裏錯過了什麼嗎?這看起來像Rails/ActiveRecord/Scopes中的一個bug,除非有一些未知的行爲期望。

+0

'.first'返回一個記錄,而不是阿雷爾,對不對? – Satya

+0

您正在使用哪種版本的ruby和rails? – shiva

+0

@shiva - Rails 4和Ruby 2.0 – Ryan

回答

48

這是不是一個錯誤或怪異的,經過一些研究後,我發現它的旨在設計。

首先,

  1. scope如果有零記錄其編程返回所有記錄 這又是一個ActiveRecord::Relation而不是nil

返回 ActiveRecord::Relation

  • 這背後的想法是讓米範圍可鏈接(IE)scopeclass methods之間的關鍵區別之一

    例子:

    現在讓我們使用以下情形:通過最近的用戶將能夠通過狀態過濾的帖子,訂貨更新的。很簡單,讓我們寫範圍爲:

    class Post < ActiveRecord::Base 
        scope :by_status, -> status { where(status: status) } 
        scope :recent, -> { order("posts.updated_at DESC") } 
    end 
    

    ,我們可以自由地稱他們是這樣的:

    Post.by_status('published').recent 
    # SELECT "posts".* FROM "posts" WHERE "posts"."status" = 'published' 
    # ORDER BY posts.updated_at DESC 
    

    或與用戶提供的PARAM:

    Post.by_status(params[:status]).recent 
    # SELECT "posts".* FROM "posts" WHERE "posts"."status" = 'published' 
    # ORDER BY posts.updated_at DESC 
    

    到目前爲止,好。現在讓我們把它們移到類的方法,只是爲了比較:

    class Post < ActiveRecord::Base 
        def self.by_status(status) 
        where(status: status) 
        end 
    
        def self.recent 
        order("posts.updated_at DESC") 
        end 
    end 
    

    除了使用一些額外的行,沒有大的改進。但是現在如果:status參數爲零或空白會發生什麼?

    Post.by_status(nil).recent 
    # SELECT "posts".* FROM "posts" WHERE "posts"."status" IS NULL 
    # ORDER BY posts.updated_at DESC 
    
    Post.by_status('').recent 
    # SELECT "posts".* FROM "posts" WHERE "posts"."status" = '' 
    # ORDER BY posts.updated_at DESC 
    

    Oooops,我不認爲我們想要允許這些查詢,對嗎?藉助範圍,我們可以很容易地解決這個問題通過增加一個存在條件,我們的範圍:

    scope :by_status, -> status { where(status: status) if status.present? } 
    

    我們去那裏:

    Post.by_status(nil).recent 
    # SELECT "posts".* FROM "posts" ORDER BY posts.updated_at DESC 
    
    Post.by_status('').recent 
    # SELECT "posts".* FROM "posts" ORDER BY posts.updated_at DESC 
    

    真棒。現在,讓我們嘗試做同樣與我們敬愛的類方法:

    class Post < ActiveRecord::Base 
        def self.by_status(status) 
        where(status: status) if status.present? 
        end 
    end 
    

    運行此:

    Post.by_status('').recent 
    NoMethodError: undefined method `recent' for nil:NilClass 
    

    和:炸彈:。區別在於範圍總是返回一個關係,而我們簡單的類方法實現則不會。類方法應該是這樣的,而不是:

    def self.by_status(status) 
        if status.present? 
        where(status: status) 
        else 
        all 
        end 
    end 
    

    請注意,我要回全部爲無/空白的情況下,其在軌道4,5返回的關係(它以前返回的項目數組從數據庫)。在Rails 3.2.x中,您應該使用scoped。還有,我們去:

    Post.by_status('').recent 
    # SELECT "posts".* FROM "posts" ORDER BY posts.updated_at DESC 
    

    所以這裏的建議是:不要從一個類的方法應該工作就像一個範圍返回nil,否則你打破由範圍隱含的chainability條件,即總是返回的關係。

    長話短說:

    無論什麼時候,作用域打算返回ActiveRecord::Relation,使其可鏈接。如果你期待firstlastfind結果,你應該使用class methods

    來源:http://blog.plataformatec.com.br/2013/02/active-record-scopes-vs-class-methods/

  • +0

    使用limit ,即使限制爲1,也會返回一個活動記錄關係,而首先返回模型的一個實例。所以,功能是不同的,在我看來很重要。如果存在的話,是否有其他方式可以從範圍中獲得一個模型,而不是關係? – Ryan

    +0

    @Ryan是否有任何理由不使用類方法? – shiva

    +1

    感謝您的信息,我明白你在說什麼......並且我看到了推理。雖然我仍然不認爲我完全同意一個範圍應該返回明顯不正確的東西。活動記錄關係是否不包含零記錄?我想你必須非常清楚如果使用像first,last或find這樣的函數不使用範圍。 – Ryan