Find out build_association behavior in Rails
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)