Search code examples
ruby-on-railsactiverecordruby-on-rails-5arel

Aliasing the source table


Is there a way to alias the source table in the context of a single scope ?

I tried this :

scope = User.all
scope.arel.source.left.table_alias = "toto"
scope.where(firstname: nil) => "SELECT `toto`.* FROM `users` `toto` WHERE `toto`.`firstname` IS NULL"

The problem is that the model class keeps the alias for all subsequent queries :

User.all => "SELECT `toto`.* FROM `users` `toto`"

Edit

I added this method to ApplicationRecord

def self.alias_source(table_alias)
  klass = Class.new(self)
  klass.all.source.left.table_alias = table_alias
  klass
end

Now, I can do :

User.alias_source(:toto).where(firstname: nil) => "SELECT `toto`.* FROM `users` `toto` WHERE `toto`.`firstname` IS NULL"

Solution

  • Since creating Anonymous Classes that inherit from ActiveRecord::Base will cause memory bloat, I am not sure I would recommend it (See Here: https://github.com/rails/rails/issues/31395).

    Maybe a better implementation would be to execute in a block form instead e.g. yield the aliased table to the block then set it back afterwards?

    e.g.

    def self.with_table_alias(table_alias, &block)
      begin 
        self.all.source.left.table_alias = table_alias
        block.call(self)
      ensure 
        self.all.source.left.table_alias = nil 
      end
    end
    

    Usage

    User.with_table_alias(:toto) do |scope| 
      puts scope.joins(:posts).where(firstname: "Ruur").to_sql
    end 
    # "SELECT `toto`.* 
    #  FROM 
    #    `users` `toto` 
    #    INNER JOIN `posts` ON `posts`.`user_id` = `toto`.`id` 
    #  WHERE 
    #    `toto`.`firstname` = 'Ruur'" 
    
    puts User.all.to_sql
    # "SELECT `users`.* FROM `users`
    

    WARNING: THIS HAS NOT BE TESTED BEYOND WHAT IS SHOWN HERE AND I WOULD NOT RECOMMEND THIS IN A PRODUCTION ENVIRONMENT WITHOUT EXTENSIVE TESTING FOR EDGE CASES AND OTHER GOTCHAS

    Update To address desired implementation

    module ARTableAlias
    
      def alias_table_name=(val) 
        @alias_table_name = val 
      end 
    
      def alias_table_name 
        @alias_table_name ||= "#{self.table_name}_alias" 
      end   
    
      def alias_source
        @alias_klass ||= Class.new(self).tap do |k|
          k.all.source.left.table_alias = alias_table_name
        end  
      end 
    end 
    

    Then usage as:

    class User < ApplicationRecord 
      extend ARTableAlias 
    
      alias_table_name = :toto
    end 
    
    User.alias_source.where(first_name = 'Ruur') 
    #=> SELECT `toto`.* FROM `users` `toto` WHERE `toto`.`first_name` = 'Ruur'
    User.where(first_name = 'Ruur') 
    #=> SELECT `users`.* FROM `users` WHERE `users`.`first_name` = 'Ruur'
    User.alias_source === User.alias_source
    #=> true