Skip to content

Commit 6d3acaf

Browse files
authored
Merge pull request rails#40842 from ghiculescu/activestorage-variant-nplusone
Add support for eager loading Active Storage variants
2 parents 3dd44a7 + 8e3ad47 commit 6d3acaf

File tree

6 files changed

+114
-2
lines changed

6 files changed

+114
-2
lines changed

activestorage/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
* Add `with_all_variant_records` method to eager load all variant records on an attachment at once.
2+
`with_attached_image` scope now eager loads variant records if using variant tracking.
3+
4+
*Alex Ghiculescu*
5+
16
* Add metadata value for presence of audio channel in video blobs
27

38
The `metadata` attribute of video blobs has a new boolean key named `audio` that is set to

activestorage/app/models/active_storage/attachment.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ class ActiveStorage::Attachment < ActiveStorage::Record
1919
after_create_commit :mirror_blob_later, :analyze_blob_later
2020
after_destroy_commit :purge_dependent_blob_later
2121

22+
scope :with_all_variant_records, -> { includes(blob: :variant_records) }
23+
2224
# Synchronously deletes the attachment and {purges the blob}[rdoc-ref:ActiveStorage::Blob#purge].
2325
def purge
2426
transaction do

activestorage/app/models/active_storage/variant_with_record.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ def create_or_find_record(image:)
4949
end
5050

5151
def record
52-
@record ||= blob.variant_records.find_by(variation_digest: variation.digest)
52+
@record ||= if blob.variant_records.loaded?
53+
blob.variant_records.find { |v| v.variation_digest == variation.digest }
54+
else
55+
blob.variant_records.find_by(variation_digest: variation.digest)
56+
end
5357
end
5458
end

activestorage/lib/active_storage/attached/model.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,13 @@ def purge_later
166166
end
167167
has_many :"#{name}_blobs", through: :"#{name}_attachments", class_name: "ActiveStorage::Blob", source: :blob, strict_loading: strict_loading
168168

169-
scope :"with_attached_#{name}", -> { includes("#{name}_attachments": :blob) }
169+
scope :"with_attached_#{name}", -> {
170+
if ActiveStorage.track_variants
171+
includes("#{name}_attachments": { blob: :variant_records })
172+
else
173+
includes("#{name}_attachments": :blob)
174+
end
175+
}
170176

171177
after_save { attachment_changes[name.to_s]&.save }
172178

activestorage/test/models/variant_with_record_test.rb

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,82 @@ class ActiveStorage::VariantWithRecordTest < ActiveSupport::TestCase
5656

5757
assert_equal "local_public", variant.image.blob.service_name
5858
end
59+
60+
test "eager loading is supported" do
61+
user = User.create!(name: "Josh")
62+
63+
blob1 = directly_upload_file_blob(filename: "racecar.jpg")
64+
assert_difference -> { ActiveStorage::VariantRecord.count }, +1 do
65+
blob1.representation(resize: "100x100").process
66+
end
67+
68+
blob2 = directly_upload_file_blob(filename: "racecar_rotated.jpg")
69+
assert_difference -> { ActiveStorage::VariantRecord.count }, +1 do
70+
blob2.representation(resize: "100x100").process
71+
end
72+
73+
assert_no_difference -> { ActiveStorage::VariantRecord.count } do
74+
user.vlogs.attach(blob1)
75+
user.vlogs.attach(blob2)
76+
end
77+
78+
user.reload
79+
80+
assert_no_difference -> { ActiveStorage::VariantRecord.count } do
81+
assert_queries(5) do
82+
# 5 queries:
83+
# attachments (vlogs) x 1
84+
# blob x 2
85+
# variant record x 2
86+
user.vlogs.map do |vlog|
87+
vlog.representation(resize: "100x100").processed
88+
end
89+
end
90+
end
91+
92+
user.reload
93+
94+
assert_no_difference -> { ActiveStorage::VariantRecord.count } do
95+
assert_queries(3) do
96+
# 3 queries:
97+
# attachments (vlogs) x 1
98+
# blob x 1
99+
# variant record x 1
100+
user.vlogs.includes(blob: :variant_records).map do |vlog|
101+
vlog.representation(resize: "100x100").processed
102+
end
103+
end
104+
end
105+
106+
user.reload
107+
108+
assert_no_difference -> { ActiveStorage::VariantRecord.count } do
109+
assert_queries(3) do
110+
# 3 queries:
111+
# attachments (vlogs) x 1
112+
# blob x 1
113+
# variant record x 1
114+
user.vlogs.with_all_variant_records.map do |vlog|
115+
vlog.representation(resize: "100x100").processed
116+
end
117+
end
118+
end
119+
120+
user.reload
121+
122+
assert_no_difference -> { ActiveStorage::VariantRecord.count } do
123+
assert_queries(4) do
124+
# 4 queries:
125+
# user x 1
126+
# attachments (vlogs) x 1
127+
# blob x 1
128+
# variant record x 1
129+
User.where(id: user.id).with_attached_vlogs.map do |u|
130+
u.vlogs.map do |vlog|
131+
vlog.representation(resize: "100x100").processed
132+
end
133+
end
134+
end
135+
end
136+
end
59137
end

activestorage/test/test_helper.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,23 @@ class ActiveSupport::TestCase
6060
ActiveStorage::Current.reset
6161
end
6262

63+
def assert_queries(expected_count)
64+
ActiveRecord::Base.connection.materialize_transactions
65+
66+
queries = []
67+
ActiveSupport::Notifications.subscribe("sql.active_record") do |*, payload|
68+
queries << payload[:sql] unless %w[ SCHEMA TRANSACTION ].include?(payload[:name])
69+
end
70+
71+
yield.tap do
72+
assert_equal expected_count, queries.size, "#{queries.size} instead of #{expected_count} queries were executed. #{queries.inspect}"
73+
end
74+
end
75+
76+
def assert_no_queries(&block)
77+
assert_queries(0, &block)
78+
end
79+
6380
private
6481
def create_blob(key: nil, data: "Hello world!", filename: "hello.txt", content_type: "text/plain", identify: true, service_name: nil, record: nil)
6582
ActiveStorage::Blob.create_and_upload! key: key, io: StringIO.new(data), filename: filename, content_type: content_type, identify: identify, service_name: service_name, record: record

0 commit comments

Comments
 (0)