refactoring activerecord models
DESCRIPTION
Slides from my presentation at the Feburary 2011 SDRuby meeting on refactoring ActiveRecord models and best practices.TRANSCRIPT
![Page 1: Refactoring ActiveRecord Models](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/1.jpg)
Refactoring ActiveRecord
ModelsBen Hughes@rubiety
http://benhugh.es
Friday, April 22, 2011
![Page 2: Refactoring ActiveRecord Models](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/2.jpg)
Your Model Layer isImportant
Best practices are not discussed enough...
Friday, April 22, 2011
![Page 3: Refactoring ActiveRecord Models](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/3.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/4.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/5.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/6.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/7.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/8.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/9.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/10.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/11.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/12.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/13.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/14.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/15.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/16.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/17.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/18.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/19.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/20.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/21.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/22.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/23.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/24.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/25.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/26.jpg)
De-normalization Patterns
Friday, April 22, 2011
![Page 27: Refactoring ActiveRecord Models](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/27.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/28.jpg)
“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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/29.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/30.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/31.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/32.jpg)
Security & Constraints
Friday, April 22, 2011
![Page 33: Refactoring ActiveRecord Models](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/33.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/34.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/35.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/36.jpg)
Gotchas
!Friday, April 22, 2011
![Page 37: Refactoring ActiveRecord Models](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/37.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/38.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/39.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/40.jpg)
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](https://reader033.vdocuments.site/reader033/viewer/2022051109/54a0cbf3ac7959553c8b4592/html5/thumbnails/41.jpg)
That’s it!Questions/Comments?
Ben Hughes@rubiety
http://benhugh.es
Friday, April 22, 2011