在 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']))
我有一个多对多模型关系:
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']))