What is Arel?
Arel is a SQL AST manager for Ruby.
What is an AST?
An abstract syntax tree (AST), or just syntax tree, is a tree representation of the abstract syntactic structure of source code written in a programming language. Each node of the tree denotes a construct occurring in the source code.
More about Arel
It is intended to be a framework framework; that is, you can build your own ORM with it, focusing on innovative object and collection modeling as opposed to database compatibility and query generation.
Case in point:
ActiveRecord is built with Arel.
Let's look at some code
A Custom SQL We need to get into code.
SELECT d.* FROM distributions d
INNER JOIN distributions_salepoints ds ON ds.distribution_id = d.id
INNER JOIN salepoints s ON s.id = ds.salepoint_id
INNER JOIN petri_bundles pb ON pb.id = d.petri_bundle_id
INNER JOIN albums a ON a.id = pb.album_id
INNER JOIN upcs u ON u.upcable_id = a.id AND u.upcable_type = 'Album'
WHERE d.converter_class = 'MusicStores::Itunes::Converter'
AND s.store_id = 1
AND u.number IN (859709356761,859709363271)
ORDER BY d.id;
The cheap and easy way
Distribution.find_by_sql("
SELECT d.* FROM distributions d
INNER JOIN distributions_salepoints ds ON ds.distribution_id = d.id
INNER JOIN salepoints s ON s.id = ds.salepoint_id
INNER JOIN petri_bundles pb ON pb.id = d.petri_bundle_id
INNER JOIN albums a ON a.id = pb.album_id
INNER JOIN upcs u ON u.upcable_id = a.id AND u.upcable_type = 'Album'
WHERE d.converter_class = 'MusicStores::Itunes::Converter'
AND s.store_id = 1
AND u.number IN (859709356761,859709363271)
ORDER BY d.id;")
What is wrong with this code?
So how can we make this better.
Using some AR methods
Distribution
.select("distributions.*")
.joins("
INNER JOIN distributions_salepoints ds ON ds.distribution_id = distributions.id
INNER JOIN salepoints s ON s.id = ds.salepoint_id
INNER JOIN petri_bundles pb ON pb.id = distributions.petri_bundle_id
INNER JOIN albums a ON a.id = pb.album_id
INNER JOIN upcs u ON u.upcable_id = a.id AND u.upcable_type = 'Album'
")
.where("
distributions.converter_class = 'MusicStores::Itunes::Converter'
AND s.store_id = 1
AND u.number IN (859709356761)
")
Not much better since its still just embedding SQL into our method calls
Is there a way we can write this query using purely Ruby code?
Let's use Arel to compose this query.
A sidebar about the arel_table method
Model.arel_table
distribution = Distribution.arel_table
=> Arel::Table:0x007f9d0dcbdf48
We can access model attributes using Hash-like methods
distribution[:converter_class]
=> Arel::Attributes::Attribute
distribution[Arel.star]
=> Arel::Attributes::Attribute
**Arel.star allows you to select all attributes
A sidebar about the joins method
.joins(petri_bundle: { album: :upcs })
Passing joins nested hashes of the associations will let you use ActiveRecord's knowledge of the association.
Example:
Distribution.joins(:salepoints, petri_bundle: { album: :upcs }).to_sql
=> "SELECT `distributions`.* FROM `distributions`
INNER JOIN `distributions_salepoints` ON `distributions_salepoints`.`distribution_id` = `distributions`.`id`
INNER JOIN `salepoints` ON `salepoints`.`id` = `distributions_salepoints`.`salepoint_id`
INNER JOIN `petri_bundles` ON `petri_bundles`.`id` = `distributions`.`petri_bundle_id`
INNER JOIN `albums` ON `albums`.`id` = `petri_bundles`.`album_id`
INNER JOIN `upcs` ON `upcs`.`upcable_id` = `albums`.`id` AND `upcs`.`upcable_type` = 'Album'"
Using Arel::Table instances to formulate our joins
Distribution
.select(Distribution.arel_table[Arel.star])
.joins(:salepoints, petri_bundle: { album: :upcs })
This is gonna get verbose, lets define methods to return the arel_table's we need.
def distribution
Distribution.arel_table
end
def upc
Upc.arel_table
end
def petri_bundle
PetriBundle.arel_table
end
def salepoint
Salepoint.arel_table
end
Using our new methods
Distribution
.select(distribution[Arel.star])
.joins(:salepoints, petri_bundle: { album: :upcs })
Arel comes with a wide assortment of predication and expression methods we can pass to the AR where method
Lets take a look at a few:
Using these on our where
where(distribution[:converter_class].eq(converter_class))
where(salepoint[:store_id].eq(store_id))
where(upc[:number].in(upcs))
# Performs a LIKE
where(distribution[:converter_class].matches("%#{converter_class}%"))
# Inequality
where(salepoint[:store_id].not_eq(store_id))
All these where clauses are chainable, so this is perfectly valid
where(distribution[:converter_class].eq(converter_class)).where(salepoint[:store_id].eq(store_id)).where(upc[:number].in(upcs))
# Or all passed to one where clause, chained with the .and method
.where(distribution[:converter_class].eq(converter_class).and(salepoint[:store_id].eq(store_id).and(upc[:number].in(upcs))))
Finishing up our new composed query
converter_class = "MusicStores::Itunes::Converter"
store_id = 1
upcs = [1,2,3]
Distribution
.select(distribution[Arel.star])
.joins(:salepoints, petri_bundle: { album: :upcs })
.where(
distribution[:converter_class].eq(converter_class)
)
.where(
salepoint[:store_id].eq(store_id)
)
.where(
# Note that i can pass an array to the in method
upc[:number].in(upcs)
)
Putting this in an object that can be reused.
Some other things you can do with Arel.
End