我有多对多的模型关系:
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
有更有效的方法吗?应该注意的是,查询可能变得更加复杂,如:所有动作但不是戏剧的电影或所有喜欢浪漫和喜剧的电影
答案 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")
正如评论中所指出的,这将获得所有动作和戏剧的电影。