Find out build_association behavior in Rails

KeisukeKoshikawa
3 min readOct 11, 2022

You can use build_association_name method when you define has_one association.

For example, Define User model and Reservation model in your Rails app that you define user has_one reservation. Then User model can use build_reservation method.

In this article, Find out build_association behavior in Rails App.

Environment

Ruby 2.7.6

Rails 6.0.5.1

Let’s Read Source Codes

build_association define in singular_association.rb

# Defines the (build|create)_association methods for belongs_to or has_one associationdef self.define_constructors(mixin, name)
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
def build_#{name}(*args, &block)
association(:#{name}).build(*args, &block)
end
def create_#{name}(*args, &block)
association(:#{name}).create(*args, &block)
end
def create_#{name}!(*args, &block)
association(:#{name}).create!(*args, &block)
end
CODE
end

Define methods dynamically with class_eval.

What will happen when it is actually called? Let`s debug the code!

lib/active_record/associations/builder/singular_association.rb:29

28: def build_#{name}(*args, &block)
=> 29: association(:#{name}).build(*args, &block)
30: end

If you use binding.pry you execute step method.

lib/active_record/associations/singular_association.rb:21

20: def build(attributes = {}, &block)
=> 21: record = build_record(attributes, &block)
22: set_new_record(record)
23: record
24: end

The build_record is the method that actually generates the instance of the related destination. We’ll also look at the set_new_record method.

lib/active_record/associations/has_one_association.rb:75

74: def set_new_record(record)
=> 75: replace(record, false)
76: end

The replace method is defined here.

42: def replace(record, save = true)
=> 43: raise_on_type_mismatch!(record) if record
44:
45: return target unless load_target || record
46:
47: assigning_another_record = target != record
48: if assigning_another_record || record.has_changes_to_save?
49: save &&= owner.persisted?
50:
51: transaction_if(save) do
52: remove_target!(options[:dependent]) if target && !target.destroyed? && assigning_another_record
53:
54: if record
55: set_owner_attributes(record)
56: set_inverse_instance(record)
57:
58: if save && !record.save
59: nullify_owner_attributes(record)
60: set_owner_attributes(target) if target
61: raise RecordNotSaved, "Failed to save the new associated #{reflection.name}."
62: end
63: end
64: end
65: end
66:
67: self.target = record
68: end

After doing some validation, if the target (related destination record) already exists, the related destination record has not been deleted, and there is a new related destination record, the remove_target! method is called.

Judging from the method name, I think that it is a method to delete the record of the related destination that already exists, but let’s follow the method.

78: def remove_target!(method)
=> 79: case method
80: when :delete
81: target.delete
82: when :destroy
83: target.destroyed_by_association = reflection
84: target.destroy
85: else
86: nullify_owner_attributes(target)
87: remove_inverse_instance(target)
88:
89: if target.persisted? && owner.persisted? && !target.save
90: set_owner_attributes(target)
91: raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " \
92: "The record failed to save after its foreign key was set to nil."
93: end
94: end
95: end

The remove_target! method deletes the record of the association that was created in the record of the related destination on line 82, and deletes the record of the related destination on line 83.

Conclusion

The build_association method used to delete the record and create a new instance if the associated record (reservation in this case) already existed.

When calling build_association at the time of updating, it seems better to execute after keeping in mind the behavior. (May cause unexpected bugs)

--

--

KeisukeKoshikawa
KeisukeKoshikawa

Written by KeisukeKoshikawa

Web Developer in Japan. I’ve been developing with ROR for less than two years. also have experience in maintaining and operating SPA with Angular(v2 ~ ).

No responses yet