在 Rails 3 多对多关联中,根据关联条件查询对象的最有效方法是什么?

In a Rails 3 many to many association, what is the most efficient way to query objects based on conditions on their associations?

我有一个多对多模型关系:

class Movie
has_many :movie_genres
has_many :genres, :through => :movie_genres

class Genre
has_many :movie_genres
has_many :movies, :through => :movie_genres

class MovieGenre
belongs_to :movie
belongs_to :genre

我想查询具有某种类型但与另一种类型没有关联的所有电影。示例:所有动作片而非剧情片

我做的是这样的:

action_movies = Genre.find_by_name('action').movies
drama_movies  = Genre.find_by_name('drama').movies
action_not_drama_movies = action_movies - drama_movies

有没有更有效的方法?应该注意的是,查询可以变得更复杂,例如:All movies that are Action but not Drama or All movies that are Romance and Comedy

没有找到用一个查询来完成它的方法(无论如何都不能不使用子查询)。但我认为这是一个更好的选择:

scope :by_genres, lambda { |genres|
  genres = [genres] unless genres.is_a? Array
  joins(:genres).where(genres: { name: genre }).uniq
}

scope :except_ids, lambda { |ids|
  where("movies.id NOT IN (?)", ids)
}

scope :intersect_ids, lambda { |ids|
  where("movies.id IN (?)", ids)
}

## all movies that are action but not drama
action_ids = Movie.by_genres("action").ids
drama_movies = Movie.by_genres("drama").except_ids(action_ids)

## all movies that are both action and dramas
action_ids = Movie.by_genres("action").ids
drama_movies = Movie.by_genres("drama").intersect_ids(action_ids)

## all movies that are either action or drama
action_or_drama_movies = Movie.by_genres(["action", "drama"])

可以在 Rails 中与原始 SQL 进行除法和相交。但我认为这通常不是一个好主意,因为它仍然需要多个查询,并且还可能使代码依赖于所使用的数据库。


我原来的回答比较幼稚。我会把它留在这里,以免其他人犯同样的错误:

使用joins,一查询就可以得到:

Movie.joins(:genres).where("genres.name = ?", "action").where("genres.name != ?", "drama")

如评论中所述,这也将获得所有动作片和剧情片。

可能使用范围更好,这里是示例和解释(但未测试),如下在电影模型中创建范围

Movie.rb

scope :action_movies, joins(movie_genre: :genre).select('movies.*','genres.*').where('genres.name  = ?', 'action')
scope :drama_movies, joins(movie_genre: :genre).select('movies.*','genres.*').where('genres.name  = ?', 'drama')

在你的控制器中,你可以调用如下

@action_movies = Movie.action_movies
@drama_movies  = Movie.drama_movies
@action_not_drama_movies = @action_movies - @drama_movies

为动态范围编辑

如果你想要动态的,那么你可以将参数发送到下面的范围是使用块的范围。

scope :get_movies, lambda { |genre_request|
 joins(movie_genre: :genre).select('movies.*','genres.*').where('genres.name  = ?', genre_request)
}

genre_request = 作用域的参数变量

在你的控制器中

  @action_movies = Movie.get_movies('action')
  @drama_movies  = Movie.get_movies('drama')

您确实可以提高效率,方法是通过 sql 语句从动作片集中删除剧情片,从而避免为所有动作片和剧情片实例化 Movie 实例。

基本构建块是类似于

的动态作用域
class Movie
  ...
  # allows to be called with a string for single genres or an array for multiple genres
  # e.g. "action" will result in SQL like `WHERE genres.name = 'action'`
  #      ["romance", "comedy"] will result in SQL like `WHERE genres.name IN ('romance', 'comedy')`
  def self.of_genre(genres_names)
    joins(:genres).where(genres: { name: genres_names })
  end
  ...
end

您可以将该示波器用作构建块来获取您想要的电影

所有动作非剧情的电影:

Movie
  .of_genre('action')
  .where("movies.id NOT IN (#{Movie.of_genre('drama').to_sql}")

这将导致 sql 子查询。使用连接会更好,但对于大多数情况来说它应该足够好,并且比连接替代方案更好。

如果您的应用有 rails 5 个应用程序,您甚至可以键入

Movie
  .of_genre('action')
  .where.not(id: Movie.of_genre('drama')) 

所有动作片而非剧情片或所有浪漫片和喜剧片

因为它是一个 rails 3 应用程序,您将不得不手动输入 move 大部分 sql,并且不能大量使用范围。 or 方法仅在 rails 5 中引入。因此这意味着必须键入:

Movie
  .joins(:genres)
  .where("(genres.name = 'action' AND movies.id NOT IN (#{Movie.of_genre('drama').to_sql}) OR genres.name IN ('romance', 'comedy')" ) 

同样,如果它是 rails 5 应用程序,这会简单得多

Movie
  .of_genre('action')
  .where.not(id: Movie.of_genre('drama'))
  .or(Movie.of_genre(['romance', 'comedy']))