2010-03-06 46 views
0

我有三個型號:Rails ActiveRecord - 執行包含的最佳方式?

class Book < ActiveRecord::Base 
    has_many :collections 
    has_many :users, :through => :collections 
end 

class User < ActiveRecord::Base 
    has_many :collections 
    has_many :books, :through => :collections 
end 

class Collection < ActiveRecord::Base 
    belongs_to :book 
    belongs_to :user 
end 

我想要顯示的書籍列表,並有一個鏈接添加或從用戶的集合中刪除。我無法弄清楚這樣做的最佳語法。

例如,如果我做到以下幾點:

控制器

class BooksController < ApplicationController 
    def index 
    @books = Book.all 
    end 
end 

查看

... 
<% if book.users.include?(current_user) %> 
... 

或明顯的逆...

... 
<% if current_user.books.include?(book) %> 
... 

然後查詢是爲每個發送書檢查包括?這是浪費。我正在考慮將用戶或集合添加到Book.all上的include,但我不確定這是否是最好的方法。實際上,我需要的僅僅是書籍對象,而不管當前用戶是否在其集合中擁有該書籍,但我不確定如何爲查詢進行論壇化。

在此先感謝您的幫助。

-Damien

回答

1

我創建了一個gem(select_extra_columns),用於返回ActiveRecord查找器中的join/calculated/aggregate列。使用這個寶石,您將能夠在一個查詢中獲得書籍詳細信息和標誌indicating if the current user has the book

在您的用戶模型中註冊select_extra_columns功能。

class Book < ActiveRecord::Base 
    select_extra_columns 
    has_many :collections 
    has_many :users, :through => :collections 
end 

現在在你的控制器中加入這一行:

@books = Book.all(
      :select => "books.*, IF(collections.id, 1, 0) AS belongs_to_user", 
      :extra_columns => {:belongs_to_user => :boolean}, 
      :joins => "LEFT OUTER JOIN collections 
         ON book.id = collections.book_id AND 
          collections.user_id = #{current_user.id}" 
     ) 

現在,在你看來,你可以做到以下幾點。

book.belongs_to_user? 
+0

這似乎是最好的方式,但我遇到了混合問題:連接和:包含在同一個查找中。我的@books = Book.all真的是@books = Book.all(:include => [:authors,...]) – dwhite 2010-03-09 03:26:59

+0

您可以使用'include'而不是'joins'。不必同時使用它們。 – 2010-03-12 08:26:01

-1

上,因爲它是直接的SQL調用關聯使用exists?。關聯數組不會被加載來執行這些檢查。

books.users.exists?(current_user) 

這是由Rails執行的SQL。

SELECT `users`.id FROM `users` 
INNER JOIN `collections` ON `users`.id = `collections`.user_id 
WHERE (`users`.`id` = 2) AND ((`collections`.book_id = 1)) LIMIT 1 

在上述SQL current_user ID = 2和book id爲1

current_user.books.exists?(book) 

這是通過滑軌執行的SQL。

SELECT `books`.id FROM `books` 
INNER JOIN `collections` ON `books`.id = `collections`.book_id 
WHERE (`books`.`id` = 3) AND ((`collections`.user_id = 4)) LIMIT 1 

在上述SQL current_user ID = 4和book id爲3

詳情,請參閱exists?方法的documentation:has_many關聯。

編輯:我已經包含額外的信息來驗證我的答案。

+0

我很驚訝我的答案被否決了。這個解決方案當然有效。我很想知道投票的原因。 – 2010-03-07 05:33:04

+0

您的解決方案有效,但效率不高/良好做法。它違背了include的目的,即儘量減少數據庫的往返次數。使用存在?每本書需要一個電話(舊的1 + N查詢問題)。如果有一百本書,那麼這100次往返數據庫服務器和100次查詢必須通過網絡編譯,執行和返回他們的查詢計劃。 – Michael 2010-03-07 06:37:15

+0

我以爲用戶想要改進'包含?'打電話給他。我一定誤解了這個問題。 – 2010-03-07 08:18:09

-1

我會先在用戶模式創建一個實例方法「緩存」的所有圖書ID在他的收藏品:

def book_ids 
    @book_ids ||= self.books.all(:select => "id").map(&:id) 
end 

這隻會每個控制器的要求執行SQL查詢一次。然後在用戶模型上創建另一個實例方法,將book_id作爲參數,並檢查是否將其包含在他的書集中。

def has_book?(book_id) 
    book_ids.include?(book_id) 
end 

然後當你通過書籍迭代:

<% if current_user.has_book?(book.id) %> 

只有2該控制器的請求:)

+0

我想知道爲什麼這個解決方案得到了一個downvote? – 2010-03-07 13:59:46

+0

我也很感興趣,知道爲什麼反對票,因爲這個解決方案非常好。不是我如何做到這一點,但仍然是一個很好的解決方案。 – 2010-03-07 19:29:02

0

你會想2個SQL查詢的SQL查詢,以及O( 1)基於查找(可能不相關,但這是原則)來檢查他們是否擁有這本書。

初始調用。

@books = Book.all 
@user = User.find(params[:id], :include => :collections) 

接下來,你會希望用戶有寫的書轉化爲固定的時間查找的哈希(如果人們永遠不會有很多書,只是在做一個array.include?是罰款) 。

@user_has_books = Hash.new 
@user.collections.each{|c|@user_has_books[c.book_id] = true} 

而且在顯示結束:

@books.each do |book| 
    has_book = @user_has_books.has_key?(book.id) 
end 

我會從緩存用戶對象的book_ids,只是因爲走這條路可以有一些有趣的和意想不到的後果,如果你曾經開始犯錯離開無論出於什麼原因(例如,memcached或隊列)序列化用戶對象。

編輯:加載中間集合而不是雙重加載書籍。

0

實質上,您需要進行一次調用才能獲取圖書信息以及指示當前用戶是否擁有圖書的布爾標誌。 ActiveRecord查找器不允許您從其他表中返回連接結果。我們通過伎倆來解決這個問題。

在您的Book模型中添加此方法。

def self.extended_book 
    self.columns # load the column definition 
    @extended_user ||= self.clone.tap do |klass| 
     klass.columns << (klass.columns_hash["belongs_to_user"] = 
         ActiveRecord::ConnectionAdapters::Column.new(
          "belongs_to_user", false, "boolean")) 
    end # add a dummy column to the cloned class 
end 

在你的控制器使用下面的代碼:

@books = Book.extended_book.all(
      :select => "books.*, IF(collections.id, 1, 0) AS belongs_to_user", 
      :joins => "LEFT OUTER JOIN collections 
         ON book.id = collections.book_id AND 
          collections.user_id = #{current_user.id}" 
     ) 

現在,在你看來,你可以做到以下幾點。

book.belongs_to_user? 

說明:

extended_book方法是創建Book類的副本,並添加一個虛擬列belongs_to_user的哈希值。在查詢期間,額外連接列不會被拒絕,因爲它存在於columns_hash中。您只能使用extended_book進行查詢。 如果您將其用於CRUD操作,DB將引發錯誤。