Skip to content

Commit

Permalink
Read has_one through association target from memory for new records.
Browse files Browse the repository at this point in the history
When reading an `has_one` through association for a new record and loading from DB
is not possible retrieves the association target from the in memory through association.

Fixes #42387
  • Loading branch information
intrip committed Jun 25, 2021
1 parent 6ae78e9 commit bef7d67
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 1 deletion.
27 changes: 27 additions & 0 deletions activerecord/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
* Read `has_one` through association target from memory for new records.

When reading an `has_one` through association for a new record and loading from DB
is not possible retrieves the association target from the in memory through association:

```ruby
class Book < ActiveRecord::Base
belongs_to :author
end

class Author < ActiveRecord::Base
end

class Reader < ActiveRecord::Base
has_one :book
has_one :author, through: :book
end

author = Author.new
book = Book.new(author: author)
reader = Reader.new(book: book)
reader.author
# => correctly returns `author`, previously returns `nil`.
```

*Jacopo Beschi*

* Allow preloading of associations with instance dependent scopes

*John Hawthorn*, *John Crepezzi*, *Adam Hess*, *Eileen M. Uchitelle*, *Dinah Shi*
Expand Down
6 changes: 5 additions & 1 deletion activerecord/lib/active_record/associations/association.rb
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ def extensions
# ActiveRecord::RecordNotFound is rescued within the method, and it is
# not reraised. The proxy is \reset and +nil+ is the return value.
def load_target
@target = find_target if (@stale_state && stale_target?) || find_target?
@target = find_target if load_target?

loaded! unless loaded?
target
Expand Down Expand Up @@ -277,6 +277,10 @@ def scope_for_create
scope.scope_for_create
end

def load_target?
(@stale_state && stale_target?) || find_target?
end

def find_target?
!loaded? && (!owner.new_record? || foreign_key_present?) && klass
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ module Associations
class HasOneThroughAssociation < HasOneAssociation #:nodoc:
include ThroughAssociation

def reader
if load_target_from_memory?
self.target = find_target_from_memory
else
super
end
end

private
def replace(record, save = true)
create_through_record(record, save)
Expand Down Expand Up @@ -40,6 +48,16 @@ def create_through_record(record, save)
end
end
end

def load_target_from_memory?
!load_target? && !loaded? && owner.new_record?
end

# Recursively reads the through_associations and collects their values.
def find_target_from_memory
through_record = owner.association(reflection.through_reflection.name).reader
through_record&.association(source_reflection.name)&.reader
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -445,4 +445,14 @@ def test_has_one_through_do_not_cache_association_reader_if_the_though_method_ha
ensure
CustomerCarrier.current_customer = nil
end

def test_has_one_through_loads_target_from_memory
category = Category.new
club = Club.new(name: "LRUG", category: category)
current_membership = CurrentMembership.new(club: club)
new_member = Member.new(name: "Chris", current_membership: current_membership)

assert_equal club, new_member.club
assert_equal category, new_member.club_category
end
end

0 comments on commit bef7d67

Please sign in to comment.