ActiveRecordQuery
ActiveRecordQuery is a DSL buit on top of ActiveRecord to help you write complex SQL queries in the cleanest way possible. The lib provides a base class to build a query pattern in your object oriented project.
Quick usage sample:
# verbose
query = Post.joins(:author).where(Post.arel_table[:created_at].gt(Date.new(2000, 1, 2))).order(title: :asc)
# clean ;)
class Query < ActiveRecordQuery::Base
from Post
join :author
where created_at > Date.new(2000, 1, 2)
order_by title.asc, author.name.asc
end
query = Query.executeThe main goal is to turn your queries (or scopes) into classes. These classes will be written naturally like a SQL query using a ruby DSL. The common problem with the actual design of activerecord is that the users tend to write the query features in chain, like this:
Post.select(:title)
.where(title: 'A title')
.where('created_at > ?', Date.today)
.order(:title)You can refactor with scopes, I guess..
class Post < ActiveRecord::Base
scope :titled, -> { where(title: 'A title') }
scope :created, -> { where('created_at < ?', Date.today) }
scope :titled_created, -> { titled.created }
def self.a_query
select(:title).titled_created.order(:title)
end
endVery messy... When arel table features comes in, it becomes even worst.
is_fixed = Post[:fixed].eq(true)
is_coming = Post[:coming].eq(true).and(Post[:activated_at].not_eq(nil))
Post.where(is_fixed.or(is_coming))Now, let's try the ActiveRecordQuery:
class PostQuery < ActiveRecordQuery::Base
from Post
where fixed == true
wor do |other|
other.where coming == true
other.where actived_at != nil
end
endInstallation
Add this line to your application's Gemfile:
gem 'activerecord-query'And then execute:
$ bundle install
Or install it yourself as:
$ gem install activerecord-query
Usage
ActiveRecordQuery adds the ActiveRecord::Base class to your project, so we can easily create query classes by extending it.
Queries
The concept is given by the notion of query pattern classes. The suggestion is that you put these classes in app/queries folder.
The query setup
Consider to have a Post model:
class Post < ActiveRecord::Base
endNow, you have to create a classe extending ActiveRecordQuery::Base class,
and then add the reference for the Post activerecord type
on from method.
class PostQuery < ActiveRecordQuery::Base
from Post
endOnce you have defined the from resource, the columns from Post will be available
as methods in the class scope.
E.g. the method title will be defined as a Arel::Attributes::Attribute object.
class PostQuery < ActiveRecordQuery::Base
from Post
select title # the 'title' method was defined from 'from'
endThe public method execute will return the ActiveRecord_Relation object.
PostQuery.execute # => ActiveRecord_Relation Or you can instantiate it also:
PostQuery.new.execute # => ActiveRecord_Relation Query Features
Conditions
The methods where and wor are available to set the query conditions.
The argument must be a Arel::Node object.
To a cleaner experience, you can use the generated methods for columns
generated from the from method.
class PostQuery < ActiveRecordQuery::Base
from Post
# and operation
where title == 'something' # using title helper
where Post.arel_table[:title].eq('something') # using arel
# or operation
wor title == 'something else'
endThe Arel predications are available:
# between
where column.between 1..10
# matches
where column.matches '%something%'
# not in all
where column.not_in_all %w[something]You can nest the conditions:
where column == 'c1'
wor do |nested|
nested.where column == 'c2'
nested.where other == 'c3'
nested.wor do |deep_nested|
deep_nested.where other == 'c1'
deep_nested.where column == 'c4'
end
endIt Generates:
column = "c1" or (column = "c2" and other = "c3" or (other = 'c1' and column = 'c4'))The dynamic values for conditions can be add as a symbol to reference
a method or can be a proc. The values will be evaluate when the execute is called.
class PostQuery < ActiveRecordQuery::Base
from Post
where title == :a_dynamic_method # references a method
where title == proc { 'a title' } # the value will be evaluate on execute
def a_dynamic_method
'a title'
end
endConditional where
The where/wor state can be conditioned by passing the option if:.
The value must be a symbol referencing a method.
class PostQuery < ActiveRecordQuery::Base
from Post
where title == 'test', if: :a_method?
def a_method?
false
end
endSelects
Selects can be done by passing a list of columns to the select method.
The args must be a list of Arel::Attributes::Attribute.
If no select is defined in the query, then the * selection will be taken.
Every call of select the attrs will be added to the selection.
class PostQuery < ActiveRecordQuery::Base
from Post
# simple select
select title, created_at
# can do a math op (it's just a arel attr)
select id + id
# plain arel attr
select Post.arel_table[:id]
endOrder by
Much like the select method, you shall pass a list of attributes to the method order_by.
Every call of order_by the attrs will be added to the selection.
class PostQuery < ActiveRecordQuery::Base
from Post
# list of arel attrs
order_by title.asc, created_at.desc
endLimits
The limit method is available to define a query limit.
An integer value is the only arg acceptable.
Every time the limit method is called, the limit will be redefined.
class PostQuery < ActiveRecordQuery::Base
from Post
limit 10
endOffsets
The offset method is available to define a query offset.
An integer value is the only arg acceptable.
Every time the offset method is called, the offset will be redefined.
class PostQuery < ActiveRecordQuery::Base
from Post
offset 10
endInner Join
The join method defines one/many relationships with the current resource (from state).
The following example has a Post and Author models, the way we define a join is the same as
defining a joins on activerecord (check the active record querying doc.).
Right after defined the join a new method will be available for retrieve the columns
from the new resource, the author method on this example. Every relationship listed in
the args will be converted to a method with the same name.
# models
class Post < ActiveRecord::Base
belongs_to :author
end
class Author < ActiveRecord::Base
has_many :posts
end
# query
class PostQuery < ActiveRecordQuery::Base
from Post
join :author
# the author helper will be available
where author.name == 'John'
endLeft Outer Join
The left_outer_join method defines one/many relationships with the current resource (from state).
The following example has a Post and Author models, the way we define a left outer join is the same as
defining a left_outer_joins on activerecord (check the active record querying doc.).
Right after defined the join, a new method will be available for retrieve the columns
from the new resource, the posts method on the example bellow. Every relationship listed in
the args will be converted to a method with the same name.
# models
class Post < ActiveRecord::Base
belongs_to :author
end
class Author < ActiveRecord::Base
has_many :posts
end
# query
class PostQuery < ActiveRecordQuery::Base
from Author
left_outer_join :posts
# the `posts` helper will be available
where posts.title == 'A title'
endGroup by
To apply a GROUP BY clause to the query, you can use the group_by method.
The method accepts a list of columns.
class PostQuery < ActiveRecordQuery::Base
from Post
group_by title
endHaving
You can add the HAVING clause to the query by defining a having method.
A column condition can be done by calling the Arel predication methods like gt.
class PostQuery < ActiveRecordQuery::Base
from Post
group_by title
having id.gt 5
endScopes
There are at least to ways to scope your query class. The first one is the use of class inheritance. The second one is extract features to modules.
Class inheritance
You can merge queries by extending the class.
Let's say that you have a base query definition AScopeQuery.
class AScopeQuery < ApplicationQuery
from Post
where title != nil
endAnd then you extend this query:
class AQuery < AScopeQuery
where id > 5
order_by title
endThe result will be the merge of the two queries:
SELECT * FROM posts WHERE title NOT NULL AND id > 5 ORDER BY title Modules
Another way to scope a query, would be including modules into yours query class. Let's define a module with activesupport concern.
module AScope
extend ActiveSupport::Concern
included do
where title == 'a scope'
end
endNote that we called the where macro inside the included method just like the
"activemodel concerns style". And then we can simply include the scope module into
the query class:
class AQuery < ApplicationQuery
from Post
include AScope
endIt is important to notice that the module must be included after the from definition
due to the scope dependency on the from builds.
Query Parameters
The query class can be instantiate/execute with user parameters.
The options method will be available on the instance context of the class.
This data can be part of the query dynamic solutions in their features.
class PostQuery < ApplicationQuery
from Post
where title == :title_value
def title_value
options[:title]
end
end
# execute with option :title
PostQuery.execute(title: 'A Title') # => select * from posts where title = "A Title"On this example, the value for title condition is dynamic set by
the options parameter. A proc can be used also:
where title == proc { options[:title] }Development
After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/marcosfelipe/activerecord-query. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the ActiveRecordQuery project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.
Author
Marcos Felipe (marcosfelipesilva54@gmail.com)