jit_preloader 1.0.4 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 515aa37a3edcd60ff85c7490bf7a96906a612c7b5645d3d81542c5e7997a9026
4
- data.tar.gz: 4cab1990cd0346da95d0d0785d28b18d5db95fecf66dbfcf14be26d0b020af5d
3
+ metadata.gz: bca887aa87a283d55ee26b1fe606948733ecfecc7ce40d4cedc8b89d90efb834
4
+ data.tar.gz: 9a7c463ccacf8daeebc23d2eaa375d8c4f908b333c492baaa4c4c931bbc7d1b3
5
5
  SHA512:
6
- metadata.gz: bf81adb00759b36e3bea5dfcaf9431c3259cac06412b57008e799585c7094d97073f836300a9631bd24fa0de7820874646861e7ea68e27cb773662d2e3826dec
7
- data.tar.gz: b96b45f91474ca01a5235d5ba071c750268e7c029fcfa371c308747433a5cb9f54bfb2560a99051c335352c8a536a01887ae01bb840337ee2e9ab3a808f5ec84
6
+ metadata.gz: 30920baa0426de8bc5188fc80ccfacf6e040f6bf1f7c934214bb75600080a090756ca957da9a59327a63b30550d7a4efdf5f4e66c873783e0c5aa627fe558e9f
7
+ data.tar.gz: 9adfa72af9bfcc684c3c22f6e82b353af9b36081224cae92e248e5c52db7f028a9c6f49dc3760d059f476980a48f4852b6f94c5e5faf82dbef28087e0b8dd3cd
@@ -0,0 +1,32 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+ branches: [master]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ gemfile:
16
+ - Gemfile
17
+ - Gemfile.5.2
18
+ - Gemfile.6.0
19
+ - Gemfile.6.1
20
+ env:
21
+ BUNDLE_GEMFILE: ${{ matrix.gemfile }}
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+ - name: Set up Ruby ${{ matrix.ruby-version }}
25
+ uses: ruby/setup-ruby@v1
26
+ with:
27
+ ruby-version: 2.7
28
+ - name: Install dependencies
29
+ run: bundle install
30
+ - name: Run tests
31
+ run:
32
+ bundle exec rspec
@@ -1,7 +1,6 @@
1
1
  name: Ruby Gem
2
2
 
3
3
  on:
4
- workflow_dispatch:
5
4
  release:
6
5
  types: [ published ]
7
6
 
@@ -13,11 +12,11 @@ jobs:
13
12
  contents: read
14
13
 
15
14
  steps:
16
- - uses: actions/checkout@v2
15
+ - uses: actions/checkout@v4
17
16
  - name: Set up Ruby 2.7
18
- uses: actions/setup-ruby@v1
17
+ uses: ruby/setup-ruby@v1
19
18
  with:
20
- ruby-version: 2.7.x
19
+ ruby-version: 2.7
21
20
 
22
21
  - name: Publish to RubyGems
23
22
  env:
data/.gitignore CHANGED
@@ -10,6 +10,7 @@
10
10
  /test/tmp/
11
11
  /test/version_tmp/
12
12
  /tmp/
13
+ .idea/*
13
14
 
14
15
  # Used by dotenv library to load environment variables.
15
16
  # .env
data/Gemfile CHANGED
@@ -1,4 +1,5 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
+ gem "activerecord", ">=7"
3
4
  # Specify your gem's dependencies in jit_preloader.gemspec
4
5
  gemspec
data/README.md CHANGED
@@ -173,6 +173,20 @@ end
173
173
 
174
174
  ```
175
175
 
176
+ Furthermore, there is an argument `max_ids_per_query` setting max ids per query. This helps prevent running a single query with too large list of ids which may be less efficient than splitting into multiple queries.
177
+ ```ruby
178
+ class Contact < ActiveRecord::Base
179
+ has_many :addresses
180
+ has_many_aggregate :addresses, :count_all, :count, "*", max_ids_per_query: 10
181
+ end
182
+
183
+ Contact.jit_preload.each do |contact|
184
+ contact.addresses_count_all
185
+ end
186
+ # SELECT contact_id, COUNT(*) FROM addresses WHERE contact_id IN (1, 2, 3, ... ,10) GROUP BY contact_id
187
+ # SELECT contact_id, COUNT(*) FROM addresses WHERE contact_id IN (11, 12, 13) GROUP BY contact_id
188
+ ```
189
+
176
190
  ### Preloading a subset of an association
177
191
 
178
192
  There are often times when you want to preload a subset of an association, or change how the SQL statement is generated. For example, if a `Contact` model has
@@ -213,6 +227,7 @@ end
213
227
  ### Jit preloading globally across your application
214
228
 
215
229
  The JitPreloader can be globally enabled, in which case most N+1 queries in your app should just disappear. It is off by default.
230
+ The `max_ids_per_query` argument on loading aggregate methods can also apply on a global level.
216
231
 
217
232
  ```ruby
218
233
  # Can be true or false
@@ -223,12 +238,24 @@ JitPreloader.globally_enabled = true
223
238
  # so that you can turn it on or off dynamically.
224
239
  JitPreloader.globally_enabled = ->{ $redis.get('always_jit_preload') == 'on' }
225
240
 
241
+ # Setting global max ids constraint on all aggregation methods.
242
+ JitPreloader.max_ids_per_query = 10
243
+
244
+ class Contact < ActiveRecord::Base
245
+ has_many :emails
246
+ has_many_aggregate :emails, :count_all, :count, "*"
247
+ end
248
+
226
249
  # When enabled globally, this would not generate an N+1 query.
227
250
  Contact.all.each do |contact|
228
251
  contact.emails.each do |email|
229
252
  # do something
230
253
  end
254
+ # When max_ides_per_query is set globally, the aggregate method will split query base on the limit.
255
+ contact.emails_count_all
231
256
  end
257
+
258
+
232
259
  ```
233
260
 
234
261
  ## What it doesn't solve
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_dependency "activerecord", ">= 5.2", "< 7"
21
+ spec.add_dependency "activerecord", "< 8"
22
22
  spec.add_dependency "activesupport"
23
23
 
24
24
  spec.add_development_dependency "bundler"
@@ -0,0 +1,63 @@
1
+ module JitPreloader
2
+ module PreloaderAssociation
3
+
4
+ # A monkey patch to ActiveRecord. The old method looked like the snippet
5
+ # below. Our changes here are that we remove records that are already
6
+ # part of the target, then attach all of the records to a new jit preloader.
7
+ #
8
+ # def run
9
+ # records = records_by_owner
10
+
11
+ # owners.each do |owner|
12
+ # associate_records_to_owner(owner, records[owner] || [])
13
+ # end if @associate
14
+
15
+ # self
16
+ # end
17
+
18
+ def run
19
+ return unless (reflection.scope.nil? || reflection.scope.arity == 0) && klass.ancestors.include?(ActiveRecord::Base)
20
+
21
+ super.tap do
22
+ if preloaded_records.any? && preloaded_records.none?(&:jit_preloader)
23
+ JitPreloader::Preloader.attach(preloaded_records) if owners.any?(&:jit_preloader) || JitPreloader.globally_enabled?
24
+ end
25
+ end
26
+ end
27
+
28
+ # Original method:
29
+ # def associate_records_to_owner(owner, records)
30
+ # return if loaded?(owner)
31
+ #
32
+ # association = owner.association(reflection.name)
33
+ #
34
+ # if reflection.collection?
35
+ # association.target = records
36
+ # else
37
+ # association.target = records.first
38
+ # end
39
+ # end
40
+ def associate_records_to_owner(owner, records)
41
+ return if loaded?(owner)
42
+
43
+ association = owner.association(reflection.name)
44
+
45
+ if reflection.collection?
46
+ new_records = association.target.any? ? records - association.target : records
47
+ association.target.concat(new_records)
48
+ association.loaded!
49
+ else
50
+ association.target = records.first
51
+ end
52
+ end
53
+
54
+ def build_scope
55
+ super.tap do |scope|
56
+ scope.jit_preload! if owners.any?(&:jit_preloader) || JitPreloader.globally_enabled?
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ ActiveRecord::Associations::Preloader::Association.prepend(JitPreloader::PreloaderAssociation)
63
+ ActiveRecord::Associations::Preloader::ThroughAssociation.prepend(JitPreloader::PreloaderAssociation)
@@ -0,0 +1,22 @@
1
+ module JitPreloader
2
+ module PreloaderBranch
3
+ """
4
+ ActiveRecord version >= 7.x.x introduced an improvement for preloading associations in batches:
5
+ https://github.com/rails/rails/blob/main/activerecord/lib/active_record/associations/preloader.rb#L121
6
+
7
+ Our existing monkey-patches will ignore associations whose classes are not descendants of
8
+ ActiveRecord::Base (example: https://github.com/clio/jit_preloader/blob/master/lib/jit_preloader/active_record/associations/preloader/ar6_association.rb#L19).
9
+ But this change breaks that behaviour because now Batch is calling `klass.base_class` (a method defined by ActiveRecord::Base)
10
+ before we have a chance to filter out the non-AR classes.
11
+ This patch for AR 7.x makes the Branch class ignore any association loaders that aren't for ActiveRecord::Base subclasses.
12
+ """
13
+
14
+ def loaders
15
+ @loaders = super.find_all do |loader|
16
+ loader.klass < ::ActiveRecord::Base
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ ActiveRecord::Associations::Preloader::Branch.prepend(JitPreloader::PreloaderBranch)
@@ -18,7 +18,7 @@ module JitPreloadExtension
18
18
  end
19
19
  end
20
20
 
21
- if Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new("6.0.0")
21
+ if Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new("7.0.0")
22
22
  def preload_scoped_relation(name:, base_association:, preload_scope: nil)
23
23
  return jit_preload_scoped_relations[name] if jit_preload_scoped_relations&.key?(name)
24
24
 
@@ -33,11 +33,11 @@ module JitPreloadExtension
33
33
  end
34
34
  end
35
35
 
36
- preloader_association = ActiveRecord::Associations::Preloader.new.preload(
37
- records,
38
- base_association,
39
- preload_scope
40
- ).first
36
+ preloader_association = ActiveRecord::Associations::Preloader.new(
37
+ records: records,
38
+ associations: base_association,
39
+ scope: preload_scope
40
+ ).call.first
41
41
 
42
42
  records.each do |record|
43
43
  record.jit_preload_scoped_relations ||= {}
@@ -90,7 +90,7 @@ module JitPreloadExtension
90
90
  class << base
91
91
  delegate :jit_preload, to: :all
92
92
 
93
- def has_many_aggregate(assoc, name, aggregate, field, table_alias_name: nil, default: 0)
93
+ def has_many_aggregate(assoc, name, aggregate, field, table_alias_name: nil, default: 0, max_ids_per_query: nil)
94
94
  method_name = "#{assoc}_#{name}"
95
95
 
96
96
  define_method(method_name) do |conditions={}|
@@ -101,6 +101,13 @@ module JitPreloadExtension
101
101
  if jit_preloader
102
102
  reflection = association(assoc).reflection
103
103
  primary_ids = jit_preloader.records.collect{|r| r[reflection.active_record_primary_key] }
104
+ max_ids_per_query = max_ids_per_query || JitPreloader.max_ids_per_query
105
+ if max_ids_per_query
106
+ slices = primary_ids.each_slice(max_ids_per_query)
107
+ else
108
+ slices = [primary_ids]
109
+ end
110
+
104
111
  klass = reflection.klass
105
112
 
106
113
  aggregate_association = reflection
@@ -115,15 +122,13 @@ module JitPreloadExtension
115
122
  table_reference = table_alias_name
116
123
  table_reference ||= association_scope.references_values.first || aggregate_association.table_name
117
124
 
118
- conditions[table_reference] = { aggregate_association.foreign_key => primary_ids }
119
-
120
125
  # If the association is a STI child model, specify its type in the condition so that it
121
126
  # doesn't include results from other child models
122
127
  parent_is_base_class = aggregate_association.klass.superclass.abstract_class? || aggregate_association.klass.superclass == ActiveRecord::Base
123
128
  has_type_column = aggregate_association.klass.column_names.include?(aggregate_association.klass.inheritance_column)
124
129
  is_child_sti_model = !parent_is_base_class && has_type_column
125
130
  if is_child_sti_model
126
- conditions[table_reference].merge!({ aggregate_association.klass.inheritance_column => aggregate_association.klass.sti_name })
131
+ conditions[table_reference] = { aggregate_association.klass.inheritance_column => aggregate_association.klass.sti_name }
127
132
  end
128
133
 
129
134
  if reflection.type.present?
@@ -131,11 +136,15 @@ module JitPreloadExtension
131
136
  end
132
137
  group_by = "#{table_reference}.#{aggregate_association.foreign_key}"
133
138
 
134
- preloaded_data = Hash[association_scope
135
- .where(conditions)
136
- .group(group_by)
137
- .send(aggregate, field)
138
- ]
139
+ preloaded_data = {}
140
+ slices.each do |slice|
141
+ data = Hash[association_scope
142
+ .where(conditions.deep_merge(table_reference => { aggregate_association.foreign_key => slice }))
143
+ .group(group_by)
144
+ .send(aggregate, field)
145
+ ]
146
+ preloaded_data.merge!(data)
147
+ end
139
148
 
140
149
  jit_preloader.records.each do |record|
141
150
  record.jit_preload_aggregates ||= {}
@@ -3,22 +3,47 @@ module JitPreloader
3
3
 
4
4
  attr_accessor :records
5
5
 
6
- def self.attach(records)
7
- new.tap do |loader|
8
- loader.records = records.dup
9
- records.each do |record|
10
- record.jit_preloader = loader
6
+ if Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new("7.0.0")
7
+ def self.attach(records)
8
+ new(records: records.dup, associations: nil).tap do |loader|
9
+ records.each do |record|
10
+ record.jit_preloader = loader
11
+ end
11
12
  end
12
13
  end
13
- end
14
14
 
15
- def jit_preload(association)
16
- # It is possible that the records array has multiple different classes (think single table inheritance).
17
- # Thus, it is possible that some of the records don't have an association.
18
- records_with_association = records.reject{|r| r.class.reflect_on_association(association).nil? }
19
- preload records_with_association, association
15
+ def jit_preload(associations)
16
+ # It is possible that the records array has multiple different classes (think single table inheritance).
17
+ # Thus, it is possible that some of the records don't have an association.
18
+ records_with_association = records.reject{|r| r.class.reflect_on_association(associations).nil? }
19
+
20
+ # Some of the records may already have the association loaded and we should not load them again
21
+ records_requiring_loading = records_with_association.select{|r| !r.association(associations).loaded? }
22
+
23
+ self.class.new(records: records_requiring_loading, associations: associations).call
24
+ end
25
+ else
26
+ def self.attach(records)
27
+ new.tap do |loader|
28
+ loader.records = records.dup
29
+ records.each do |record|
30
+ record.jit_preloader = loader
31
+ end
32
+ end
33
+ end
34
+
35
+ def jit_preload(associations)
36
+ # It is possible that the records array has multiple different classes (think single table inheritance).
37
+ # Thus, it is possible that some of the records don't have an association.
38
+ records_with_association = records.reject{ |record| record.class.reflect_on_association(associations).nil? }
39
+
40
+ # Some of the records may already have the association loaded and we should not load them again
41
+ records_requiring_loading = records_with_association.select{ |record| !record.association(associations).loaded? }
42
+ preload records_with_association, associations
43
+ end
20
44
  end
21
45
 
46
+
22
47
  # We do not want the jit_preloader to be dumpable
23
48
  # If you dump a ActiveRecord::Base object that has a jit_preloader instance variable
24
49
  # you will also end up dumping all of the records the preloader has reference to.
@@ -1,3 +1,3 @@
1
1
  module JitPreloader
2
- VERSION = "1.0.4"
2
+ VERSION = "2.1.0"
3
3
  end
data/lib/jit_preloader.rb CHANGED
@@ -8,7 +8,10 @@ require 'jit_preloader/active_record/base'
8
8
  require 'jit_preloader/active_record/relation'
9
9
  require 'jit_preloader/active_record/associations/collection_association'
10
10
  require 'jit_preloader/active_record/associations/singular_association'
11
- if Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new("6.0.0")
11
+ if Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new("7.0.0")
12
+ require 'jit_preloader/active_record/associations/preloader/ar7_association'
13
+ require 'jit_preloader/active_record/associations/preloader/ar7_branch'
14
+ elsif Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new("6.0.0")
12
15
  require 'jit_preloader/active_record/associations/preloader/ar6_association'
13
16
  elsif Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new("5.2.2")
14
17
  require 'jit_preloader/active_record/associations/preloader/ar5_association'
@@ -23,6 +26,16 @@ module JitPreloader
23
26
  @enabled = value
24
27
  end
25
28
 
29
+ def self.max_ids_per_query=(max_ids)
30
+ if max_ids && max_ids >= 1
31
+ @max_ids_per_query = max_ids
32
+ end
33
+ end
34
+
35
+ def self.max_ids_per_query
36
+ @max_ids_per_query
37
+ end
38
+
26
39
  def self.globally_enabled?
27
40
  if @enabled && @enabled.respond_to?(:call)
28
41
  @enabled.call
@@ -1,4 +1,5 @@
1
1
  require "spec_helper"
2
+ require "db-query-matchers"
2
3
 
3
4
  RSpec.describe JitPreloader::Preloader do
4
5
  let!(:contact1) do
@@ -479,6 +480,69 @@ RSpec.describe JitPreloader::Preloader do
479
480
  end
480
481
  end
481
482
  end
483
+
484
+ context "with dive limit set" do
485
+ let!(:contact_book_1) { ContactBook.create(name: "The Yellow Pages") }
486
+ let!(:contact_book_2) { ContactBook.create(name: "The Yellow Pages") }
487
+ let!(:contact_book_3) { ContactBook.create(name: "The Yellow Pages") }
488
+ let!(:company1) { Company.create(name: "Company1", contact_book: contact_book_1) }
489
+ let!(:company2) { Company.create(name: "Company2", contact_book: contact_book_1) }
490
+ let!(:company3) { Company.create(name: "Company2", contact_book: contact_book_2) }
491
+ let!(:company4) { Company.create(name: "Company4", contact_book: contact_book_3) }
492
+ let!(:company5) { Company.create(name: "Company5", contact_book: contact_book_3) }
493
+
494
+ context "from the global value" do
495
+ before do
496
+ JitPreloader.max_ids_per_query = 2
497
+ end
498
+
499
+ after do
500
+ JitPreloader.max_ids_per_query = nil
501
+ end
502
+
503
+ it "can handle queries" do
504
+ contact_books = ContactBook.jit_preload.to_a
505
+
506
+ expect(contact_books.first.companies_count).to eq 2
507
+ expect(contact_books.second.companies_count).to eq 1
508
+ expect(contact_books.last.companies_count).to eq 2
509
+ end
510
+
511
+ it "makes the right number of queries based on dive limit" do
512
+ contact_books = ContactBook.jit_preload.to_a
513
+ expect do
514
+ contact_books.first.companies_count
515
+ end.to make_database_queries(count: 2)
516
+
517
+ expect do
518
+ contact_books.second.companies_count
519
+ contact_books.last.companies_count
520
+ end.to_not make_database_queries
521
+ end
522
+ end
523
+
524
+ context "from aggregate argument" do
525
+ it "can handle queries" do
526
+ contact_books = ContactBook.jit_preload.to_a
527
+
528
+ expect(contact_books.first.companies_count_with_max_ids_set).to eq 2
529
+ expect(contact_books.second.companies_count_with_max_ids_set).to eq 1
530
+ expect(contact_books.last.companies_count_with_max_ids_set).to eq 2
531
+ end
532
+
533
+ it "makes the right number of queries based on dive limit" do
534
+ contact_books = ContactBook.jit_preload.to_a
535
+ expect do
536
+ contact_books.first.companies_count_with_max_ids_set
537
+ end.to make_database_queries(count: 2)
538
+
539
+ expect do
540
+ contact_books.second.companies_count_with_max_ids_set
541
+ contact_books.last.companies_count_with_max_ids_set
542
+ end.to_not make_database_queries
543
+ end
544
+ end
545
+ end
482
546
  end
483
547
 
484
548
  end
@@ -10,6 +10,7 @@ class ContactBook < ActiveRecord::Base
10
10
  has_many :children, through: :parents
11
11
 
12
12
  has_many_aggregate :companies, :count, :count, "*"
13
+ has_many_aggregate :companies, :count_with_max_ids_set, :count, "*", max_ids_per_query: 2
13
14
  has_many_aggregate :employees, :count, :count, "*"
14
15
  has_many_aggregate :company_employees, :count, :count, "*"
15
16
  has_many_aggregate :children, :count, :count, "*"
metadata CHANGED
@@ -1,35 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jit_preloader
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.4
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kyle d'Oliveira
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-10-14 00:00:00.000000000 Z
11
+ date: 2024-04-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '5.2'
20
17
  - - "<"
21
18
  - !ruby/object:Gem::Version
22
- version: '7'
19
+ version: '8'
23
20
  type: :runtime
24
21
  prerelease: false
25
22
  version_requirements: !ruby/object:Gem::Requirement
26
23
  requirements:
27
- - - ">="
28
- - !ruby/object:Gem::Version
29
- version: '5.2'
30
24
  - - "<"
31
25
  - !ruby/object:Gem::Version
32
- version: '7'
26
+ version: '8'
33
27
  - !ruby/object:Gem::Dependency
34
28
  name: activesupport
35
29
  requirement: !ruby/object:Gem::Requirement
@@ -151,7 +145,8 @@ executables: []
151
145
  extensions: []
152
146
  extra_rdoc_files: []
153
147
  files:
154
- - ".github/workflows/CODEOWNERS"
148
+ - ".github/CODEOWNERS"
149
+ - ".github/workflows/ci.yml"
155
150
  - ".github/workflows/gem-push.yml"
156
151
  - ".gitignore"
157
152
  - ".rspec"
@@ -170,6 +165,8 @@ files:
170
165
  - lib/jit_preloader/active_record/associations/collection_association.rb
171
166
  - lib/jit_preloader/active_record/associations/preloader/ar5_association.rb
172
167
  - lib/jit_preloader/active_record/associations/preloader/ar6_association.rb
168
+ - lib/jit_preloader/active_record/associations/preloader/ar7_association.rb
169
+ - lib/jit_preloader/active_record/associations/preloader/ar7_branch.rb
173
170
  - lib/jit_preloader/active_record/associations/preloader/collection_association.rb
174
171
  - lib/jit_preloader/active_record/associations/preloader/singular_association.rb
175
172
  - lib/jit_preloader/active_record/associations/singular_association.rb
File without changes