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

时间:2017-09-19 04:32:20

标签: ruby-on-rails ruby oracle ruby-on-rails-3

我有多对多的模型关系:

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

有更有效的方法吗?应该注意的是,查询可能变得更加复杂,如:所有动作但不是戏剧的电影或所有喜欢浪漫和喜剧的电影

3 个答案:

答案 0 :(得分:0)

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

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')

答案 1 :(得分:0)

您可以通过sql语句从动作片集中删除戏剧电影,避免必须为所有动作和戏剧电影实例化Movie个实例,从而提高效率。

基本构建块是一个类似于widjajayd proposed

的动态范围
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应用程序,你必须手动输入移动大部分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']))

答案 2 :(得分:-1)

看不到使用一个查询的方法(无论如何不使用子查询)。但是我想这是一个让它变得更好的一个:

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")

正如评论中所指出的,这将获得所有动作和戏剧的电影。

相关问题