Mincer
Mincer is an ActiveRecord::Relation wrapper that applies usefull features to your queries. It can:
Paginate Sort Search Dump to Json(Using postgres >= 9.2) Generate digest(useful for caching)
Installation
Add this line to your application's Gemfile:
gem 'mincer'
And then execute:
$ bundle
Or install it yourself as:
$ gem install mincer
Usage
Lets assume we have 2 models
class Employee < ActiveRecord::Base
belongs_to :company
end
class Company < ActiveRecord::Base
has_many :employees
end
Generating custom query
rails generate mincer:query EmployeesList
Will generate employees_list_query.rb file in /app/queries/ directory
class EmployeesListQuery < Mincer::Base
def build_query(relation, args)
# Apply your conditions, custom selects, etc. to relation
relation
end
end
It inherits from Mincer::Base. Lets instantiate it
class EmployeesListQuery < Mincer::Base
# method should always return relation
def build_query(relation, args)
custom_select = <<-SQL
employees.id,
employees.full_name as employee_name,
companies.name as company_name
SQL
relation.joins(:company).select(custom_select)
end
end
employees = EmployeesListQuery.new(Employee)
employees
will delegate all methods, that it can't find on itself, to relation objects. This means you can use
employees
as you would use any ActiveRecord::Relation object:
<% employees.each do |employee| %>
<%= employee.employee_name %>
<%= employee.company_name %>
<% end %>
Now lets look what more can we do with this object
Pagination
Mincer supports kaminari and will_paginate.
In order to use pagination you need to include one of them
in your Gemfile
. Example of using pagination
employees = EmployeesListQuery.new(Employee, {'page' => 2, 'per_page' => 10})
By default all Mincer
objects will use pagination, even if no arguments are passed.
To set default values for pagination please refer to kaminari
or will_paginate
documentation.
To disable pagination you can use class method skip_pagination!
:
class EmployeesListQuery < Mincer::Base
skip_pagination!
# method should always return relation
def build_query(relation, args)
custom_select = <<-SQL
employees.id,
employees.full_name as employee_name,
companies.name as company_name
SQL
relation.joins(:company).select(custom_select)
end
end
Sorting
Example of using sorting:
employees = EmployeesListQuery.new(Employee, {'sort' => 'employee_name', 'order' => 'DESC'})
By default all Mincer objects will sort by attribute id
in ASC
order. To change defaults you can override
them like this
class EmployeesListQuery < Mincer::Base
# method should always return relation
def build_query(relation, args)
custom_select = <<-SQL
employees.id,
employees.full_name as employee_name,
companies.name as company_name
SQL
relation.joins(:company).select(custom_select)
end
def default_sort_attribute
'employee_name'
end
def default_sort_order
'DESC'
end
end
To disable sorting use class method skip_sorting!
like this:
class EmployeesListQuery < Mincer::Base
skip_sorting!
# method should always return relation
def build_query(relation, args)
custom_select = <<-SQL
employees.id,
employees.full_name as employee_name,
companies.name as company_name
SQL
relation.joins(:company).select(custom_select)
end
end
Mincer will validate sort
and order
params and will not allow to sort by attributes that do not exist.
Default white list consists of all attributes from original scope, in our example Employee.attribute_name
.
You can expand the list by overriding allowed_sort_attributes
list like this:
def allowed_sort_attributes
super + %w{employee_name company_name}
end
This will allow to sort by all Employee attributes + employee_name
and company_name
Or restrict it like this:
def allowed_sort_attributes
%w{employee_name}
end
in this example sorting allowed only by employee_name
.
ActionView
If you are using Rails or bare ActionView there are few helper methods at your disposal to help generating links
for sorting. Currently there are 2 methods available: sort_url_for
and sort_class_for
.
Example of usage in HAML:
%ul
%li{ :class => (sort_class_for employees, 'id') }
= link_to 'ID', sort_url_for(employees, 'id')
%li{ :class => (sort_class_for employees, 'employee_name') }
= link_to 'Employee', sort_url_for(employees, 'employee_name')
%li{ :class => (sort_class_for employees, 'company_name') }
= link_to 'Company', sort_url_for(employees, 'company_name')
In this example li
will receive class="sorted order_down"
or class="sorted order_up"
if this attribute was used for search.
Generated url will be enhanced with sort
and order
attributes.
Search
Mincer borrowed allot of search logic from PgSearch. Currently search only works with postgres.
Example of usage:
employees = EmployeesListQuery.new(Employee, {'pattern' => 'whatever'})
By default search will be performed on all text/string columns of current model. If you want to explicitly set searchable columns or you can do so using pg_search
method:
pg_search [{ :columns => %w{employees.full_name companies.name} } ]
By default search will use [unaccent] to ignore accent marks. You can read more about unaccent
here
You need to enable unaccent
extension. If you use Rails, please use migration for that:
enable_extension 'unaccent'
or run CREATE EXTENSION IF NOT EXISTS unaccent;
If by any chance you need to disable unaccent
:
pg_search [{ :columns => %w{employees.full_name companies.name} }, :ignore_accent => false ]
If you set any_word
attribute to true - search will return all items containing any word in the search terms.
pg_search [{ :columns => %w{employees.full_name companies.name} }, :any_word => true ]
If you set ignore_case
attribute to true - search will ignore case.
pg_search [{ :columns => %w{employees.full_name companies.name} }, :ignore_case => true ]
If you set prefix_matching
attribute to true - lexemes in a tsquery can will be labeled with * to specify prefix matching.
pg_search [{ :columns => %w{employees.full_name companies.name} }, :prefix_matching => true ]
Options like unaccent
, any_word
, ignore_case
, prefix_matching
can be set to be used only on query or document. In Example if you use specific column that already has unaccented and lowercased text with GIN/GIST index and do not want to additionally use unaccent
or ignore_case
functions on that column(because this will cause index not to work) -you can disable those options. Ex.
pg_search [{ :columns => %w{employees.full_name} }, :ignore_case => {query: true} ]
This way ignore_case
function will be used only on pattern that you are searching for and not on columns.
If you set param_name
attribute to any other string - this string will be used to extract search term from params(Default param_name = 'patern').
pg_search [{ :columns => %w{employees.full_name companies.name} }, :param_name => 's']
employees = EmployeesListQuery.new(Employee, {'s' => 'whatever'})
There are 3 search engines you can use: trigram
, fulltext
and array
.
You can specify which one to use, along with other options like this:
pg_search [{ :columns => %w{employees.full_name companies.name}, :engines => [:fulltext, :trigram] ,:ignore_case => true, :threshold => 0.5, :dictionary => :english }]
You can also add several search statements:
pg_search [
{ :columns => %w{employees.full_name}, :engines => [:fulltext, :trigram] ,:ignore_case => true},
{ :columns => %w{employees.tags}, :engines => [:array] ,:ignore_case => true, :any_word => true, param_name: 'tag'}
]
employees = EmployeesListQuery.new(Employee, {'patern' => 'whatever', 'tag' => 'fired'})
In this Mincer will search for all employees that are fired OR patern matches full_name. You can use additional option join_with: :and
. To specify that you need only employees whith matching full name and tag
pg_search [
{ :columns => %w{employees.full_name}, :engines => [:fulltext, :trigram] ,:ignore_case => true},
{ :columns => %w{employees.tags}, :engines => [:array] ,:ignore_case => true, :any_word => true, param_name: 'tag'}
], join_with: :and
You can read details on search engines here: Trigram, Fulltext
JSON generation
Mincer allows you to dump query result to JSON using Postgres JSON Functions Didn't had time to do benchmarking, but it's extremely fast.
Pros:
- Speed
- No extra dependencies(you don't need any other JSON generators)
Cons:
- Works only with postgres version >= 9.2
- If you are using ruby methods to generate some fields - you won't be able to use them in Mincer objects(Carrierwave image_urls, resource urls). You will have to duplicate logic inside postgres select query.
To dump query result to json string you have to call to_json
on Mincer object:
EmployeesListQuery.new(Employee).to_json
In our example it will return something like this
"[{\"id\":1,\"employee_name\":\"John Smith\",\"company_name\":\"Microsoft\"},{\"id\":2,\"employee_name\":\"Jane Smith\",\"company_name\":\"37 Signals\"}]"
In addition you can pass option root
to to_json
method if you need to include root to json string:
EmployeesListQuery.new(Employee).to_json(root: 'employees')
# returns
"{\"employees\":[{\"id\":1,\"employee_name\":\"John Smith\",\"company_name\":\"Microsoft\"},{\"id\":2,\"employee_name\":\"Jane Smith\",\"company_name\":\"37 Signals\"}]}"
Digest
Digest is very useful for cache invalidation on your views when you are using custom queries. We will modify a bit example:
class EmployeesListQuery < Mincer::Base
digest! %w{employee_updated_at company_updated_at}
def build_query(relation, args)
custom_select = <<-SQL
employees.id,
employees.full_name as employee_name,
companies.name as company_name,
employees.updated_at as employee_updated_at,
companies.updated_at as company_updated_at
SQL
relation.joins(:company).select(custom_select)
end
end
In this example we will use 2 updated_at timestamps to generate digest. Whenever one of them will change - digest will change also. To get digest you should use method digest
on Mincer model
EmployeesListQuery.new(Employee).digest # "\\x20e93b4dc5e029130f3d60d697137934"
To generate digest you need to install extension 'pgcrypto'. If you use Rails, please use migration for that
enable_extension 'pgcrypto'
or run CREATE EXTENSION IF NOT EXISTS pgcrypto;
TODO
- Create general configuration for Mincer that would allow to:
- Change sort html classes
- Change default arguments(sort, order, pattern, page, per_page..)
- Disable some processors for all Mincer objects
- Create rails generators.
- Add coalescing as option for document
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request
License
Copyright (c) 2013-2014 Alex Krasynskyi
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.