Add `ActiveRecord::Base#[]`

Problem

Active Record is amazing! However, you can do so much with its current public API. A common example is querying for {greater,lesser}-than attributes. People often use Areal to avoid plain/hardcoded SQL expressions.

There’s this PR that implements those comparison methods as symbols but it feels a bit hacky IMHO. This other amazing PR is similar, but it’s open for a long time and it doesn’t seem to have any plans to get merged soon.

Possible solution

Could we implement something minimal (and yet so powerful) like:

class ApplicationRecord < ActiveRecord::Base
  def self.[](column)
    arel_table[column]
  end
end

# This is optional
module Arel
  module Predications
    alias_method :>, :gt
    alias_method :<, :lt
    alias_method :>=, :gteq
    alias_method :<=, :lteq
    alias_method :"!=", :not_eq
    alias_method :==, :eq
  end
end

# usage

Post.where(Post[:created_at] <= 30.days.ago)

# We can even use it in more places than `where`
some_query.select([Post[:author].as("author_name")])

as @Adam_Lassek suggested here?

This would require Arel to be a 1st-class citizen in Active Record. I think Arel is pretty stable and powerful and we should take advantage of it.

I’d love to hear @rafaelfranca 's opinion on this.

I loved this PR! Have you changed your opinion about it? Do you think this could make into Rails 7? It’s a major release, and I think it would be a nice addition to Active Record.

As you can see in the comments of that PR we decided to not go with it. So far we are not happy with any API and we don’t plan to rush any decision.

I don’t understand why people find Arel necessary “better” than SQL fragments. I sort of understand it when you are writing libraries as it prevents you from having to fully abstract the fragment and specify things out like:

where "#{connection.quote_table_name self.class.table_name}.#{connection.quote_column_name 'created_at'}" <= ?", 30.days.ago

But in applications you don’t generally need as much protection. You know if your table name needs quotes or not (usually not) and you know if the table needs to be specified due to joins or not. So much of that boilerplate is removed and it becomes:

where 'created_at <= ?', 30.days.ago

or at worse

where 'my_table.created_at <= ?', 30.days.ago

To me there is something nice about SQL fragments as they are less magic. Sometimes with a DSL it is unclear exactly how to use it to get the what you want. But with SQL fragments it’s just regular SQL, strings and placeholders.

Arel is there to be used for things like libraries that have the special need to be fully abstract for any database types, table name, column name, etc. But for regular applications sql fragments seem quite sufficient.

For those that really want a DSL there are libraries like Ransack and Squeel that can provide a clever DSL.

Just my two cents.

2 Likes

Ranges can do away with the need for text fragments for many comparisons:

User.where(created_at: 3.days.ago..)

instead of

User.where('created_at >= ?', 3.days.ago)
1 Like

The arel-helpers gem implements that exact functionality (and various other things) if you want to start using it today…