Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question about .select with .includes #8

Open
chuan29812 opened this issue Sep 14, 2023 · 3 comments
Open

Question about .select with .includes #8

chuan29812 opened this issue Sep 14, 2023 · 3 comments

Comments

@chuan29812
Copy link

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

@lorint
Copy link
Owner

lorint commented Sep 14, 2023

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.

@chuan29812
Copy link
Author

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!

@lorint
Copy link
Owner

lorint commented Sep 22, 2023

I am hopeful that this might become a part of Rails 8.

eager_load_trick2.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants