Module: ViewModel::ActiveRecord::AssociationManipulation
- Extended by:
- ActiveSupport::Concern
- Included in:
- ViewModel::ActiveRecord
- Defined in:
- lib/view_model/active_record/association_manipulation.rb
Overview
Mix-in for VM::ActiveRecord providing direct manipulation of directly-associated entities. Avoids loading entire collections.
Instance Method Summary collapse
-
#append_associated(association_name, subtree_hash_or_hashes, references: {}, before: nil, after: nil, deserialize_context: self.class.new_deserialize_context) ⇒ Object
Create or update members of a associated collection.
-
#delete_associated(association_name, associated_id, type: nil, deserialize_context: self.class.new_deserialize_context) ⇒ Object
Removes the association between the models represented by this viewmodel and the provided associated viewmodel.
- #load_associated(association_name, scope: nil, eager_include: true, serialize_context: self.class.new_serialize_context) ⇒ Object
-
#replace_associated(association_name, update_hash, references: {}, deserialize_context: self.class.new_deserialize_context) ⇒ Object
Replace the current member(s) of an association with the provided hash(es).
Instance Method Details
#append_associated(association_name, subtree_hash_or_hashes, references: {}, before: nil, after: nil, deserialize_context: self.class.new_deserialize_context) ⇒ Object
Create or update members of a associated collection. For an ordered collection, the items are inserted either before ‘before`, after `after`, or at the end.
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 |
# File 'lib/view_model/active_record/association_manipulation.rb', line 116 def append_associated(association_name, subtree_hash_or_hashes, references: {}, before: nil, after: nil, deserialize_context: self.class.new_deserialize_context) if self.changes.changed? raise ArgumentError.new('Invalid call to append_associated on viewmodel with pending changes') end association_data = self.class._association_data(association_name) direct_reflection = association_data.direct_reflection raise ArgumentError.new("Cannot append to single association '#{association_name}'") unless association_data.collection? ViewModel::Utils.wrap_one_or_many(subtree_hash_or_hashes) do |subtree_hashes| model_class.transaction do ViewModel::Callbacks.wrap_deserialize(self, deserialize_context: deserialize_context) do |hook_control| association_changed!(association_name) deserialize_context.run_callback(ViewModel::Callbacks::Hook::BeforeValidate, self) if association_data.through? raise ArgumentError.new('Polymorphic through relationships not supported yet') if association_data.polymorphic? direct_viewmodel_class = association_data.direct_viewmodel root_update_data, referenced_update_data = construct_indirect_append_updates(association_data, subtree_hashes, references) else raise ArgumentError.new('Polymorphic STI relationships not supported yet') if association_data.polymorphic? direct_viewmodel_class = association_data.viewmodel_class root_update_data, referenced_update_data = construct_direct_append_updates(association_data, subtree_hashes, references) end update_context = ViewModel::ActiveRecord::UpdateContext.build!(root_update_data, referenced_update_data, root_type: direct_viewmodel_class) # Set new parent new_parent = ViewModel::ActiveRecord::UpdateOperation::ParentData.new(direct_reflection.inverse_of, self) update_context.root_updates.each { |update| update.reparent_to = new_parent } # Set place in list. if association_data.ordered? new_positions = select_append_positions(association_data, direct_viewmodel_class._list_attribute_name, update_context.root_updates.count, before: before, after: after) update_context.root_updates.zip(new_positions).each do |update, new_pos| update.reposition_to = new_pos end end # Because append_associated can take from other parents, edit-check previous parents (other than this model) unless association_data.through? inverse_assoc_name = direct_reflection.inverse_of.name previous_parent_ids = Set.new update_context.root_updates.each do |update| update_model = update.viewmodel.model parent_model_id = update_model.read_attribute(update_model .association(inverse_assoc_name) .reflection.foreign_key) if parent_model_id && parent_model_id != self.id previous_parent_ids << parent_model_id end end if previous_parent_ids.present? previous_parents = self.class.find(previous_parent_ids.to_a, eager_include: false) previous_parents.each do |parent_view| ViewModel::Callbacks.wrap_deserialize(parent_view, deserialize_context: deserialize_context) do |pp_hook_control| changes = ViewModel::Changes.new(changed_associations: [association_name]) deserialize_context.run_callback(ViewModel::Callbacks::Hook::OnChange, parent_view, changes: changes) pp_hook_control.record_changes(changes) end end end end child_context = self.context_for_child(association_name, context: deserialize_context) updated_viewmodels = update_context.run!(deserialize_context: child_context) # Propagate changes and finalize the parent updated_viewmodels.each do |child| child_changes = child.previous_changes if association_data.nested? nested_children_changed! if child_changes.changed_nested_tree? referenced_children_changed! if child_changes.changed_referenced_children? elsif association_data.owned? referenced_children_changed! if child_changes.changed_owned_tree? end end final_changes = self.clear_changes! if association_data.through? updated_viewmodels.map! do |direct_vm| direct_vm._read_association(association_data.indirect_reflection.name) end end # Could happen if hooks attempted to change the parent, which aren't # valid since we're only editing children here. unless final_changes.contained_to?(associations: [association_name.to_s]) raise ViewModel::DeserializationError::InvalidParentEdit.new(final_changes, blame_reference) end deserialize_context.run_callback(ViewModel::Callbacks::Hook::OnChange, self, changes: final_changes) hook_control.record_changes(final_changes) updated_viewmodels end end end end |
#delete_associated(association_name, associated_id, type: nil, deserialize_context: self.class.new_deserialize_context) ⇒ Object
Removes the association between the models represented by this viewmodel and the provided associated viewmodel. The associated model will be garbage-collected if the assocation is specified with ‘dependent: :destroy` or `:delete_all`
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 |
# File 'lib/view_model/active_record/association_manipulation.rb', line 232 def delete_associated(association_name, associated_id, type: nil, deserialize_context: self.class.new_deserialize_context) if self.changes.changed? raise ArgumentError.new('Invalid call to delete_associated on viewmodel with pending changes') end association_data = self.class._association_data(association_name) direct_reflection = association_data.direct_reflection unless association_data.collection? raise ArgumentError.new("Cannot remove element from single association '#{association_name}'") end check_association_type!(association_data, type) target_ref = ViewModel::Reference.new(type || association_data.viewmodel_class, associated_id) model_class.transaction do ViewModel::Callbacks.wrap_deserialize(self, deserialize_context: deserialize_context) do |hook_control| association_changed!(association_name) deserialize_context.run_callback(ViewModel::Callbacks::Hook::BeforeValidate, self) association = self.model.association(direct_reflection.name) association_scope = association.scope if association_data.through? raise ArgumentError.new('Polymorphic through relationships not supported yet') if association_data.polymorphic? direct_viewmodel = association_data.direct_viewmodel association_scope = association_scope.where(association_data.indirect_reflection.foreign_key => associated_id) else raise ArgumentError.new('Polymorphic STI relationships not supported yet') if association_data.polymorphic? # viewmodel type for current association: nil in case of empty polymorphic association direct_viewmodel = association.klass.try { |k| association_data.viewmodel_class_for_model!(k) } if association_data.pointer_location == :local # If we hold the pointer, we can immediately check if the type and id match. if target_ref != ViewModel::Reference.new(direct_viewmodel, model.read_attribute(direct_reflection.foreign_key)) raise ViewModel::DeserializationError::AssociatedNotFound.new(association_name.to_s, target_ref, blame_reference) end else # otherwise add the target constraint to the association scope association_scope = association_scope.where(id: associated_id) end end models = association_scope.to_a if models.blank? raise ViewModel::DeserializationError::AssociatedNotFound.new(association_name.to_s, target_ref, blame_reference) elsif models.size > 1 raise ViewModel::DeserializationError::Internal.new( "Internal error: encountered multiple records for #{target_ref} in association #{association_name}", blame_reference) end child_context = self.context_for_child(association_name, context: deserialize_context) child_vm = direct_viewmodel.new(models.first) ViewModel::Callbacks.wrap_deserialize(child_vm, deserialize_context: child_context) do |child_hook_control| changes = ViewModel::Changes.new(deleted: true) child_context.run_callback(ViewModel::Callbacks::Hook::OnChange, child_vm, changes: changes) child_hook_control.record_changes(changes) association.delete(child_vm.model) end if association_data.nested? nested_children_changed! elsif association_data.owned? referenced_children_changed! end final_changes = self.clear_changes! unless final_changes.contained_to?(associations: [association_name.to_s]) raise ViewModel::DeserializationError::InvalidParentEdit.new(final_changes, blame_reference) end deserialize_context.run_callback(ViewModel::Callbacks::Hook::OnChange, self, changes: final_changes) hook_control.record_changes(final_changes) child_vm end end end |
#load_associated(association_name, scope: nil, eager_include: true, serialize_context: self.class.new_serialize_context) ⇒ Object
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
# File 'lib/view_model/active_record/association_manipulation.rb', line 8 def load_associated(association_name, scope: nil, eager_include: true, serialize_context: self.class.new_serialize_context) association_data = self.class._association_data(association_name) direct_reflection = association_data.direct_reflection association = self.model.association(direct_reflection.name) association_scope = association.scope if association_data.through? raise ArgumentError.new('Polymorphic through relationships not supported yet') if association_data.polymorphic? associated_viewmodel = association_data.viewmodel_class direct_viewmodel = association_data.direct_viewmodel else raise ArgumentError.new('Polymorphic STI relationships not supported yet') if association_data.polymorphic? associated_viewmodel = association.klass.try { |k| association_data.viewmodel_class_for_model!(k) } direct_viewmodel = associated_viewmodel end if association_data.ordered? association_scope = association_scope.order(direct_viewmodel._list_attribute_name) end if association_data.through? association_scope = associated_viewmodel.model_class .joins(association_data.indirect_reflection.inverse_of.name) .merge(association_scope) end association_scope = association_scope.merge(scope) if scope vms = association_scope.map { |model| associated_viewmodel.new(model) } ViewModel.preload_for_serialization(vms) if eager_include if association_data.collection? vms else if vms.size > 1 raise ViewModel::DeserializationError::Internal.new("Internal error: encountered multiple records for single association #{association_name}", self.blame_reference) end vms.first end end |
#replace_associated(association_name, update_hash, references: {}, deserialize_context: self.class.new_deserialize_context) ⇒ Object
Replace the current member(s) of an association with the provided hash(es). Only mentioned member(s) will be returned.
This interface deals with associations directly where reasonable, with the notable exception of referenced+shared associations. That is to say, that owned associations should be presented in the form of direct update hashes, regardless of their referencing. Reference and shared associations are excluded to ensure that the update hash for a shared entity is unique, and that edits may only be specified once.
64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/view_model/active_record/association_manipulation.rb', line 64 def replace_associated(association_name, update_hash, references: {}, deserialize_context: self.class.new_deserialize_context) _updated_parent, changed_children = self.class.replace_associated_bulk( association_name, { self.id => update_hash }, references: references, deserialize_context: deserialize_context ).first changed_children end |