staying railsy - while scaling complexity or ruby on rails in enterprise software
DESCRIPTION
Staying railsy - while scaling complexity. Making Ruby on rails excel in complex software applications.TRANSCRIPT
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.)
Photo CreditsUsed under Creative Commons from typedow
Used under Creative Commons from gaymerbear
http://popartmachine.com/blog/pop-art-based-on-hawaiian-shirt-pattern.html
Used under Creative Commons from Mandy_Jansen
Used under Creative Commons from casey.marshall