Rails ActiveRecord - 执行包含的最佳方式?

时间:2010-03-06 20:57:03

标签: ruby-on-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,但我不确定这是最好的方法。实际上,我所需要的只是book对象,只是一个布尔列,表明当前用户是否在他们的集合中有书,但我不知道如何对查询进行论证以便这样做。

提前感谢您的帮助。

-Damien

5 个答案:

答案 0 :(得分:1)

我创建了一个gem(select_extra_columns),用于在ActiveRecord查找器中返回join / calculated / aggregate列。使用此gem,您将能够在一个查询中获取书籍详细信息和标记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?

答案 1 :(得分:0)

你将需要2个SQL查询和基于O(1)的查找(可能不相关,但这是原则)来检查他们是否有这本书。

初次通话。

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

接下来,您将要将用户拥有的书籍写入哈希以进行恒定时间查找(如果人们不会有很多书籍,只需要做一个数组。包含?很好)。

@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或队列)开始序列化你的用户对象,那么走这条路会会产生一些有趣和意想不到的后果。

编辑:加载中间集合而不是双重装书。

答案 2 :(得分: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将抛出错误。

答案 3 :(得分:-1)

在关联上使用exists?,因为它是直接SQL调用。未加载关联数组以执行这些检查。

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)

这是Rails执行的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?关联中:has_many方法的documentation

修改:我已提供其他信息以验证我的回答。

答案 4 :(得分:-1)

我首先在User模型中创建一个实例方法,用于“缓存”其集合中的所有Book ID:

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

每个控制器请求只执行一次SQL查询。然后在User模型上创建另一个实例方法,该方法将book_id作为参数,并检查它是否包含在他的图书集中。

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

然后,当你遍历书籍时:

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

该控制器请求只有2个SQL查询:)