brick
brick copied to clipboard
Question about .select with .includes
Dear maintainer/creator, I am very interested in your .select with .includes work here, as I am trying to accomplish something very similar for my company. I also saw your post in Rails forum. Did you ever consider making this a part of Rails?
Also, im trying to find how you are achieving this in this repo. I was able to grep for _brick_eager_load, but that wasnt too helpful. If you dont mind could you share a list of files Im supposed to look for to study how you accomplished this?
Sincerely, Chuan
If you just want the .select() and .includes() stuff then that's pretty simple -- just add this anywhere, say in application.rb
is fine:
# An intelligent .eager_load() and .includes() that creates t0_r0 style aliases only for the columns
# used in .select(). To enable this behaviour, include the flag :_brick_eager_load as the first
# entry in your .select().
# More information: https://discuss.rubyonrails.org/t/includes-and-select-for-joined-data/81640
class ActiveRecord::Associations::JoinDependency
def apply_column_aliases(relation)
if !(@join_root_alias = relation.select_values.empty?) &&
relation.select_values.first.to_s == '_brick_eager_load'
relation.select_values.shift
used_cols = {}
# Find and expand out all column names being used in select(...)
new_select_values = relation.select_values.map(&:to_s).each_with_object([]) do |col, s|
if col.include?(' ') # Some expression? (No chance for a simple column reference)
s << col # Just pass it through
else
col = if (col_parts = col.split('.')).length == 1
[col]
else
[col_parts[0..-2].join('.'), col_parts.last]
end
used_cols[col] = nil
end
end
if new_select_values.present?
relation.select_values = new_select_values
else
relation.select_values.clear
end
@aliases ||= Aliases.new(join_root.each_with_index.map do |join_part, i|
join_alias = join_part.table&.table_alias || join_part.table_name
keys = [join_part.base_klass.primary_key] # Always include the primary key
# # %%% Optional to include all foreign keys:
# keys.concat(join_part.base_klass.reflect_on_all_associations.select { |a| a.belongs_to? }.map(&:foreign_key))
# Add foreign keys out to referenced tables that we belongs_to
join_part.children.each { |child| keys << child.reflection.foreign_key if child.reflection.belongs_to? }
# Add the foreign key that got us here -- "the train we rode in on" -- if we arrived from
# a has_many or has_one:
if join_part.is_a?(ActiveRecord::Associations::JoinDependency::JoinAssociation) &&
!join_part.reflection.belongs_to?
keys << join_part.reflection.foreign_key
end
keys = keys.compact # In case we're using composite_primary_keys
j = 0
columns = join_part.column_names.each_with_object([]) do |column_name, s|
# Include columns chosen in select(...) as well as the PK and any relevant FKs
if used_cols.keys.find { |c| (c.length == 1 || c.first == join_alias) && c.last == column_name } ||
keys.find { |c| c == column_name }
s << Aliases::Column.new(column_name, "t#{i}_r#{j}")
end
j += 1
end
Aliases::Table.new(join_part, columns)
end)
end
relation._select!(-> { aliases.columns })
end
end
If you want to do the more interesting things with .brick_where()
or .brick_select()
, it gets quite a bit more involved because then you have to understand how "brick_links" works -- this is a patch to AREL which finds how every ActiveRecord chain of association names relates back to an exact table correlation name chosen by AREL when the AST tree is being walked.
Thank you very much for the answer, now I completely understand how it is done. It is quite clever. Do you have any consideration of contributing this to Rails? It seems like a very useful addition.
Another goal of mine is to enable/implement the same behavior for preload, it seems like i'd have to override the preload_association
method in some ways. Did you ever given thought to preload?
Thanks again!
I am hopeful that this might become a part of Rails 8.
https://github.com/lorint/brick/assets/5301131/ee95f696-504b-4a01-b39e-46af1d499ec5