Repositor
Installation & Setup
Manual:
gem install 'repositor'
For gemfile:
gem 'repositor'
And run in console:
bundle install
Description
This gem is an implementation of Repository Pattern described in book Fearless Refactoring Rails Controllers by Andrzej Krzywda 2014 (c). Awesome book, recommend read for all who are scary open own controller files ;)
I split record instance manage and collection manage into two almost same (but not) layers - Repos & Queries
Repo - for single record (:find, :new, :create, :update, :destroy)
Query - for collection of records (:all, :where and others)
The main reason to user RepoObject is that your controller don't communicate with ORM layer (ActiveRecord or Mongoid). It will communicate with Repo/Query so you are not stricted about your database adapter or data API. It's some sort of anti-corruption layer also. If in future you will want to change it, you will need just to reconfigure your Repository layer. Sounds nice. Let's try it..
With some support of helper_method
your controller can be only 50-60 lines of code. Nothing more.
With Repo and Query you controller could look something like this:
class ProductsController < ApplicationController
# you are free from any action callbacks...
# but define this 2 helper methods:
helper_method :product, :products
def create
@product = repo.create(product_params)
@product.valid? ? redirect_to(default_redirect) : render(:new)
end
def update
@product = repo.update(product, product_params)
@product.valid? ? redirect_to(default_redirect) : render(:edit)
end
def destroy
repo.destroy(product) and redirect_to default_redirect
end
private
def product_params
params.require(:product).permit(:name, :price, :dscription)
end
def default_redirect
products_path
end
# Helper method that find or init new instance for you and cache it in ivar
# You can use it for at view show action just `product` method
# or for _form partial also `product`
def product
@product ||= repo.find_or_initialize(params[:id])
end
# Second helper that allow to cache all collection
# At view method `products` allows you access to the colelction
# No any @'s anymore!
def products
@products ||= query.all
end
# Declaration of repo object:
def repo
@products_repo ||= ProductRepo.new
end
# Declaration of query object:
def query
@product_query ||= ProductQuery.new
end
# By default repositor will try to find `Product` model and communicate with it
# if you need specify other model, pass in params
# ProductRepo.new(model: TopProduct)
# or
# ProductQuery.new(model: SaleProduct)
end
How to use
By generator:
rails generate repos
Or manually:
In app
directory you need to create new repos
and queries
directory . Recomended to create application_repo.rb
and inherit from it all repos, so you could keep all your repos under single point of inheritance. (Same for queries)
app/repos/application_repo.rb
class ApplicationRepo < Repositor::Repo::ActiveRecordAdapter
# now supported only ActiveRecord but will be added more soon
#
# Adapter allow you to use 4 default methods for CRUD:
# :new, :create, :update, :destroy
#
# Only 1 for find record
# :find
#
# And additional helpers
# find_or_initialize(id, friendly: false) => support for friendly_id gem
# or
# friendly_find(slugged_id)
end
app/queries/application_query.rb
class ApplicationQuery < Repositor::Query::ActiveRecordAdapter
# now supported only ActiveRecord but will be added more soon
#
# Adapter allow you to use 3 methods for CRUD:
# :all, :first, :last
end
Than you need to create app/repos/product_repo.rb
:
class ProductRepo < ApplicationRepo
# here you have default methods for repository actions
# if you want communicate with model class,
# just can use model method to send it any method you need
# Very good approach is that you got a place where you can
# control persistence process, define some logic and reuse it everywhere.
def update(record, params)
result = record.update(params) if params[:ok] == 'ok'
if result
# trigger some event
end
result # don't forgot return object
end
end
and app/queries/product_query.rb
class ProductQuery < ApplicationQuery
# here you can define all scopes
# ATTENTION! Your queries always must reutrn Relation (!!!)
# Simple scopes extraction from model
def active
where(status: 'active')
end
def disabled(status: 'disabled')
where(status: 'active')
end
# You can combine queries
def active_and_disabled
active | disabled
end
end
Also Repositor
allow redirect defined record methods to instance if it was passed as first argument:
class ProductRepo < ApplicationRepo
allow_instance_methods :new_record?
end
product_repo.new_record?(product)
# same as:
product.new_record?
# And not allowed will raise exception
product_repo.persited?(product) # => NoMethodError
Only with the reason that you are not linked with data, only with it repo.
Keep your model skinny and without business logic.
TODO
- Add mongoid support
- Add sequel support
- Some improvements ? =)