繁体   English   中英

在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

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

可能使用范围更好,这是示例和说明(但未经测试),请在影片模型中创建范围,如下所示

电影.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实例化Movie实例,确实可以提高效率。

基本组成部分是动态范围,类似于widjajayd提出的内容

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

看不到一种通过一个查询来完成此操作的方法(无论如何也不是不使用子查询)。 但是我认为这是一个更好的选择:

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

正如评论中指出的那样,这也将获得所有兼具动作和戏剧性的电影。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM