class ActiveRecord::Associations::JoinDependency
FIXME: Hopefully we can get this into Rails core so this is no longer required in our codebase, but the rule that are broken here are mostly due to the style of the Rails codebase conflicting with our own. Ignoring them to avoid noise in RuboCop, but allow us to keep the same syntax from the original codebase.
rubocop:disable Style/BlockDelimiters, Layout/SpaceAfterComma, Style/HashSyntax rubocop:disable Layout/AlignHash
Public Instance Methods
# File lib/active_record/virtual_attributes/virtual_fields.rb, line 253 def instantiate(result_set, *_, &block) primary_key = aliases.column_alias(join_root, join_root.primary_key) seen = Hash.new { |i, object_id| i[object_id] = Hash.new { |j, child_class| j[child_class] = {} } } model_cache = Hash.new { |h,klass| h[klass] = {} } parents = model_cache[join_root] column_aliases = aliases.column_aliases(join_root) # New Code column_aliases += select_values_from_references(column_aliases, result_set) if result_set.present? # End of New Code message_bus = ActiveSupport::Notifications.instrumenter payload = { record_count: result_set.length, class_name: join_root.base_klass.name } message_bus.instrument('instantiation.active_record', payload) do result_set.each { |row_hash| parent_key = primary_key ? row_hash[primary_key] : row_hash parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases, &block) if ActiveRecord.version.to_s < "6.0" construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases) else construct(parent, join_root, row_hash, seen, model_cache) end } end parents.values end
This monkey patches the ActiveRecord::Associations::JoinDependency
to include columns into the main record that might have been added through a `select` clause.
This can be seen with the following:
Vm.select(Vm.arel_table[Arel.star]).select(:some_vm_virtual_col) .includes(:tags => {}).references(:tags)
Which will produce a SQL SELECT statement kind of like this:
SELECT "vms".*, (<virtual_attribute_arel>) AS some_vm_virtual_col, "vms"."id" AS t0_r0 "vms"."vendor" AS t0_r1 "vms"."format" AS t0_r1 "vms"."version" AS t0_r1 ... "tags"."id" AS t1_r0 "tags"."name" AS t1_r1
This is because rails is trying to reduce the number of queries needed to fetch all of the records in the include, so it grabs the columns for both of the tables together to do it. Unfortunately (or fortunately… depending on how you look at it), it does not remove any `.select` columns from the query that is run in the process, so that is brought along for the ride, but never used when this method instanciates the objects.
The “New Code” here simply also instanciates any extra rows that might have been included in the select (virtual_columns) as well and brought back with the result set.
# File lib/active_record/virtual_attributes/virtual_fields.rb, line 327 def select_values_from_references(column_aliases, result_set) join_dep_keys = aliases.columns.map(&:right) join_root_aliases = column_aliases.map(&:first) additional_attributes = result_set.first.keys .reject { |k| join_dep_keys.include?(k) } .reject { |k| join_root_aliases.include?(k) } if ActiveRecord.version.to_s >= "6.0" additional_attributes.map { |k| Aliases::Column.new(k, k) } else additional_attributes.map { |k| [k, k] } end end