Module: Familia::Features::Relationships::Participation::ThroughModelOperations
- Defined in:
- lib/familia/features/relationships/participation/through_model_operations.rb
Overview
ThroughModelOperations provides lifecycle management for through models in participation relationships.
Through models implement the join table pattern, creating an intermediate object between target and participant that can carry additional attributes (e.g., role, permissions, metadata).
Key characteristics:
- Deterministic identifier: Built from target, participant, and through class
- Auto-lifecycle: Created on add, destroyed on remove
- Idempotent: Re-adding updates existing model
- Atomic: All operations use transactions
- Cache-friendly: Auto-updates updated_at for invalidation
Example: class Membership < Familia::Horreum feature :object_identifier field :customer_objid field :domain_objid field :role field :updated_at end
class Domain < Familia::Horreum participates_in Customer, :domains, through: :Membership end
# Through model auto-created with deterministic key
customer.add_domains_instance(domain, through_attrs: { role: 'admin' })
# => #
Class Method Summary collapse
-
.build_key(target:, participant:, through_class:) ⇒ String
Build a deterministic key for the through model.
-
.find_and_destroy(through_class:, target:, participant:) ⇒ void
Find and destroy a through model instance.
-
.find_or_create(through_class:, target:, participant:, attrs: {}) ⇒ Object
Find or create a through model instance.
-
.validated_attrs(through_class, attrs) ⇒ Hash
Validate attribute keys against the through model's field schema.
Class Method Details
.build_key(target:, participant:, through_class:) ⇒ String
Build a deterministic key for the through model
The key format ensures uniqueness and allows direct lookup: targettarget.prefix:targettarget.objid:participantparticipant.prefix:participantparticipant.objid:throughthrough.prefix
53 54 55 56 57 58 |
# File 'lib/familia/features/relationships/participation/through_model_operations.rb', line 53 def build_key(target:, participant:, through_class:) # Use prefix for Redis key construction (prefix may differ from config_name if explicitly set) "#{target.class.prefix}:#{target.objid}:" \ "#{participant.class.prefix}:#{participant.objid}:" \ "#{through_class.prefix}" end |
.find_and_destroy(through_class:, target:, participant:) ⇒ void
This method returns an undefined value.
Find and destroy a through model instance
Used during remove operations to clean up the join table entry.
125 126 127 128 129 130 |
# File 'lib/familia/features/relationships/participation/through_model_operations.rb', line 125 def find_and_destroy(through_class:, target:, participant:) key = build_key(target: target, participant: participant, through_class: through_class) existing = through_class.load(key) # Use the public exists? method for a more robust check existing&.destroy! if existing&.exists? end |
.find_or_create(through_class:, target:, participant:, attrs: {}) ⇒ Object
Find or create a through model instance
This method is idempotent - calling it multiple times with the same target/participant pair will update the existing through model rather than creating duplicates.
The through model's updated_at is set on both create and update for cache invalidation.
75 76 77 78 79 80 81 82 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 |
# File 'lib/familia/features/relationships/participation/through_model_operations.rb', line 75 def find_or_create(through_class:, target:, participant:, attrs: {}) key = build_key(target: target, participant: participant, through_class: through_class) # Try to load existing model - load returns nil if key doesn't exist existing = through_class.load(key) # Check if we got a valid loaded object using the public API # This is called outside transaction boundaries (see participant_methods.rb # and target_methods.rb for the transaction boundary documentation) if existing&.exists? # Update existing through model with validated attributes safe_attrs = validated_attrs(through_class, attrs) safe_attrs.each { |k, v| existing.send("#{k}=", v) } existing.updated_at = Familia.now.to_f if existing.respond_to?(:updated_at=) # Save returns boolean, but we want to return the model instance existing.save if safe_attrs.any? || existing.respond_to?(:updated_at=) existing # Return the model, not the save result else # Create new through model with our deterministic key as objid # Pass objid during initialization to prevent auto-generation inst = through_class.new(objid: key) # Set foreign key fields if they exist (validated via respond_to?) target_field = "#{target.class.config_name}_objid" participant_field = "#{participant.class.config_name}_objid" inst.send("#{target_field}=", target.objid) if inst.respond_to?("#{target_field}=") inst.send("#{participant_field}=", participant.objid) if inst.respond_to?("#{participant_field}=") # Set updated_at for cache invalidation inst.updated_at = Familia.now.to_f if inst.respond_to?(:updated_at=) # Set custom attributes (validated against field schema) safe_attrs = validated_attrs(through_class, attrs) safe_attrs.each { |k, v| inst.send("#{k}=", v) } # Save returns boolean, but we want to return the model instance inst.save inst # Return the model, not the save result end end |
.validated_attrs(through_class, attrs) ⇒ Hash
Validate attribute keys against the through model's field schema
This prevents arbitrary method invocation by ensuring only defined fields can be set via the attrs hash.
141 142 143 144 145 146 |
# File 'lib/familia/features/relationships/participation/through_model_operations.rb', line 141 def validated_attrs(through_class, attrs) return {} if attrs.nil? || attrs.empty? valid_fields = through_class.fields.map(&:to_sym) attrs.select { |k, _v| valid_fields.include?(k.to_sym) } end |