Module: ActiveItem::Associations

Extended by:
ActiveSupport::Concern
Includes:
ModelLoader
Included in:
Base
Defined in:
lib/active_item/associations.rb

Overview

Provides has_many and belongs_to association macros with lazy loading, preloading support, and dependent record handling (destroy, nullify, restrict).

Instance Method Summary collapse

Methods included from ModelLoader

#safe_constantize_model

Instance Method Details

#check_dependent_associationsObject



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/active_item/associations.rb', line 19

def check_dependent_associations
  self.class._associations.each do |name, config|
    next unless config[:type] == :has_many && config[:dependent]

    case config[:dependent]
    when :restrict_with_exception
      raise DeleteRestrictionError, name if send(name).limit(1).any?
    when :restrict_with_error
      if send(name).limit(1).any?
        error_message = config[:message] || "Cannot delete #{self.class.name} because dependent #{name} exist"
        errors.add(:base, error_message)
        throw(:abort)
      end
    when :destroy
      send(name).each(&:destroy)
    when :delete_all
      send(name).each(&:delete)
    when :nullify
      foreign_key = config[:foreign_key]
      send(name).each { |record| record.update(foreign_key => nil) }
    end
  end
end

#load_belongs_to_association(name) ⇒ Object



295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/active_item/associations.rb', line 295

def load_belongs_to_association(name)
  config = self.class._associations[name]
  return nil unless config

  cache_var = :"@_association_cache_#{name}"
  return instance_variable_get(cache_var) if instance_variable_defined?(cache_var)

  foreign_key_value = send(config[:foreign_key])
  return nil if foreign_key_value.nil?

  klass = safe_constantize_model(config[:class_name])

  begin
    record = klass.find(foreign_key_value)
  rescue ActiveItem::RecordNotFound
    raise unless config[:optional]

    record = nil
  end
  instance_variable_set(cache_var, record)
  record
end

#load_has_many_association(name) ⇒ Object



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/active_item/associations.rb', line 242

def load_has_many_association(name)
  config = self.class._associations[name]
  return Relation.new(Object, conditions: { _empty: true }) unless config

  return Relation.new(nil, preloaded_records: _preloaded_associations[name], class_name: config[:class_name]) if _preloaded_associations.key?(name)

  local_key_value = send(config[:primary_key])
  return Relation.new(nil, conditions: { _empty: true }, class_name: config[:class_name]) if local_key_value.nil?

  conditions = { config[:foreign_key].to_sym => local_key_value }
  relation = Relation.new(nil, conditions: conditions, index_name: config[:index],
                               class_name: config[:class_name], owner: self)

  if config[:scope]
    if config[:scope].arity.zero?
      relation.instance_exec(&config[:scope]) || relation
    else
      config[:scope].call(relation)
    end
  else
    relation
  end
end

#load_has_many_through_association(name) ⇒ Object

Load a has_many :through association. Two queries: GSI query on join table → batch_find on target table.



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
# File 'lib/active_item/associations.rb', line 268

def load_has_many_through_association(name)
  config = self.class._associations[name]
  return Relation.new(Object, conditions: { _empty: true }) unless config

  # If preloaded (via includes), return cached records
  return Relation.new(nil, preloaded_records: _preloaded_associations[name], class_name: config[:class_name]) if _preloaded_associations.key?(name)

  through_name = config[:through]
  source_name = config[:source]
  target_class = safe_constantize_model(config[:class_name])

  # Query 1: Load intermediate records via the through association
  intermediate_records = send(through_name).to_a

  # Determine the foreign key on the join model that points to the target
  # Convention: source_name + "_id" (e.g., :author → "author_id")
  target_foreign_key = "#{source_name}_id"
  target_ids = intermediate_records.filter_map { |r| r.send(target_foreign_key) }.uniq

  return Relation.new(nil, preloaded_records: [], class_name: config[:class_name]) if target_ids.empty?

  # Query 2: Batch load target records
  target_records = target_class.batch_find(target_ids)

  Relation.new(nil, preloaded_records: target_records, class_name: config[:class_name])
end

#set_belongs_to_association(name, record) ⇒ Object



318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
# File 'lib/active_item/associations.rb', line 318

def set_belongs_to_association(name, record)
  config = self.class._associations[name]
  return unless config

  cache_var = :"@_association_cache_#{name}"
  instance_variable_set(cache_var, record)

  if record.nil?
    send("#{config[:foreign_key]}=", nil)
  else
    pk = config[:primary_key] || record.class.primary_key
    send("#{config[:foreign_key]}=", record.send(pk))
  end

  hook_method = :"after_set_#{name}_association"
  send(hook_method, record) if respond_to?(hook_method, true)
end