Staying railsy - while scaling complexity or Ruby on Rails in Enterprise Software

Preview:

DESCRIPTION

Staying railsy - while scaling complexity. Making Ruby on rails excel in complex software applications.

Citation preview

Staying Railsywhile scaling complexity

Rails in the Enterprise

David Williams@metakube

Of all the terms I hate with a passion, “professional” would probably rank

above “enterprise” and just below “potty mouth”.

–@dhh in 2009

What does

ENTERPRISEmean?

XML

SOAP

EDI

BPEL ABAP/4

COBOL Work

flow

automationvisibilitycompliance

it comes down to

$

But we’re software people. Start talking tech.

–y’all, just now

COMPLEXITY

What does

COMPLEXITYmean?

languagesstandard user rolescontrollers database tablespermissionslines of codeAPI calls / dayrows in biggest tablespend through system

514

166183

157758k

>100k3m

$5.6b

InternationalizationLocalization

XSS protectionSingle sign-onData securityExtensibility

CustomizationAPI integration

S*@P

mixin

sco

mple

x queries

state

machin

es

RULEm

etaco

de

mixin

sco

mple

x queries

state

machin

es

RULEm

etaco

de

Aspect Oriented Programming

Cross Cutting Concerns

Decorator Pattern

Adapter Pattern

Pointcuts

Separation of Concerns

Multiple Inheritance

Mixins

class RequisitionHeader

< ActiveRecord::Base

has_custom_fields

acts_as_revisionable

securable_by_account

api_in

api_out

end

class RequisitionHeader

< ActiveRecord::Base

has_custom_fields

acts_as_revisionable

securable_by_account

api_in

api_out

end

Let’s talk about these

mixin

sco

mple

x queries

state

machin

es

RULESm

etaco

de

I ♥ruby

class Api::DepartmentsController < Api::BaseController api_scaffold :department

end

Coupa::Application.routes.draw do namespace :api do resources :departments endend

class Department < ActiveRecord::Base

api_in [:name, :active], [], {:keys => ["id", "name"]}

api_out [:name, :active]

end

class Department < ActiveRecord::Base

api_in [:name, :active], [], {:keys => ["id", "name"]}

api_out [:name, :active]

end

(lack of) associations

class Department < ActiveRecord::Base

api_in [:name, :active], [], {:keys => ["id", "name"]}

api_out [:name, :active]

end

(lack of) associations

class Api::BaseController < AppController

def self.api_scaffold(model_id, options={})options.assert_valid_keys(:class_name, :scope, :user_scope, :validate_if, :before_save)singular_name = model_id.to_sclass_name = options[:class_name] || singular_name.camelizemodel_class = class_name.constantize

[way more code than I can fit]self.module_eval(overlay_methods)

end

end

APIs and...

bulk loaderscustom fieldssearchable table viewsbudgetingapproval workflowsrevision trackingreportingnotificationscachingand more...

APIs and...

mixin

sco

mple

x queries

state

machin

es

RULEm

etaco

de

We use AASM*Monkeypatched for transactional error handling, and for the event transitions to work like they used to.

*

class RequisitionHeader < ActiveRecord::Basestate :pending_buyer_action, :enter => Proc.new { |r|

Notifier.req_requires_action(r) }

state :pending_approval, :after_enter => Proc.new { |r|

ApprovalNotify.next_approver(r)}

state :ordered, :enter => Proc.new { |r|

OrderHeader.create_from_req(r) }end

class RequisitionHeader < ActiveRecord::Basestate :pending_buyer_action, :enter => Proc.new { |r|

Notifier.req_requires_action(r) }

state :pending_approval, :after_enter => Proc.new { |r|

ApprovalNotify.next_approver(r) }

state :ordered, :enter => Proc.new { |r|

OrderHeader.create_from_req(r) }end

class RequisitionHeader < ActiveRecord::Base event :submit_for_approval do

transitions :to => :pending_approval,

:from => [:draft, :cart], :guard => :approvable?

transitions :to => :pending_buyer_action,

:from => [:draft, :cart],

:guard => :submittable? endend

class RequisitionHeader < ActiveRecord::Base event :submit_for_approval do

transitions :to => :pending_approval,

:from => [:draft, :cart], :guard => :approvable?

transitions :to => :pending_buyer_action,

:from => [:draft, :cart],

:guard => :submittable? endend

class RequisitionHeader < ActiveRecord::Base event :submit_for_approval do

transitions :to => :pending_approval,

:from => [:draft, :cart], :guard => :approvable?

transitions :to => :pending_buyer_action,

:from => [:draft, :cart],

:guard => :submittable? endend

What’s the point?

class RequisitionHeader < ActiveRecord::Base validates_presence_of :ship_to_address,

:if => Proc.new { |requisition_header|

requisition_header.status && !%w(draft cart pending_buyer_action). include?(requisition_header.status)

}

end

class RequisitionHeader < ActiveRecord::Base def editable?

user = User.current_user case self.status when 'pending_approval' approvable_by? && user && user.authorized?('approver', 'edit') when 'cart', 'draft' user == self.requested_by || user == self.created_by when 'pending_buyer_action' user && user.authorized?('buying', 'edit') else false end endend

Yawn. Show me a hack.

–y’all, just now

module ActiveSupport::Inflector def humanize_with_translation(underscored_word) begin (I18n.translate!("statuses.#{underscored_word}",

:default => :"activerecord.models.#{underscored_word}", :count => 1) unless underscored_word.blank?) || ''

rescue I18n::MissingTranslationData => e humanize_without_translation(underscored_word) end end alias_method_chain :humanize, :translationend

I18n loose end tying: Ugly but useful

mixin

sco

mple

x queries

state

machin

es

RULEm

etaco

de

IF YOU TREAT YOUR DB AS DUMB

HOW CAN IT LOVE YOU?

IF YOU TREAT YOUR DB AS DUMB

HOW CAN IT LOVE YOU?

SELECT distinct suppliers.id FROM suppliers JOIN supplier_items ON supplier_items.supplier_id = suppliers.id LEFT OUTER JOIN catalogs ON catalogs.id = supplier_items.catalog_id LEFT OUTER JOIN contracts ON contracts.id = supplier_items.contract_id LEFT OUTER JOIN business_group_assignments ON (business_group_assignments.securable_id = contracts.id AND business_group_assignments.securable_type = 'Contract') STRAIGHT_JOIN items ON (items.id = supplier_items.item_id AND items.active = 1 AND (items.connect_item_id IS NULL OR items.imported_from_connect = 1)) WHERE ( suppliers.status = 'active' AND (supplier_items.catalog_id IS NULL OR ( catalogs.status = 'accepted' AND (catalogs.start_date IS NULL OR '2011-11-15 18:30:00' >= catalogs.start_date) AND (catalogs.end_date IS NULL OR '2011-11-15 18:30:00' < catalogs.end_date) )) AND (supplier_items.contract_id IS NULL OR (contracts.status = 'published' AND business_group_assignments.business_group_id in (3,2,1) AND contracts.start_date <= '2011-11-15 18:30:00' AND (contracts.end_date IS NULL OR contracts.end_date > '2011-11-15 18:30:00') ))

SELECT distinct suppliers.id FROM suppliers JOIN supplier_items ON supplier_items.supplier_id = suppliers.id LEFT OUTER JOIN catalogs ON catalogs.id = supplier_items.catalog_id LEFT OUTER JOIN contracts ON contracts.id = supplier_items.contract_id LEFT OUTER JOIN business_group_assignments ON (business_group_assignments.securable_id = contracts.id AND business_group_assignments.securable_type = 'Contract') STRAIGHT_JOIN items ON (items.id = supplier_items.item_id AND items.active = 1 AND (items.connect_item_id IS NULL OR items.imported_from_connect = 1)) WHERE ( suppliers.status = 'active' AND (supplier_items.catalog_id IS NULL OR ( catalogs.status = 'accepted' AND (catalogs.start_date IS NULL OR '2011-11-15 18:30:00' >= catalogs.start_date) AND (catalogs.end_date IS NULL OR '2011-11-15 18:30:00' < catalogs.end_date) )) AND (supplier_items.contract_id IS NULL OR (contracts.status = 'published' AND business_group_assignments.business_group_id in (3,2,1) AND contracts.start_date <= '2011-11-15 18:30:00' AND (contracts.end_date IS NULL OR contracts.end_date > '2011-11-15 18:30:00') ))Not very Rails

y

So let’s step back a moment and talk about a common problem...

Displaying paginated search results

What you’re querying on doesn’t always match what you’re

displaying

Suppliers

Contacts

Addresses

Commodities

Users

Countries

Roles Permissions

And this is a simple one...

Suppliers

Contacts

Addresses

Commodities

Users

Countries

Roles Permissions

And this is a simple one...

Shipping Terms

Payment Terms

Phone Numbers

Online Stores

Parent Suppliers

outer join != :include

.joins(begin options[:include].deep_exec{|c|

c.to_sym.send(:outer) }

rescue []end)

Depends on Ernie Miller’s awesome Metawhere / Squeel

Depends on a Ruby monkeypatch

Wow, that’s nasty. Why would you do that?

–y’all, just now

30% improvement in common cases

30% improvement in common cases

90% improvement in some nasty cases

30% improvement in common cases

90% improvement in some nasty cases

YMMV

MySQL doesn’t ♥ sorting

Narrow your SELECT clauses

.paginate({:select => 'SQL_CALC_FOUND_ROWS

DISTINCT #{table}.id', :per_page => 20, :page => options[:page] })

.paginate({:select => 'SQL_CALC_FOUND_ROWS

DISTINCT #{table}.id', :per_page => 20, :page => options[:page] })

MySQL count query avoidance hack. YMMV.

.paginate({:select => 'SQL_CALC_FOUND_ROWS

DISTINCT #{table}.id', :per_page => 20, :page => options[:page] })

MySQL count query avoidance hack. YMMV.

[Then query just the rows you want]

class Array def deep_exec(&blk) result = [] each do |e| if e.respond_to?(:deep_exec) result << e.deep_exec(&blk) else result << yield(e) end end result endend

Like deep_clone? Try deep_exec.

class Array def deep_exec(&blk) result = [] each do |e| if e.respond_to?(:deep_exec) result << e.deep_exec(&blk) else result << yield(e) end end result endend

class Hash def deep_exec(&blk) result = {} each do |k,v| result[yield k] =

if v.respond_to?(:deep_exec) v.deep_exec(&blk) else yield v end

end result endend

Like deep_clone? Try deep_exec.

Totally out of time...

We’re Hiring!(I know, total surprise.)