我发现了很多标题相似的问题,但是没有一个可以解决我的问题。
我有一个模型Program
,其中有很多Videos
:
class Program < ActiveRecord::Base
has_many :videos
...
end
然后我在Video
中拥有作用域:
class Video < ActiveRecord::Base
belongs_to :program
scope :trailer, -> { where(video_type: 0) }
...
end
首先,当我有一个程序列表并想要访问视频时,我没有使用include
方法的N + 1程序:
> @programs.includes(:videos).map { |p| p.videos.size }
Program Load (0.6ms) SELECT "programs".* FROM "programs" ORDER BY "programs"."id" ASC LIMIT 10
Video Load (0.5ms) SELECT "videos".* FROM "videos" WHERE "videos"."program_id" IN (8, 9, 10, 11, 12, 13, 14, 15, 16, 17)
但是,当我尝试获取范围时,它将再次触摸数据库:
> @programs.includes(:videos).map { |p| p.videos.trailer }
Program Load (0.6ms) SELECT "programs".* FROM "programs" ORDER BY "programs"."id" ASC LIMIT 10
Video Load (0.5ms) SELECT "videos".* FROM "videos" WHERE "videos"."program_id" IN (8, 9, 10, 11, 12, 13, 14, 15, 16, 17)
Video Load (0.4ms) SELECT "videos".* FROM "videos" WHERE "videos"."program_id" = $1 AND "videos"."video_type" = $2 ORDER BY "videos"."id" ASC LIMIT 1 [["program_id", 8], ["video_type", 0]]
Video Load (0.4ms) SELECT "videos".* FROM "videos" WHERE "videos"."program_id" = $1 AND "videos"."video_type" = $2 ORDER BY "videos"."id" ASC LIMIT 1 [["program_id", 9], ["video_type", 0]]
Video Load (12.4ms) SELECT "videos".* FROM "videos" WHERE "videos"."program_id" = $1 AND "videos"."video_type" = $2 ORDER BY "videos"."id" ASC LIMIT 1 [["program_id", 10], ["video_type", 0]]
Video Load (0.3ms) SELECT "videos".* FROM "videos" WHERE "videos"."program_id" = $1 AND "videos"."video_type" = $2 ORDER BY "videos"."id" ASC LIMIT 1 [["program_id", 11], ["video_type", 0]]
Video Load (0.3ms) SELECT "videos".* FROM "videos" WHERE "videos"."program_id" = $1 AND "videos"."video_type" = $2 ORDER BY "videos"."id" ASC LIMIT 1 [["program_id", 12], ["video_type", 0]]
Video Load (0.3ms) SELECT "videos".* FROM "videos" WHERE "videos"."program_id" = $1 AND "videos"."video_type" = $2 ORDER BY "videos"."id" ASC LIMIT 1 [["program_id", 13], ["video_type", 0]]
Video Load (0.3ms) SELECT "videos".* FROM "videos" WHERE "videos"."program_id" = $1 AND "videos"."video_type" = $2 ORDER BY "videos"."id" ASC LIMIT 1 [["program_id", 14], ["video_type", 0]]
Video Load (0.3ms) SELECT "videos".* FROM "videos" WHERE "videos"."program_id" = $1 AND "videos"."video_type" = $2 ORDER BY "videos"."id" ASC LIMIT 1 [["program_id", 15], ["video_type", 0]]
Video Load (0.4ms) SELECT "videos".* FROM "videos" WHERE "videos"."program_id" = $1 AND "videos"."video_type" = $2 ORDER BY "videos"."id" ASC LIMIT 1 [["program_id", 16], ["video_type", 0]]
Video Load (0.4ms) SELECT "videos".* FROM "videos" WHERE "videos"."program_id" = $1 AND "videos"."video_type" = $2 ORDER BY "videos"."id" ASC LIMIT 1 [["program_id", 17], ["video_type", 0]]
您可以看到它将多次加载数据库,从而导致性能下降。
#<Benchmark::Tms:0x007f95faa8fab0 @label="", @real=0.02663199999369681, @cstime=0.0, @cutime=0.0, @stime=0.0, @utime=0.019999999999999574, @total=0.019999999999999574>
我能想到的一种解决方案是将视频转换为数组并搜索数组:
> @programs.includes(:videos).map { |program| program.videos.to_ary.select { |v| v.video_type == 0 } }
Program Load (0.5ms) SELECT "programs".* FROM "programs" WHERE "programs"."id" IN (8, 9, 10, 11, 12, 13, 14, 15, 16, 17)
Video Load (0.4ms) SELECT "videos".* FROM "videos" WHERE "videos"."program_id" IN (17, 16, 13, 12, 11, 9, 8, 15, 14, 10)
性能更好,但是代码很复杂。
#<Benchmark::Tms:0x007f95faac8720 @label="", @real=0.006901999993715435, @cstime=0.0, @cutime=0.0, @stime=0.0, @utime=0.010000000000000675, @total=0.010000000000000675>
我可以想到的另一种解决方案是在has_many
中为范围添加新的Program
:
class Program < ActiveRecord::Base
has_many :videos
has_many :trailer_videos, -> { where(video_type: 0) }, class: 'Video'
...
end
然后,如果我includes
并直接调用新关系,它也将渴望加载。
> @programs.includes(:trailer_videos).map { |program| program.trailer_videos }
Program Load (0.5ms) SELECT "programs".* FROM "programs" WHERE "programs"."id" IN (8, 9, 10, 11, 12, 13, 14, 15, 16, 17)
Video Load (0.3ms) SELECT "videos".* FROM "videos" WHERE "videos"."video_type" = $1 AND "videos"."program_id" IN (17, 16, 13, 12, 11, 9, 8, 15, 14, 10) [["video_type", 0]]
以下是基准,这是非常快的:
#<Benchmark::Tms:0x007f95fdea96c0 @label="", @real=0.004801000002771616, @cstime=0.0, @cutime=0.0, @stime=0.0, @utime=0.009999999999999787, @total=0.009999999999999787>
但是,以这种方式,它将使Program
模型变得如此沉重。因为对于Video
中的每个范围,我需要在Program
中添加一个相关的关联。
因此,我正在寻找一种更好的解决方案,该解决方案将范围逻辑保留在Video
内,但没有N + 1问题。
欢呼
答案 0 :(得分:3)
一种解决方案是将merge
与eager_load
一起使用:
@programs.eager_load(:videos).merge(Video.trailer).map { |p| p.videos.size }
它仅产生一个查询。
答案 1 :(得分:2)
正如我所说,IMO为您添加has_many :trailer_videos, -> { where(video_type: 0) }, class: 'Video'
的方法是解决您的问题的最简单,最好的方法。在模型中添加更多此类关联没有任何缺点。
答案 2 :(得分:2)
如果程序员知道视频类型,则可以使用ActiveRecord::Enum
和一些简单的元编程来为枚举中的每个可能值以编程方式创建关联。
class Video < ActiveRecord::Base
enum video_type: [:trailer, :promo, :foo, :bar]
end
class Program < ActiveRecord::Base
# this creates trailer_videos etc assocations
Video.video_types.each do |key, int|
# eval is needed since we need to dynamically create
# the lamba for each type
has_many "#{key}_videos".to_sym, eval "->{ Video.send(#{key}) }"
end
end
答案 3 :(得分:0)
在Rails Associations
中有一个可选的scope参数,该参数接受应用于Relation
的lambda(请参阅https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many-label-Scopes)
因此您可以将模型编写为:
# app/models/video.rb
class Video < ActiveRecord::Base
belongs_to :program
scope :trailer, -> { where(video_type: 0) }
...
end
# app/models/program.rb
class Program < ActiveRecord::Base
has_many :videos
has_many :trailer_videos, -> { trailer }, class: 'Video'
...
end
这样,您可以将范围的定义保留在Video
中,并从Program
中重用它。