2010-03-06 38 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将引发错误。

相关问题