在 Rails 中使用范围进行热切加载

Ste*_*hen 10 ruby-on-rails

我发现了许多标题相似的问题,但没有一个能解决我的问题。

我有一个模型Program,它有很多Videos

class Program < ActiveRecord::Base
  has_many :videos
  ...
end
Run Code Online (Sandbox Code Playgroud)

然后我有范围Video

class Video < ActiveRecord::Base
  belongs_to :program

  scope :trailer, -> { where(video_type: 0) }
  ...
end
Run Code Online (Sandbox Code Playgroud)

首先,当我有一个程序列表并想要访问视频时,我没有使用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)
Run Code Online (Sandbox Code Playgroud)

但是,当我尝试获取范围时,它会再次触及数据库:

> @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]]
Run Code Online (Sandbox Code Playgroud)

您可以看到它会多次加载数据库,从而导致性能不佳。

#<Benchmark::Tms:0x007f95faa8fab0 @label="", @real=0.02663199999369681, @cstime=0.0, @cutime=0.0, @stime=0.0, @utime=0.019999999999999574, @total=0.019999999999999574>
Run Code Online (Sandbox Code Playgroud)

我能想到的一种解决方案是将视频转换为数组并搜索数组:

> @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)
Run Code Online (Sandbox Code Playgroud)

性能更好,但代码复杂。

#<Benchmark::Tms:0x007f95faac8720 @label="", @real=0.006901999993715435, @cstime=0.0, @cutime=0.0, @stime=0.0, @utime=0.010000000000000675, @total=0.010000000000000675>
Run Code Online (Sandbox Code Playgroud)

我能想到的另一个解决方案是添加一个新has_manyProgramfor 范围:

class Program < ActiveRecord::Base
  has_many :videos
  has_many :trailer_videos, -> { where(video_type: 0) }, class: 'Video'
  ...
end
Run Code Online (Sandbox Code Playgroud)

然后,如果我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]]
Run Code Online (Sandbox Code Playgroud)

基准测试如下,速度超快:

#<Benchmark::Tms:0x007f95fdea96c0 @label="", @real=0.004801000002771616, @cstime=0.0, @cutime=0.0, @stime=0.0, @utime=0.009999999999999787, @total=0.009999999999999787>
Run Code Online (Sandbox Code Playgroud)

然而,这样一来,就会让Program模型变得如此沉重。因为在每个范围内Video,我需要加入相关协会Program


因此,我正在寻找更好的解决方案,它将范围逻辑保持在 内Video,但没有 N+1 问题。

干杯

Pav*_*van 8

正如我所说,IMO 你添加的has_many :trailer_videos, -> { where(video_type: 0) }, class: 'Video'方法是解决你的问题的简单和最好的方法。我认为向模型添加更多此类关联没有任何缺点。


Ily*_*hov 5

在 Rails 中,Associations有一个可选的作用域参数,它接受应用于的 lambda Relation(请参阅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
Run Code Online (Sandbox Code Playgroud)

这样您就可以将范围的定义保留在 中Video并从 中重用它Program