Thanks for your explanation on this matter!
I also volunteer for helping. I have been working a lot with Arel and SQL and I think I can help. Can you also add me, @rafaelfranca.
I second taking inspiration from Sequel, one of my all-time fave Ruby libraries. One thing to keep in mind though is that there are two distinct layers here:
- “sugar” a la Squeel or the Sequel operator-based layer; and
- A clean, composable, stable object API that undergirds the sugar.
I think mainly nobody has come up with a better name for arel_table
@flanger001 what is your main entry point to getting Arel objects? I think most people use arel_table
, but I don’t know for sure. AFAIK most Active Record query builder methods will take an arel object, so I don’t think that’s our main hurdle. My take is that we need to avoid folks ever typing “arel” or “Arel” as much as possible but still expose the power / flexibility.
@rafaelfranca Sign me up to help with Arel! The app I work on has over 13k lines of Arel/query object code.
I used arel_table
at first but have switched to Arel::SelectManager.new(Arel::Table.engine)
recently because I use things like CTEs and referenced windows a lot.
Interesting. Are you passing the resulting select manager to AR to actually perform the query? Do you have any short-ish examples?
We could easily add factory methods to make constructing the select manager easier, but I’m not sure that’s going to get us to the “don’t type the letters a r e l” goal.
The entry point I use is mainly arel_table[]
. In order not to type the “a r e l” letters I add a []
delegator to ApplicationRecord
:
def self.[](attribute)
arel_table[attribute]
end
I also use a sprinkle of Arel.star
and Arel.sql
.
I call to_sql
on the manager and pass it to ApplicationRecord.connection.execute
(usually), find_by_sql
when it makes sense. —I should note I’m doing a lot of analytic type stuff, so often the results I want are counts and sums and what not.
Short-ish example coming …
I’ve seen this before. I like it but I’m worried about adding the []
method because maybe we want to use the method for something else. But I’ve had this same worry for literally years and we’ve not implemented that method. Maybe it’s time we do it?
@tenderlove here’s a rough example of how I compose query objects together. It’s probably missing some stuff and some of these methods I have in mixins, etc.
class MyQuery
attr_reader :company_id, :other_query_object
delegate :to_sql, to: :query
def initialize(company_id, other_query_object:)
@company_id = company_id
@other_query_object = other_query_object
end
def execute
cast_values ApplicationRecord.connection.execute(to_sql)
end
def query
@query ||= begin
manager
.project([
widgets[:person_id],
widgets[:cost].sum,
Arel::Nodes::Count.new(Arel.star)])
.from(widgets)
.where(widgets[:company_id].eq(company_id))
.group(1)
.with(widgets_cte)
end
end
def manager
@manager ||= Arel::SelectManager.new(Arel::Table.engine)
end
def widgets_cte
Arel::Nodes::As.new(Arel::Table.new(:widgets), other_query_object)
end
end
(query
and manager
are memoized so I can call them multiple times without duplicates from rebuilding)
I use Model[:column]
extensively, provided by the arel-helpers gem (which I found out about at RailsConf years and years ago).
Most of the day-to-day Arel I write is making clauses out of columns, so I actually tend to not directly use arel_table
other than when I’m writing something generic. I have a number of complex joins (for example) that are written in much more explicit Arel (which are probably hard for newcomers to the project to understand but that’s the tradeoff).
My main entry is .arel_table
as you said, e.g. Task.arel_table
. Sometimes I use the #arel
method on a relation:
[1] pry(main)> Task.all.arel
=> #<Arel::SelectManager:0x00007ff1cc026cd0
@ast=
#<Arel::Nodes::SelectStatement:0x00007ff1cc026ca8
@cores=
[#<Arel::Nodes::SelectCore:0x00007ff1cc026c80
(I wouldn’t use it on .all
but this is for simplicity)
I find it’s the most useful in scopes. Here’s a more thorough example from a personal project:
class Station < ApplicationRecord
has_many :gas_entries
has_many :user_stations, :dependent => :destroy
has_many :users, :through => :user_stations
scope :with_user_visits, ->(user) {
visits_t = Arel::Table.new(:visits)
visits_d = GasEntry.for_user(user).select(:station_id).arel # <-- #arel
visits = Arel::Nodes::As.new(visits_t, visits_d)
stations_d = arel_table.
project(arel_table[Arel.star], visits_t[:station_id].count.as("visits")).
join(visits_t, Arel::Nodes::OuterJoin).on(visits_t[:station_id].eq(arel_table[:id])).
group(arel_table[:id], visits_t[:station_id]).
with(visits)
from(stations_d.as("stations"))
}
end
Essentially the idea here was to use a CTE to build a visits
count column, because then in my view I can just use a station.visits
method, and I didn’t have to do any counting in Ruby.
I will add that one thing I hope to work with @rafaelfranca on here is exposing some more of this within ActiveRecord. The reason we reach to Arel is because ActiveRecord can’t do some of the stuff we want, so we can reduce the need for Arel if we can expose things through ActiveRecord.
If not typing Arel is a goal, what should be done about the requirement to type Arel.sql to escape deprecation notices for SQL fragments in things like order()?
I’ve made a PR so we can discuss the []
feature. https://github.com/rails/rails/pull/39198
I don’t know yet. Maybe we can’t eliminate it 100%, but it’s a goal to strive for
Nice. We should steal stuff from this gem and put it in Rails. Thank you.
Great, thanks!
Arel::Nodes::Count.new(Arel.star)
can be rewritten as Arel.star.count
.
We probably need an as
factory method on Arel::Table
. We have this, but it doesn’t seem to be on the Table class, and I’m not sure if converting the parameter to a sql literal is correct. Would you mind investigating?
I’m not sure what to do about creating the select manager. Lets discuss it.
Awesome, thank you! Let’s figure out how to get rid of the references to “Arel”.
Arel has some builder methods like outer_join
so we can eliminate the reference to Arel::Nodes::OuterJoin
. I think the PR here will help us eliminate some of the Arel::Table.new
calls.
Anyway, I think we just need to start implementing method to start chipping away at the references to “Arel” as much as possible