Class: ViewModel::ActiveRecord
- Includes:
- AssociationManipulation
- Defined in:
- lib/view_model/active_record/update_context.rb,
lib/view_model/active_record.rb,
lib/view_model/active_record/update_data.rb,
lib/view_model/active_record/update_operation.rb
Overview
Partially parsed tree of user-specified update hashes, created during deserialization.
Defined Under Namespace
Modules: AssociationManipulation, CollectionNestedController, Controller, ControllerBase, NestedControllerBase, SingularNestedController Classes: AbstractCollectionUpdate, AssociationData, Cache, Cloner, FunctionalUpdate, OwnedCollectionUpdate, ReferencedCollectionUpdate, UpdateContext, UpdateData, UpdateOperation, Visitor
Constant Summary collapse
- FUNCTIONAL_UPDATE_TYPE =
for functional updates
'_update'
- ACTIONS_ATTRIBUTE =
'actions'
- VALUES_ATTRIBUTE =
'values'
- BEFORE_ATTRIBUTE =
'before'
- AFTER_ATTRIBUTE =
'after'
Constants inherited from ViewModel
BULK_UPDATES_ATTRIBUTE, BULK_UPDATE_ATTRIBUTE, BULK_UPDATE_TYPE, ID_ATTRIBUTE, MIGRATED_ATTRIBUTE, NEW_ATTRIBUTE, REFERENCE_ATTRIBUTE, TYPE_ATTRIBUTE, VERSION_ATTRIBUTE
Class Attribute Summary collapse
-
._list_attribute_name ⇒ Object
readonly
Returns the value of attribute _list_attribute_name.
Instance Attribute Summary collapse
-
#changed_associations ⇒ Object
readonly
Returns the value of attribute changed_associations.
Attributes inherited from Record
#changed_attributes, #model, #previous_changes
Class Method Summary collapse
-
._association_data(association_name) ⇒ Object
internal.
- ._list_member? ⇒ Boolean
-
.acts_as_list(attr = :position) ⇒ Object
Specifies that the model backing this viewmodel is a member of an ‘acts_as_manual_list` collection.
-
.association(association_name, as: nil, viewmodel: nil, viewmodels: nil, external: false, read_only: false, through: nil, through_order_attr: nil) ⇒ Object
Adds an association from the model to this viewmodel.
-
.associations(*assocs, **args) ⇒ Object
Specify multiple associations at once.
- .cacheable!(**opts) ⇒ Object
- .deep_schema_version(include_referenced: true, include_external: true) ⇒ Object
- .dependent_viewmodels(seen = Set.new, include_referenced: true, include_external: true) ⇒ Object
- .deserialize_from_view(subtree_hash_or_hashes, references: {}, deserialize_context: new_deserialize_context) ⇒ Object
-
.eager_includes(include_referenced: true, vm_path: []) ⇒ Object
Constructs a preload specification of the required models for serializing/deserializing this view.
-
.find(id_or_ids, scope: nil, lock: nil, eager_include: true) ⇒ Object
Load instances of the viewmodel by id(s).
-
.load(scope: nil, eager_include: true, lock: nil) ⇒ Object
Load instances of the viewmodel by scope TODO: is this too much of a encapsulation violation?.
-
.model_previously_new?(model) ⇒ Boolean
Rails 6.1 introduced “previously_new_record?”, but this library still supports activerecord >= 5.0.
Instance Method Summary collapse
- #_read_association(association_name) ⇒ Object
-
#_read_association_touched(association_name, touched_ids:) ⇒ Object
Helper to return entities that were part of the last deserialization.
- #_serialize_association(association_name, json, serialize_context:) ⇒ Object
- #association_changed!(association_name) ⇒ Object
- #associations_changed? ⇒ Boolean
-
#changes ⇒ Object
Additionally pass ‘changed_associations` while constructing changes.
- #clear_changes! ⇒ Object
- #context_for_child(member_name, context:) ⇒ Object
- #destroy!(deserialize_context: self.class.new_deserialize_context) ⇒ Object
-
#initialize ⇒ ActiveRecord
constructor
A new instance of ActiveRecord.
- #serialize_members(json, serialize_context: self.class.new_serialize_context) ⇒ Object
Methods included from AssociationManipulation
#append_associated, #delete_associated, #load_associated, #replace_associated
Methods inherited from Record
#==, attribute, #attribute_changed!, #changed_nested_children?, #changed_referenced_children?, deserialize_members_from_view, for_new_model, #hash, #id, inherited, initialize_as_viewmodel_record, member_names, model_class, #model_is_new!, #nested_children_changed!, #new_model?, #referenced_children_changed!, resolve_viewmodel, #serialize_view, should_register?, #stable_id?, #validate!
Methods inherited from ViewModel
#==, accepts_schema_version?, add_view_alias, attribute, attributes, #blame_reference, deserialization_includes, deserialize_context_class, deserialize_members_from_view, encode_json, extract_reference_metadata, extract_reference_only_metadata, extract_viewmodel_metadata, #hash, #id, inherited, initialize_as_viewmodel, is_update_hash?, lock_attribute_inheritance, member_names, #model, new_deserialize_context, new_serialize_context, #preload_for_serialization, preload_for_serialization, root!, root?, schema_hash, schema_versions, #serialize, serialize, serialize_as_reference, serialize_context_class, serialize_from_cache, serialize_to_hash, #serialize_to_hash, #serialize_view, #stable_id?, #to_json, #to_reference, #validate!, #view_name
Constructor Details
#initialize ⇒ ActiveRecord
Returns a new instance of ActiveRecord.
264 265 266 267 268 |
# File 'lib/view_model/active_record.rb', line 264 def initialize(*) super model_is_new! if model.new_record? @changed_associations = [] end |
Class Attribute Details
._list_attribute_name ⇒ Object (readonly)
Returns the value of attribute _list_attribute_name.
37 38 39 |
# File 'lib/view_model/active_record.rb', line 37 def _list_attribute_name @_list_attribute_name end |
Instance Attribute Details
#changed_associations ⇒ Object (readonly)
Returns the value of attribute changed_associations.
34 35 36 |
# File 'lib/view_model/active_record.rb', line 34 def changed_associations @changed_associations end |
Class Method Details
._association_data(association_name) ⇒ Object
internal
256 257 258 259 260 261 |
# File 'lib/view_model/active_record.rb', line 256 def _association_data(association_name) association_data = self._members[association_name.to_s] raise ArgumentError.new("Invalid association '#{association_name}'") unless association_data.is_a?(AssociationData) association_data end |
._list_member? ⇒ Boolean
57 58 59 |
# File 'lib/view_model/active_record.rb', line 57 def _list_member? _list_attribute_name.present? end |
.acts_as_list(attr = :position) ⇒ Object
Specifies that the model backing this viewmodel is a member of an ‘acts_as_manual_list` collection.
43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/view_model/active_record.rb', line 43 def acts_as_list(attr = :position) @_list_attribute_name = attr @generated_accessor_module.module_eval do define_method('_list_attribute') do model.public_send(attr) end define_method('_list_attribute=') do |x| model.public_send(:"#{attr}=", x) end end end |
.association(association_name, as: nil, viewmodel: nil, viewmodels: nil, external: false, read_only: false, through: nil, through_order_attr: nil) ⇒ Object
Adds an association from the model to this viewmodel. The associated model will be recursively (de)serialized by its own viewmodel type, which will be inferred from the model name, or may be explicitly specified.
An association to a root viewmodel type will be serialized with an indirect reference, while a child viewmodel type will be directly nested.
-
as
sets the name of the association in the viewmodel -
viewmodel
,viewmodels
specifies the viewmodel(s) to use for the association -
external
indicates an association external to the view. Externalized associations are not included in (de)serializations of the parent, and must be independently manipulated using ‘AssociationManipulation`. External associations may only be made to root viewmodels. -
through
names an ActiveRecord association that will be used like an ActiveRecordhas_many:through:
. -
through_order_attr
the through model is ordered by the given attribute (only applies to whenthrough
is set).
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/view_model/active_record.rb', line 83 def association(association_name, as: nil, viewmodel: nil, viewmodels: nil, external: false, read_only: false, through: nil, through_order_attr: nil) vm_association_name = (as || association_name).to_s if through direct_association_name = through indirect_association_name = association_name else direct_association_name = association_name indirect_association_name = nil end target_viewmodels = Array.wrap(viewmodel || viewmodels) association_data = AssociationData.new( owner: self, association_name: vm_association_name, direct_association_name: direct_association_name, indirect_association_name: indirect_association_name, target_viewmodels: target_viewmodels, external: external, read_only: read_only, through_order_attr: through_order_attr) _members[vm_association_name] = association_data @generated_accessor_module.module_eval do define_method vm_association_name do _read_association(vm_association_name) end define_method :"serialize_#{vm_association_name}" do |json, serialize_context: self.class.new_serialize_context| _serialize_association(vm_association_name, json, serialize_context: serialize_context) end end end |
.associations(*assocs, **args) ⇒ Object
Specify multiple associations at once
128 129 130 |
# File 'lib/view_model/active_record.rb', line 128 def associations(*assocs, **args) assocs.each { |assoc| association(assoc, **args) } end |
.cacheable!(**opts) ⇒ Object
250 251 252 253 |
# File 'lib/view_model/active_record.rb', line 250 def cacheable!(**opts) include ViewModel::ActiveRecord::Cache::CacheableView create_viewmodel_cache!(**opts) end |
.deep_schema_version(include_referenced: true, include_external: true) ⇒ Object
242 243 244 245 246 247 248 |
# File 'lib/view_model/active_record.rb', line 242 def deep_schema_version(include_referenced: true, include_external: true) (@deep_schema_version ||= {})[[include_referenced, include_external]] ||= begin vms = dependent_viewmodels(include_referenced: include_referenced, include_external: include_external) ViewModel.schema_versions(vms).freeze end end |
.dependent_viewmodels(seen = Set.new, include_referenced: true, include_external: true) ⇒ Object
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 |
# File 'lib/view_model/active_record.rb', line 224 def dependent_viewmodels(seen = Set.new, include_referenced: true, include_external: true) return if seen.include?(self) seen << self _members.each_value do |data| next unless data.is_a?(AssociationData) next unless include_referenced || !data.referenced? next unless include_external || !data.external? data.viewmodel_classes.each do |vm| vm.dependent_viewmodels(seen, include_referenced: include_referenced, include_external: include_external) end end seen end |
.deserialize_from_view(subtree_hash_or_hashes, references: {}, deserialize_context: new_deserialize_context) ⇒ Object
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
# File 'lib/view_model/active_record.rb', line 166 def deserialize_from_view(subtree_hash_or_hashes, references: {}, deserialize_context: new_deserialize_context) model_class.transaction do ViewModel::Utils.wrap_one_or_many(subtree_hash_or_hashes) do |subtree_hashes| root_update_data, referenced_update_data = UpdateData.parse_hashes(subtree_hashes, references) _updated_viewmodels = UpdateContext .build!(root_update_data, referenced_update_data, root_type: self) .run!(deserialize_context: deserialize_context) end end rescue ViewModel::DeserializationError => e if (new_error = customize_deserialization_error(e)) raise new_error else raise end end |
.eager_includes(include_referenced: true, vm_path: []) ⇒ Object
Constructs a preload specification of the required models for serializing/deserializing this view. Cycles in the schema will be broken after two layers of eager loading.
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 |
# File 'lib/view_model/active_record.rb', line 188 def eager_includes(include_referenced: true, vm_path: []) association_specs = {} return nil if vm_path.count(self) > 2 child_path = vm_path + [self] _members.each do |assoc_name, association_data| next unless association_data.is_a?(AssociationData) next if association_data.external? case when association_data.through? viewmodel = association_data.direct_viewmodel children = viewmodel.eager_includes(include_referenced: include_referenced, vm_path: child_path) when !include_referenced && association_data.referenced? children = nil # Load up to the root viewmodel, but no further when association_data.polymorphic? children_by_klass = {} association_data.viewmodel_classes.each do |vm_class| klass = vm_class.model_class.name children_by_klass[klass] = vm_class.eager_includes(include_referenced: include_referenced, vm_path: child_path) end children = DeepPreloader::PolymorphicSpec.new(children_by_klass) else viewmodel = association_data.viewmodel_class children = viewmodel.eager_includes(include_referenced: include_referenced, vm_path: child_path) end association_specs[association_data.direct_reflection.name.to_s] = children end DeepPreloader::Spec.new(association_specs) end |
.find(id_or_ids, scope: nil, lock: nil, eager_include: true) ⇒ Object
Load instances of the viewmodel by id(s)
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/view_model/active_record.rb', line 133 def find(id_or_ids, scope: nil, lock: nil, eager_include: true) find_scope = self.model_class.all find_scope = find_scope.order(:id).lock(lock) if lock find_scope = find_scope.merge(scope) if scope ViewModel::Utils.wrap_one_or_many(id_or_ids) do |ids| models = find_scope.where(id: ids).to_a if models.size < ids.size missing_ids = ids - models.map(&:id) if missing_ids.present? raise ViewModel::DeserializationError::NotFound.new( missing_ids.map { |id| ViewModel::Reference.new(self, id) }) end end vms = models.map { |m| self.new(m) } ViewModel.preload_for_serialization(vms, lock: lock) if eager_include vms end end |
.load(scope: nil, eager_include: true, lock: nil) ⇒ Object
Load instances of the viewmodel by scope TODO: is this too much of a encapsulation violation?
157 158 159 160 161 162 163 164 |
# File 'lib/view_model/active_record.rb', line 157 def load(scope: nil, eager_include: true, lock: nil) load_scope = self.model_class.all load_scope = load_scope.lock(lock) if lock load_scope = load_scope.merge(scope) if scope vms = load_scope.map { |model| self.new(model) } ViewModel.preload_for_serialization(vms, lock: lock) if eager_include vms end |
.model_previously_new?(model) ⇒ Boolean
Rails 6.1 introduced “previously_new_record?”, but this library still supports activerecord >= 5.0. This is an approximation.
376 377 378 379 380 381 382 |
# File 'lib/view_model/active_record.rb', line 376 def self.model_previously_new?(model) if (id_changes = model.saved_change_to_id) old_id, _new_id = id_changes return true if old_id.nil? end false end |
Instance Method Details
#_read_association(association_name) ⇒ Object
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 |
# File 'lib/view_model/active_record.rb', line 334 def _read_association(association_name) association_data = self.class._association_data(association_name) associated = model.public_send(association_data.direct_reflection.name) return nil if associated.nil? case when association_data.through? # associated here are join-table models; we need to get the far side out join_models = associated if association_data.ordered? attr = association_data.direct_viewmodel._list_attribute_name join_models = join_models.sort_by { |j| j[attr] } end join_models.map do |through_model| model = through_model.public_send(association_data.indirect_reflection.name) association_data.viewmodel_class_for_model!(model.class).new(model) end when association_data.collection? associated_viewmodels = associated.map do |x| associated_viewmodel_class = association_data.viewmodel_class_for_model!(x.class) associated_viewmodel_class.new(x) end # If any associated type is a list member, they must all be if association_data.ordered? associated_viewmodels.sort_by!(&:_list_attribute) end associated_viewmodels else associated_viewmodel_class = association_data.viewmodel_class_for_model!(associated.class) associated_viewmodel_class.new(associated) end end |
#_read_association_touched(association_name, touched_ids:) ⇒ Object
Helper to return entities that were part of the last deserialization. The interface is complex due to the data requirements, and the implementation is inefficient.
Intended to be used by replace_associated style methods which may touch very large collections that must not be returned fully. Since the collection is not being returned, order is also ignored.
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 |
# File 'lib/view_model/active_record.rb', line 391 def _read_association_touched(association_name, touched_ids:) association_data = self.class._association_data(association_name) associated = model.public_send(association_data.direct_reflection.name) return nil if associated.nil? case when association_data.through? # associated here are join-table models; we need to get the far side out associated.map do |through_model| model = through_model.public_send(association_data.indirect_reflection.name) next unless self.class.model_previously_new?(through_model) || touched_ids.include?(model.id) association_data.viewmodel_class_for_model!(model.class).new(model) end.reject(&:nil?) when association_data.collection? associated.map do |model| next unless self.class.model_previously_new?(model) || touched_ids.include?(model.id) association_data.viewmodel_class_for_model!(model.class).new(model) end.reject(&:nil?) else # singleton always touched by definition model = associated association_data.viewmodel_class_for_model!(model.class).new(model) end end |
#_serialize_association(association_name, json, serialize_context:) ⇒ Object
420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 |
# File 'lib/view_model/active_record.rb', line 420 def _serialize_association(association_name, json, serialize_context:) associated = self.public_send(association_name) association_data = self.class._association_data(association_name) json.set! association_name do case when associated.nil? json.null! when association_data.referenced? if association_data.collection? json.array!(associated) do |target| self.class.serialize_as_reference(target, json, serialize_context: serialize_context) end else self.class.serialize_as_reference(associated, json, serialize_context: serialize_context) end else self.class.serialize(associated, json, serialize_context: serialize_context) end end end |
#association_changed!(association_name) ⇒ Object
299 300 301 302 303 304 305 306 307 308 309 310 311 |
# File 'lib/view_model/active_record.rb', line 299 def association_changed!(association_name) association_name = association_name.to_s association_data = self.class._association_data(association_name) if association_data.read_only? raise ViewModel::DeserializationError::ReadOnlyAssociation.new(association_name, blame_reference) end unless @changed_associations.include?(association_name) @changed_associations << association_name end end |
#associations_changed? ⇒ Boolean
313 314 315 |
# File 'lib/view_model/active_record.rb', line 313 def associations_changed? @changed_associations.present? end |
#changes ⇒ Object
Additionally pass ‘changed_associations` while constructing changes.
318 319 320 321 322 323 324 325 326 |
# File 'lib/view_model/active_record.rb', line 318 def changes ViewModel::Changes.new( new: new_model?, changed_attributes: changed_attributes, changed_associations: changed_associations, changed_nested_children: changed_nested_children?, changed_referenced_children: changed_referenced_children?, ) end |
#clear_changes! ⇒ Object
328 329 330 331 332 |
# File 'lib/view_model/active_record.rb', line 328 def clear_changes! super.tap do @changed_associations = [] end end |
#context_for_child(member_name, context:) ⇒ Object
442 443 444 445 446 447 448 449 450 451 452 453 454 |
# File 'lib/view_model/active_record.rb', line 442 def context_for_child(member_name, context:) # Synthetic viewmodels don't exist as far as the traversal context is # concerned: pass through the child context received from the parent return context if self.class.synthetic # associations to roots start a new tree member_data = self.class._members[member_name.to_s] if member_data.association? && member_data.referenced? return context.for_references end super end |
#destroy!(deserialize_context: self.class.new_deserialize_context) ⇒ Object
286 287 288 289 290 291 292 293 294 295 296 297 |
# File 'lib/view_model/active_record.rb', line 286 def destroy!(deserialize_context: self.class.new_deserialize_context) model_class.transaction do ViewModel::Callbacks.wrap_deserialize(self, deserialize_context: deserialize_context) do |hook_control| changes = ViewModel::Changes.new(deleted: true) deserialize_context.run_callback(ViewModel::Callbacks::Hook::OnChange, self, changes: changes) hook_control.record_changes(changes) model.destroy! end rescue ::ActiveRecord::StatementInvalid, ::ActiveRecord::InvalidForeignKey, ::ActiveRecord::RecordNotSaved => e raise ViewModel::DeserializationError::DatabaseConstraint.from_exception(e, self.blame_reference) end end |
#serialize_members(json, serialize_context: self.class.new_serialize_context) ⇒ Object
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 |
# File 'lib/view_model/active_record.rb', line 270 def serialize_members(json, serialize_context: self.class.new_serialize_context) self.class._members.each do |member_name, member_data| next if member_data.association? && member_data.external? member_context = case member_data when AssociationData self.context_for_child(member_name, context: serialize_context) else serialize_context end self.public_send("serialize_#{member_name}", json, serialize_context: member_context) end end |