Moving to repositories
no golden hammer
Arturs Meisters
Developer at Funding Circle
Classical approach
class LoanSignupController < ApplicationController
def create
loan = Loan.create(params[:loan])
if loan.persisted?
redirect_to loan_path(loan.id)
else
render :new
end
end
end
class Loan < ActiveRecord::Base
validate :amount, :title, persistence: true
end
Simple!
It becomes trickier
class Loan < ActiveRecord::Base
belong_to :borrower
has_many :loan_parts
has_many :loan_part_distributions, through: :loan_parts
has_many :investors
has_many :repayments
validates :amount, :title, :description, presence: true
validates :amount,
numericality: {
greater_than: 1000,
less_than: 100_000
}
end
...that’s without any methods
Why did this happen?
fields in DB that shouldn't be exposed
def show
loan = Loan.find(params[:id])
render :json
end
def show
loan = LoanPresenter.new(Loan.find(params[:id]))
render loan.to_json
end
class LoanPresenter
def initialize(loan)
@loan = loan
end
def to_json
{
id: @loan.id,
amount: @loan.amount.to_money,
title: @loan.title
}
end
end
read-only fields
● attr_accessible
● strong parameters
a lot of validations
user experience that doesn't map to DB
schema directly
def create
user = User.new(params[:user])
if user.valid?
user.save!
redirect_to :user_profile(user)
else
render :new
end
end
def create
user = User.create_with_profile_and_sequrity_questions(params[:user])
# and so on...
end
def create_with_profile_and_sequrity_questions(params)
profile = Profile.new(
first_name: first_name[:first_name],
last_name: params[:last_name]
)
user_params = params.reject{|k,_|
[:first_name, :last_name].include?(k)
}
create(user_params.merge(profile: profile))
end
complex domain logic
lengthy life cycle with many different states
validates :amount, :title, presence: true, if: "active?"
validates :closed_at, presence: true if: "closed?"
validates :recovered_amount, if: "defaulted?"
before :assign_goverment_bid if: "fully_funded?"
So what is active record?
“An object that wraps a row in database table
or view, encapsulates database access, and
adds domain logic on that data.”
Martin Fowler
Patterns of Enterprise Application Architecture (2003)
An object that wraps a row in database table
or view, encapsulates database access, and
adds domain logic on that data.
Martin Fowler
Patterns of Enterprise Application Architecture (2003)
Our ActiveRecord models had a lot of domain
logic in them
… and almost every change in persistence layer
caused problems within domain layer and vice
versa
It is fast and easy to start with ActiveRecord,
but it may not be enough
Moving forward
monolith to SOA
Goals
● process driven application
● manageable chunks of code
● moving in small steps
A way
API dictates how and what data needs to be
returned
class LoanEndpoint
#... skipped code
get "/investor/:id/loans" do
loans = LoanRepository.new.all_for_investor(params[:id])
render(loans)
end
end
separate domain logic from data access layer
class LoanRepository
def all_for_investor(investor_id)
loans = Loan.join(:investor)
.where(investor_id: investor)
loans.map{|loan| map_to_entity(loan) }
end
#... skipped code
end
class LoanRepository
#... skipped code
private
def map_to_entity(record)
Entities::Loan.new.tap do |loan|
loan.id = record.id
loan.title = record.title
loan.amount_cents = (record.amount * 100).to_i
end
end
end
class LoanRepository
#... skipped code
private
def map_to_entity(record)
mapper.map(record)
end
def mapper
@mapper ||= Loan::Mapper.new
end
end
class LoanEntity
attribute :id
attribute :status
attribute :owner_id
attribute :loan_parts
def principal_remaining
if status != "paid"
loan_parts.inject(0){|sum, loan_part|
sum + loan_part.principal_remaining
}
else
0
end
end
end
class LoanValidator < ActiveModel::Validator
MINIMUM_LOAN_AMOUNT = 5_000
def validate(loan_entity)
unless loan.amount >= MINIMUM_LOAN_AMOUNT
loan.errors.add(
:amount,
"must be greater or equal to #{MINIMUM_LOAN_AMOUNT}"
)
end
end
end
resource’s properties are combined from
different sources
class LoanRepository
def all_for_investor(investor_id)
loans = Loan
.join(:investor)
.preload(:borrower)
.where(investor_id: investor_id)
loans.map{|loan| map_to_entity(loan) }
end
#... skipped code
end
class LoanRepository
def all_for_investor(investor_id)
loans = Loan
.join(:investor)
.preload(:borrower)
.where(investor_id: investor_id)
loans.map{|loan| map_to_entity(loan) }
end
#... skipped code
end
class LoanRepository
#... skipped code
private
def map_to_entity(record)
Entities::Loan.new.tap do |loan|
loan.id = record.id
loan.title = record.title
loan.amount_cents = (record.amount * 100).to_i
loan.borrower = Entities::Borrower.new.tap do |borrower|
borrower.id = record.borrower.id
borrower.name = record.borrower.full_name
end
end
end
end
external service as data source
class LoanRepository
def all_for_investor(investor_id)
loans = LoanService.fetch_for_investor(investor_id)
.and_then do |records|
borrower_ids = records.map{|record| record[:borrower_id] }
borrowers = Borrower.find(borrower_ids)
records.map do |record|
map_loan(record, borrowers)
end
end
end
#...skipped code
end
class LoanRepository
#...skipped code
private
def map_loan(loan, borrower)
Entities::Loan.new.tap do |loan|
loan.id = record[:id]
loan.title = record[:title]
loan.amount_cents = record[:amount_cents]
loan.borrower = map_loan_borrower(loan, borrowers)
end
end
def map_loan_borrower(loan, borrowers)
borrower = borrowers.detect{|borrower| loan.borrower_id = borrower.id }
Entities::Borrower.new.tap do |borrower|
borrower.id = record.borrower.id
borrower.name = record.borrower.full_name
end
end
end
more focus on business processes than
resources
class LoanAcceptanceService
def call
end
end
class LoanDefaultingService
def call
end
end
class UserSignUpService
def call
end
end
class SellLenderLoanPart
def initialize(lender_source = nil, loan_part_source = nil)
@lender_source = lender_source || LenderRepository.new
@loan_part_source = loan_part_source || LoanPartRepository.new
end
def call(buyer_id, loan_part_id)
buyer = @lender_source.find(buyer_id)
loan_part = @loan_part_source.find(loan_part_id)
seller_loan_part = loan_part.snapshot
@loan_part_source.create(seller_loan_part.sell)
loan_part.change_ownership_to(buyer)
@loan_part_source.update(loan_part)
end
end
Problems and challenges
● more application layers
● a little less flexibility in how you access data
● mind-shift to work with in-memory objects
● responsibilities of entities
● validations
Repositories
Repository mediates between the domain and
data mapping layers using a collection-like
interface for accessing domain objects
Martin Fowler
Patterns of Enterprise Application Architecture (2003)
Summary
● Now we are able to move in small steps
● Abstract away from data sources
● Process driven applications
but…
● I would never start with repositories
Questions?
https://github.com/kalifs

Moving to repositiories

  • 1.
  • 2.
  • 4.
  • 6.
    class LoanSignupController <ApplicationController def create loan = Loan.create(params[:loan]) if loan.persisted? redirect_to loan_path(loan.id) else render :new end end end class Loan < ActiveRecord::Base validate :amount, :title, persistence: true end
  • 7.
  • 8.
  • 9.
    class Loan <ActiveRecord::Base belong_to :borrower has_many :loan_parts has_many :loan_part_distributions, through: :loan_parts has_many :investors has_many :repayments validates :amount, :title, :description, presence: true validates :amount, numericality: { greater_than: 1000, less_than: 100_000 } end
  • 11.
  • 12.
    Why did thishappen?
  • 13.
    fields in DBthat shouldn't be exposed
  • 14.
    def show loan =Loan.find(params[:id]) render :json end
  • 15.
    def show loan =LoanPresenter.new(Loan.find(params[:id])) render loan.to_json end class LoanPresenter def initialize(loan) @loan = loan end def to_json { id: @loan.id, amount: @loan.amount.to_money, title: @loan.title } end end
  • 16.
  • 17.
  • 18.
    a lot ofvalidations
  • 19.
    user experience thatdoesn't map to DB schema directly
  • 20.
    def create user =User.new(params[:user]) if user.valid? user.save! redirect_to :user_profile(user) else render :new end end
  • 21.
    def create user =User.create_with_profile_and_sequrity_questions(params[:user]) # and so on... end def create_with_profile_and_sequrity_questions(params) profile = Profile.new( first_name: first_name[:first_name], last_name: params[:last_name] ) user_params = params.reject{|k,_| [:first_name, :last_name].include?(k) } create(user_params.merge(profile: profile)) end
  • 22.
  • 23.
    lengthy life cyclewith many different states
  • 24.
    validates :amount, :title,presence: true, if: "active?" validates :closed_at, presence: true if: "closed?" validates :recovered_amount, if: "defaulted?" before :assign_goverment_bid if: "fully_funded?"
  • 25.
    So what isactive record?
  • 26.
    “An object thatwraps a row in database table or view, encapsulates database access, and adds domain logic on that data.” Martin Fowler Patterns of Enterprise Application Architecture (2003)
  • 27.
    An object thatwraps a row in database table or view, encapsulates database access, and adds domain logic on that data. Martin Fowler Patterns of Enterprise Application Architecture (2003)
  • 28.
    Our ActiveRecord modelshad a lot of domain logic in them
  • 29.
    … and almostevery change in persistence layer caused problems within domain layer and vice versa
  • 30.
    It is fastand easy to start with ActiveRecord, but it may not be enough
  • 31.
  • 32.
  • 37.
    Goals ● process drivenapplication ● manageable chunks of code ● moving in small steps
  • 40.
  • 45.
    API dictates howand what data needs to be returned
  • 47.
    class LoanEndpoint #... skippedcode get "/investor/:id/loans" do loans = LoanRepository.new.all_for_investor(params[:id]) render(loans) end end
  • 48.
    separate domain logicfrom data access layer
  • 50.
    class LoanRepository def all_for_investor(investor_id) loans= Loan.join(:investor) .where(investor_id: investor) loans.map{|loan| map_to_entity(loan) } end #... skipped code end
  • 51.
    class LoanRepository #... skippedcode private def map_to_entity(record) Entities::Loan.new.tap do |loan| loan.id = record.id loan.title = record.title loan.amount_cents = (record.amount * 100).to_i end end end
  • 52.
    class LoanRepository #... skippedcode private def map_to_entity(record) mapper.map(record) end def mapper @mapper ||= Loan::Mapper.new end end
  • 53.
    class LoanEntity attribute :id attribute:status attribute :owner_id attribute :loan_parts def principal_remaining if status != "paid" loan_parts.inject(0){|sum, loan_part| sum + loan_part.principal_remaining } else 0 end end end
  • 54.
    class LoanValidator <ActiveModel::Validator MINIMUM_LOAN_AMOUNT = 5_000 def validate(loan_entity) unless loan.amount >= MINIMUM_LOAN_AMOUNT loan.errors.add( :amount, "must be greater or equal to #{MINIMUM_LOAN_AMOUNT}" ) end end end
  • 55.
    resource’s properties arecombined from different sources
  • 57.
    class LoanRepository def all_for_investor(investor_id) loans= Loan .join(:investor) .preload(:borrower) .where(investor_id: investor_id) loans.map{|loan| map_to_entity(loan) } end #... skipped code end
  • 58.
    class LoanRepository def all_for_investor(investor_id) loans= Loan .join(:investor) .preload(:borrower) .where(investor_id: investor_id) loans.map{|loan| map_to_entity(loan) } end #... skipped code end
  • 59.
    class LoanRepository #... skippedcode private def map_to_entity(record) Entities::Loan.new.tap do |loan| loan.id = record.id loan.title = record.title loan.amount_cents = (record.amount * 100).to_i loan.borrower = Entities::Borrower.new.tap do |borrower| borrower.id = record.borrower.id borrower.name = record.borrower.full_name end end end end
  • 60.
  • 61.
    class LoanRepository def all_for_investor(investor_id) loans= LoanService.fetch_for_investor(investor_id) .and_then do |records| borrower_ids = records.map{|record| record[:borrower_id] } borrowers = Borrower.find(borrower_ids) records.map do |record| map_loan(record, borrowers) end end end #...skipped code end
  • 62.
    class LoanRepository #...skipped code private defmap_loan(loan, borrower) Entities::Loan.new.tap do |loan| loan.id = record[:id] loan.title = record[:title] loan.amount_cents = record[:amount_cents] loan.borrower = map_loan_borrower(loan, borrowers) end end def map_loan_borrower(loan, borrowers) borrower = borrowers.detect{|borrower| loan.borrower_id = borrower.id } Entities::Borrower.new.tap do |borrower| borrower.id = record.borrower.id borrower.name = record.borrower.full_name end end end
  • 63.
    more focus onbusiness processes than resources
  • 65.
    class LoanAcceptanceService def call end end classLoanDefaultingService def call end end class UserSignUpService def call end end
  • 66.
    class SellLenderLoanPart def initialize(lender_source= nil, loan_part_source = nil) @lender_source = lender_source || LenderRepository.new @loan_part_source = loan_part_source || LoanPartRepository.new end def call(buyer_id, loan_part_id) buyer = @lender_source.find(buyer_id) loan_part = @loan_part_source.find(loan_part_id) seller_loan_part = loan_part.snapshot @loan_part_source.create(seller_loan_part.sell) loan_part.change_ownership_to(buyer) @loan_part_source.update(loan_part) end end
  • 67.
    Problems and challenges ●more application layers ● a little less flexibility in how you access data ● mind-shift to work with in-memory objects ● responsibilities of entities ● validations
  • 68.
  • 69.
    Repository mediates betweenthe domain and data mapping layers using a collection-like interface for accessing domain objects Martin Fowler Patterns of Enterprise Application Architecture (2003)
  • 70.
    Summary ● Now weare able to move in small steps ● Abstract away from data sources ● Process driven applications but… ● I would never start with repositories
  • 71.

Editor's Notes

  • #27 consider moving this up, before examples
  • #30 because of testing because everything touch db because of chain calls from one method to another in models
  • #38 migration to SOA before this slide - maybe one slide
  • #52 straight forward mapping
  • #53 straight forward mapping
  • #54 straight forward mapping
  • #55 straight forward mapping
  • #68 new problems that this pattern introduces
  • #69 desired end goal where we want to be after x months
  • #71 performance issues, persistance, validation problems we have. to go back to ideas slide and show how this solves it . summarize