refactoring activerecord models

41
Refactoring ActiveRecord Models Ben Hughes @rubiety http://benhugh.es Friday, April 22, 2011

Upload: ben-hughes

Post on 29-Dec-2014

1.363 views

Category:

Documents


0 download

DESCRIPTION

Slides from my presentation at the Feburary 2011 SDRuby meeting on refactoring ActiveRecord models and best practices.

TRANSCRIPT

Page 1: Refactoring ActiveRecord Models

Refactoring ActiveRecord

ModelsBen Hughes@rubiety

http://benhugh.es

Friday, April 22, 2011

Page 2: Refactoring ActiveRecord Models

Your Model Layer isImportant

Best practices are not discussed enough...

Friday, April 22, 2011

Page 3: Refactoring ActiveRecord Models

Organization & Style

Breaking up Models with Modularity

Extracting Repetition (Keeping Models DRY)

De-normalization Patterns

Using Callbacks & Validations with Care

Model Security & Constraints

Gotchas!

Friday, April 22, 2011

Page 4: Refactoring ActiveRecord Models

Opinions!

Some of the topics are opinionated and best practices can be argued from

multiple perspectives.

Would love to hear about alternative approaches after the talk!

Friday, April 22, 2011

Page 5: Refactoring ActiveRecord Models

Model Naming• Pluralize-able Noun:

InternationalProfile over InternationalOrderLogEntry over OrderHistoryAddressBookEntry over AddressBook

• Trade-off between context and brevity:ProductCategory vs. CategoryEmployeeGroup vs. GroupCustomerLocation vs. Location

• Use Explicit Join Model Naming:ProductCategoryAssignment over ProductCategoryProductBundleMember over BundleProduct

Friday, April 22, 2011

Page 6: Refactoring ActiveRecord Models

Model vs. Resource NamingExample::Application.routes.draw do resources :customers do resources :locations endend

class CustomerLocation < ActiveRecord::Baseend

class LocationsController < ActionController::Base def new @location = @customer.locations.build endend

# url_for([@customer, @location])# customer_customer_locations_path(@customer) => Wrong!

Friday, April 22, 2011

Page 7: Refactoring ActiveRecord Models

Model vs. Resource Namingclass CustomerLocation < ActiveRecord::Base def self.model_name ActiveSupport::ModelName.new("Location") endend

# url_for([@customer, @location])# customer_locations_path(@customer) => Right!

Friday, April 22, 2011

Page 8: Refactoring ActiveRecord Models

Attribute Naming• Underscore Casing:

first_name over firstnamezip_code over zipcode

• Err on the side of verbosity:phone_number over. phone_nopurchase_order_number over. po_num

• Optimize For String#humanize:address_2 over address2

• Reserve _id For True Foreign Keys:cim_profile_code over cim_profile_idtransaction_reference over transaction_id

• Be Consistent with name vs title, etc.

Friday, April 22, 2011

Page 9: Refactoring ActiveRecord Models

Association NamingAvoid Context-Redundancy:

class CustomerLocation < ActiveRecord::Base belongs_to :customerend

# Redundant:class Customer < ActiveRecord::Base has_many :customer_locationsend

@customer.customer_locations

# Preferred:class Customer < ActiveRecord::Base has_many :locations, :class_name => "CustomerLocation"end

@customer.locations

Friday, April 22, 2011

Page 10: Refactoring ActiveRecord Models

Method Implementations• Implement to_s

• Implement to_param

class Product < ActiveRecord::Base def to_s name endend

class Product < ActiveRecord::Base def to_param "#{id}-#{to_s.slugify}" endend

Friday, April 22, 2011

Page 11: Refactoring ActiveRecord Models

Be Consistent with Order

1. Module Inclusions

2. Attribute Protection: attr_accessible/attr_protected

3. Associations

4. Class-Level Method Invocations (acts_as_tree, etc.)

5. Scopes (Default Scope, then Named Scopes)

6. Callbacks, In Invocation Order

7. Any attr_accessor Declarations

8. Validations

9. Class Methods

10. Instance Methods (Starting with to_s, to_param)

For Example (My Personal Preference):

Friday, April 22, 2011

Page 12: Refactoring ActiveRecord Models

Model Modularityclass Order < ActiveRecord::Base include OrderWorkflow include OrderPayment ...end

# app/models/order_workflow.rbmodule OrderWorkflow end

# app/models/order_workflow.rbmodule OrderPayment end

Friday, April 22, 2011

Page 13: Refactoring ActiveRecord Models

Model Modularityclass Order < ActiveRecord::Base include Order::Workflow include Order::Payment ...end

# app/models/order/workflow.rbmodule Order::Workflow end

# app/models/order/payment.rbmodule Order::Payment end

Friday, April 22, 2011

Page 14: Refactoring ActiveRecord Models

Model Modularityclass Order < ActiveRecord::Base include Order::Workflow include Order::Payment ...end

# app/models/order/workflow.rbmodule Order::Workflow end

# app/models/order/payment.rbmodule Order::Payment end

# spec/models/order_spec.rbdescribe Order do end

# spec/models/order/workflow_spec.rbdescribe Order, "Workflow" do end

# spec/models/order/payment_spec.rbdescribe Order, "Payment" do end

Friday, April 22, 2011

Page 15: Refactoring ActiveRecord Models

Model Modularity# Traditional self.included hook:module Order::Workflow def self.included(base) base.send(:extend, ClassMethods) base.send(:include, InstanceMethods)

base.class_eval do state_machine :state, :initial => :new do ... end end end module ClassMethods ... end module InstanceMethods ... endend

Friday, April 22, 2011

Page 16: Refactoring ActiveRecord Models

Model Modularity# Using ActiveSupport::Concernmodule Order::Workflow extend ActiveSupport::Concern included do state_machine :state, :initial => :new do ... end end module ClassMethods ... end module InstanceMethods ... endend

Friday, April 22, 2011

Page 17: Refactoring ActiveRecord Models

Model Namespacingclass Enterprise::Base < ActiveRecord::Base establish_connection "enterprise_#{Rails.env}" self.abstract_class = true def self.model_name ActiveSupport::ModelName.new(self.name.split("::").last) endend

class Enterprise::Customer < Enterprise::Base has_many :locations, :class_name => "Enterprise::CustomerLocation"end

class Enterprise::CustomerLocation < Enterprise::Base belongs_to :customer, :class_name => "Enterprise::Customer"end

Friday, April 22, 2011

Page 18: Refactoring ActiveRecord Models

Extracting to Modulesmodule Votable def self.included(model) model.class_eval do has_many :votes, :class_name => 'ContentVote', :as => :votable end end def calculate_total_popularity ... endend

Friday, April 22, 2011

Page 19: Refactoring ActiveRecord Models

Small Methods with Verbose Names

• Easier to Test

• Better Bug Isolation from Traces

• Increases Self-Documentation Dramatically

class FileImport < ActiveRecord::Base has_attached_file :file validate :ensure_file_is_valid protected def ensure_file_is_valid ensure_rows_exist ensure_at_least_two_columns ensure_one_column_contains_unique_values end def ensure_rows_exist ... end def ensure_at_least_two_columns ... end def ensure_one_column_contains_unique_values ... endend

Friday, April 22, 2011

Page 20: Refactoring ActiveRecord Models

Inquiry Methodsclass Article < ActiveRecord::Base def published? published_at and published_at >= Time.zone.now endend

Friday, April 22, 2011

Page 21: Refactoring ActiveRecord Models

Composed Ofclass Customer < ActiveRecord::Base composed_of :balance, :class_name => "Money", :mapping => %w(balance amount) composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]end class Address attr_reader :street, :city def initialize(street, city) @street, @city = street, city end

def close_to?(other_address) city == other_address.city end

def ==(other_address) city == other_address.city && street == other_address.street endend

Friday, April 22, 2011

Page 22: Refactoring ActiveRecord Models

Callbacks with Care

• Try to escape tunnel vision on one use case

• Can’t cleanly disable callbacks, unlike validations

• Conditionally run callbacks when appropriate

• Avoid sending e-mails in callbacks

• Not model-level functionality to begin with

• Edge cases and accidental sending - Importers!

Friday, April 22, 2011

Page 23: Refactoring ActiveRecord Models

Conditional Validationsclass Customer attr_accessor :managing validates_presence_of :first_name validates_presence_of :last_name with_options :unless => :managing do |o| o.validates_inclusion_of :city, :in => ["San Diego", "Rochester"] o.validates_length_of :biography, :minimum => 100 endend

@customer.managing = [email protected] = params[:customer]@customer.save

Friday, April 22, 2011

Page 24: Refactoring ActiveRecord Models

Conditional Callbacksclass Customer has_many :locations before_create :create_initial_locations attr_accessor :importing protected def create_initial_locations unless importing ... end endend

@customer.importing = true

Friday, April 22, 2011

Page 25: Refactoring ActiveRecord Models

Conditional Callbacksmodule Importable def importing @importing = true yield self @importing = nil endend

class Customer < ActiveRecord::Base include Importable ...end

@customer.importing(&:save)

Friday, April 22, 2011

Page 26: Refactoring ActiveRecord Models

De-normalization Patterns

Friday, April 22, 2011

Page 27: Refactoring ActiveRecord Models

Delegationclass Order < ActiveRecord::Base has_many :items, :class => "OrderItem"end

class OrderItem < ActiveRecord::Base belongs_to :order has_many :details, :class => "OrderItemDetail"end

class OrderItemDetail < ActiveRecord::Base belongs_to :order_item delegate :order, :to => :order_item, :allow_nil => trueend

@order_item_detail.order

Friday, April 22, 2011

Page 28: Refactoring ActiveRecord Models

“Initial” Related Modelclass Ticket < ActiveRecord::Base has_many :comments before_create :create_initial_comment attr_accessor :initial_comment protected def create_initial_comment comments.build(:comment => initial_comment) if initial_comment.present? endend

@ticket = Ticket.new(:initial_comment => "First")@[email protected] # => 1

Friday, April 22, 2011

Page 29: Refactoring ActiveRecord Models

Nested Attributesclass Bundle < ActiveRecord::Base has_many :products accepts_nested_attributes_for :products, :allow_destroy => trueend

Bundle.create( :name => "My Bundle", :products_attributes => [ {:name => "One", :price => 1}, {:name => "Two", :price => 2} ])

Friday, April 22, 2011

Page 30: Refactoring ActiveRecord Models

Array Virtual Attribute for has_manyclass Customer < ActiveRecord::Base has_many :contacts before_save :maintain_contact_emails def contact_emails @contact_emails || contacts.map(&:email) end def contact_emails=(value) @contact_emails = value end protected def maintain_contact_emails if @contact_emails @contact_emails.each do |email| contacts.build(:email => email) unless contacts.exists?(:email => email) end contacts.where(["email NOT IN (?)", @contact_emails]).each(&:mark_for_destruction) end endend

Friday, April 22, 2011

Page 31: Refactoring ActiveRecord Models

Array Virtual Attribute for has_manyclass OrderItem < ActiveRecord::Base has_many :details, :class_name => "OrderItemDetail" before_save :maintain_details attr_accessor :engravings def maintain_details if details.size < quantity (quantity - details.size).times { details.build } elsif details.size > quantity (details.size - quantity).times { details.last.mark_for_destruction } end if @engravings details.each_with_index do |detail, i| detail.engraving = @engravings[i] detail.save unless detail.new_record? end end ... endend

@order_item = @order.items.build( :quantity => 2, :engravings => ["Hello Ben", "Hello John"])

@order_item.save@order_item.details.count # => 2

Friday, April 22, 2011

Page 32: Refactoring ActiveRecord Models

Security & Constraints

Friday, April 22, 2011

Page 33: Refactoring ActiveRecord Models

Always use attr_accessible!Or maybe attr_protected...

class User < ActiveRecord::Baseend

class UsersController < ActionController::Base def create @user = User.create(params[:user]) endend

# POST /users# {# 'first_name' => 'Ben',# 'last_Name' => 'Hughes',# 'admin' => '1'# }

Friday, April 22, 2011

Page 34: Refactoring ActiveRecord Models

Always use attr_accessible!Perhaps with Ryan Bates’ trusted_params...

class User < ActiveRecord::Base attr_accessible :first_name, :last_nameend

class UsersController < ActionController::Base def create params[:user].trust if admin? params[:user].trust(:spam, :important) if moderator? @user = User.create(params[:user]) endend

Friday, April 22, 2011

Page 35: Refactoring ActiveRecord Models

Careful with send!

class UsersController < ActionController def show params[:fields_to_show].map do |field| @user.send(field) if @user.respond_to?(field) end.compact endend

# GET /users/1# fields_to_show => [# 'first_name',# 'last_name',# 'destroy' !!!!!# ]

Friday, April 22, 2011

Page 36: Refactoring ActiveRecord Models

Gotchas

!Friday, April 22, 2011

Page 37: Refactoring ActiveRecord Models

Callback Return Values

class User < ActiveRecord::Base before_save :do_something protected def do_something AnotherClass.do_something(self) endend

Friday, April 22, 2011

Page 38: Refactoring ActiveRecord Models

Use :select with Caution

class User < ActiveRecord::Base def name "#{first_name} #{last_name}" endend

User.select('last_name, anniversary').each do |user| user.nameend

# ActiveRecord::MissingAttributeError: missing attribute: first_name

Friday, April 22, 2011

Page 39: Refactoring ActiveRecord Models

Careful with after_initialize

class User < ActiveRecord::Base def after_initialize self.role = Role.find_by_name("Member") unless role endend

User.all

Friday, April 22, 2011

Page 40: Refactoring ActiveRecord Models

Scopes and Non-Deterministic Methods

class User < ActiveRecord::Base scope :active, where(["activated_at > ?", Time.zone.now])end

User.active

# Should Be:class User < ActiveRecord::Base scope :active, lambda { where(["activated_at > ?", Time.zone.now]) }end

Friday, April 22, 2011

Page 41: Refactoring ActiveRecord Models

That’s it!Questions/Comments?

Ben Hughes@rubiety

http://benhugh.es

Friday, April 22, 2011