1 What is Arel?
Arel is a SQL. AST manager for Ruby. It
- Simplifies the generation of complex SQL queries
- Adapts to various RDBMSes
Arel might be a short for ActiveRelation
2 Generate SQL string with Arel
The method Arel::Nodes::Node#to_sql
could generate SQL string.
The gem active_record
has required Arel gem, and the bind between
Arel and active_record is very close, because Arel needs a engine
to work,
which is very confused to me.
require 'active_record' ActiveRecord::Base.establish_connection( adapter: 'mysql2', database: 'test', host: 'localhost', username: 'root', password: '1024' ) user = Arel::Table.new('users') arel = user. project('id', 'user_name'). # select where(user[:nick_name].eq('dsg')). # where order('created_at DESC'). # order skip(10). # offset take(5); # limit sql = arel.to_sql #=> # SELECT id, user_name FROM `users` # WHERE `user`.`nick_name` = 'dsg' # ORDER BY created_at DESC # LIMIT 5 # OFFSET 10
When we get the sql, we can use ActiveRecord::Base.find_by_sql(sql)
to get
the record.
An ActiveRecord::Base.connection is needed here, but the generating-sql progress is no business of the connecion.
Another example usage of Arel in Rails.
# # SELECT `roles`.` FROM `roles` # WHERE( # `roles`.`id` < 10 # AND `roles`.`id` > 0 # OR `roles`.`id` = 1024 # ) t = Role.arel_table Role.where( t[:id]. lt(10). and(t[:id].gt 0). or(t[:id].eq 1024) )
3 The Arel-SQL mapping
To know the Arel-SQL mapping, we should first know two concepts:
- Abstract Syntax Tree
- SQL Design Pattern
3.1 Abstract Syntax Tree
Abstract Syntax Tree is a tree representation of the abstract syntactic structure of source code written in a programming language.
An example of AST can be as below:
while b!=0 do if a > b a = a - b else b = b - a end end return a
3.2 SQL Design Pattern
Take the SELECT
part as an example:
The design comes from SQL As Understood By SQLite
- select_statement
- select_core
The SELECT
part can be seen as below:
|-- SelectCore | |-- Projections(id, user_name, ...) | |-- Where | |-- Group |-- Order |-- Limit |-- Limit |-- Offset
3.3 Arel Design Pattern
Come back to the sql
user = Arel::Table.new('users') arel = user. project('id', 'user_name'). # select where(user[:nick_name].eq('dsg')). # where order('created_at DESC'). # order skip(10). # offset take(5); # limit
We can get the sql
SELECT id, user_name FROM `users` WHERE `user`.`nick_name` = 'dsg' ORDER BY created_at DESC LIMIT 5 OFFSET 10
Arel gives a method to draw an AST image.
File.write('arel.dot', arel.to_dot) system %x(dot arel.dot -T png -o arel.png)
Then we get the map of Arel
From the AST, we know
* The concept of select_statement and select_core comes from
SQL Design Pattern
* The left, right branch concept comes from
Abstract Syntax Tree
4 Arel Source Code Inspection
Everything goes to the method: to_sql
An tiny example of sql transferring
id = Arel::Nodes::SqlLiteral.new('id') count = id.count count.to_sql
We could use pry's show-method count.to_sql
to find the method
# From: lib/arel/nodes/node.rb @ line 34: # Owner: Arel::Nodes::Node # Visibility: public # Number of lines: 3 def to_sql engine = Table.engine engine.connection.visitor.accept self end
To find the method accept
, we should know the ancestor chain of
Arel::Table.engine.connection.visitor
visitor = Arel::Table.engine.connection.visitor visitor.class.ancestors #=> # # [ # [ 0] ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::BindSubstitution < Arel::Visitors::MySQL, # [ 1] Arel::Visitors::BindVisitor, # [ 2] Arel::Visitors::MySQL < Arel::Visitors::ToSql, # [ 3] Arel::Visitors::ToSql < Arel::Visitors::Visitor, # [ 4] Arel::Visitors::Visitor < Object, # [ 5] Object < BasicObject, # [ 6] JSON::Ext::Generator::GeneratorMethods::Object, # [ 7] ActiveSupport::Dependencies::Loadable, # [ 8] PP::ObjectMixin, # [ 9] Kernel, # [10] BasicObject # ]
# Arel::Visitors::Visitor def accept object visit object end
The object here is id.count
. Inspect the class of id.count
ruby
id.count.class #=> Arel::Nodes::Count
Then we seek the visit
method, which is the core of the Arel gem.
The accept, visit concepts come from the
Visitor Pattern,
while it's not the same with the traditional Visitor Pattern
. Check this
reference
if you are intersted.
def visit object send dispatch[object.class], object rescue NoMethodError => e raise e if respond_to?(dispatch[object.class], true) superklass = object.class.ancestors.find { |klass| respond_to?(dispatch[klass], true) } raise(TypeError, "Cannot visit #{object.class}") unless superklass dispatch[object.class] = dispatch[superklass] retry end
Very intersting, just send dispatch[object.class]
, that is
send dispatch[Arel::Nodes::Count], object #=> visit_Arel_Nodes_Count(id.count)
Finally, find the visit_Arel_Nodes_Count
method
# Arel::Visitors::ToSql def visit_Arel_Nodes_Count o "COUNT(#{o.distinct ? 'DISTINCT ' : ''}#{o.expressions.map { |x| visit x }.join(', ')})#{o.alias ? " AS #{visit o.alias}" : ''}" end
Go through more complexed sql
user = Arel::Table.new('users') arel = user. project('id', 'user_name'). where(user[:nick_name].eq('dsg')). order('created_at DESC'). skip(10). take(5); arel.to_sql
5 Is there a simple way to create SQL strings?
The AST, SQL Lang Pattern, Visitor Pattern may be a little complexed? Maybe there is a simple way to create SQL strings as below:
class NewArel attr_accessor :where, :select, :order, :skip, :limit def where(string) @wheres ||= [] @wheres << string self end def select(*args) @selects ||= [] @selects = @selects.concat(args).compact.uniq self end def order(string) @orders ||= [] @orders << string self end # ... omited def to_sql [ "SELECT #{@selects.join(', ')}", "WHERE #{@where.join('AND ')}" ].join(' ') end end arel = NewArel.new. where('id < 10'). where('id > 5'). select(:id, :user_name) arel.to_sql
6 What's more about Arel
- TreeManage
- SelectManager
- UpdateManager
- InsertManager
- DeleteManager
- visitors
- mysql
- sqlite
- mssql
- ....