lib/pragma/decorator/association/adapter/active_record.rb
# frozen_string_literal: true
module Pragma
module Decorator
module Association
module Adapter
# The ActiveRecord association adapter is used in AR environments and tries to minimize the
# number of SQL queries that are made to retrieve the associated object's data.
#
# @api private
class ActiveRecord < Base
include Adaptor
class << self
# Returns whether the adapter supports the given bond.
#
# @param bond [Bond] the bond to check
#
# @return [Boolean] whether the object is an instance of +ActiveRecord::Base+
def supports?(bond)
Object.const_defined?('::ActiveRecord::Base') &&
bond.model.is_a?(::ActiveRecord::Base)
end
end
# Initializes the adapter.
#
# @param bond [Bond] the bond to use in the adapter
#
# @raise [InconsistentTypeError] when the association's real type is different from the
# one defined on the decorator ()e.g. decorator defines the association as +belongs_to+,
# but ActiveRecord reports its type is +has_one+)
def initialize(bond)
super
check_type_consistency
end
# Returns the primary key of the associated object.
#
# If the +exec_context+ of the association is +decorator+, this will simply return early
# with the value returned by +#id+ on the associated object.
#
# If the association is a +belongs_to+, there are three possible scenarios:
#
# * the association does not have a custom scope: this will compute the PK by calling
# the foreign key on the parent model;
# * the association has a custom scope and it has not been loaded: this will compute
# the PK by +pluck+ing the PK column of the associated object;
# * the association has a custom scope and it has been loaded: this will compute
# the PK by retrieving the PK attribute from the loaded object.
#
# If the association is a +has_one+, there are two possible scenarios:
#
# * the association has already been loaded: this will compute the PK by retrieving the
# PK attribute from the loaded object;
# * the association has not been loaded: this will compute the PK by +pluck+ing the PK
# column of the associated object;
#
# Custom scopes are always respected in both +belongs_to+ and +has_one+.
#
# +nil+ values are handled gracefully in all cases.
#
# @return [String|Integer|NilClass] the PK of the associated object
#
# @todo Allow to specify a different PK attribute when +exec_context+ is +decorator+
def primary_key
return associated_object&.id if association_reflection.nil? ||
reflection.options[:exec_context] == :decorator
case reflection.type
when :belongs_to
compute_belongs_to_fk
when :has_one
compute_has_one_fk
else
fail "Cannot compute primary key for #{reflection.type} association"
end
end
# Returns the expanded associated object.
#
# This will simply return the associated object itself, delegating caching to AR.
#
# @return [Object] the associated object
#
# @todo Ensure the required attributes are present on the associated object
def full_object
associated_object
end
private
def compute_belongs_to_fk
primary_key = if association_reflection.polymorphic?
association_reflection.options[:primary_key] || associated_object.class.primary_key
else
association_reflection.association_primary_key
end
if model.association(reflection.attribute).loaded?
return associated_object&.public_send(primary_key)
end
if association_reflection.scope.nil?
return model.public_send(association_reflection.foreign_key)
end
pluck_association_fk do |scope|
fk = model.public_send(association_reflection.foreign_key)
scope.where(primary_key => fk)
end
end
def compute_has_one_fk
if model.association(reflection.attribute).loaded?
return associated_object&.public_send(association_reflection.association_primary_key)
end
pluck_association_fk do |scope|
pk = model.public_send(association_reflection.active_record_primary_key)
scope.where(association_reflection.foreign_key => pk)
end
end
def pluck_association_fk
scope = association_reflection.klass.all
if association_reflection.scope
scope = scope.instance_eval(&association_reflection.scope)
end
yield(scope).pluck(association_reflection.association_primary_key).first
end
def association_reflection
@association_reflection ||= model.class.reflect_on_association(reflection.attribute)
end
def check_type_consistency
return unless association_reflection
return if association_reflection.macro.to_sym == reflection.type.to_sym
fail InconsistentTypeError.new(
decorator: decorator,
reflection: reflection,
model_type: association_reflection.macro
)
end
end
end
end
end
end