P. 1
Refactoring ActiveRecord Models

Refactoring ActiveRecord Models

4.0

|Views: 1,497|Likes:
Published by Ben Hughes
Slides from my presentation at the Feburary 2011 SDRuby meeting on refactoring ActiveRecord models and best practices.
Slides from my presentation at the Feburary 2011 SDRuby meeting on refactoring ActiveRecord models and best practices.

More info:

Categories:Types, Speeches
Published by: Ben Hughes on Feb 04, 2011
Copyright:Attribution Non-commercial

Availability:

Read on Scribd mobile: iPhone, iPad and Android.
download as PDF, TXT or read online from Scribd
See more
See less

06/23/2014

pdf

text

original

Refactoring ActiveRecord Models

Ben Hughes @rubiety http://benhugh.es

Friday, April 22, 2011

Your Model Layer is Important Best practices are not discussed enough...
Friday, April 22, 2011

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

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

Model Naming
• Pluralize-able Noun:
InternationalProfile over International OrderLogEntry over OrderHistory AddressBookEntry over AddressBook

• Trade-off between context and brevity:
ProductCategory vs. Category EmployeeGroup vs. Group CustomerLocation vs. Location

• Use Explicit Join Model Naming:
Friday, April 22, 2011

ProductCategoryAssignment over ProductCategory ProductBundleMember over BundleProduct

Model vs. Resource Naming
Example::Application.routes.draw do resources :customers do resources :locations end end class CustomerLocation < ActiveRecord::Base end class LocationsController < ActionController::Base def new @location = @customer.locations.build end end # url_for([@customer, @location]) # customer_customer_locations_path(@customer)

=> Wrong!

Friday, April 22, 2011

Model vs. Resource Naming
class CustomerLocation < ActiveRecord::Base def self.model_name ActiveSupport::ModelName.new("Location") end end # url_for([@customer, @location]) # customer_locations_path(@customer)

=> Right!

Friday, April 22, 2011

Attribute Naming
• Underscore Casing:
first_name over firstname zip_code over zipcode

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

• Optimize For String#humanize: • Reserve _id For True Foreign Keys:
address_2 over address2 cim_profile_code over cim_profile_id transaction_reference over transaction_id

• Be Consistent with name vs title, etc.
Friday, April 22, 2011

Association Naming
Avoid Context-Redundancy:
class CustomerLocation < ActiveRecord::Base belongs_to :customer end # Redundant: class Customer < ActiveRecord::Base has_many :customer_locations end @customer.customer_locations # Preferred: class Customer < ActiveRecord::Base has_many :locations, :class_name => "CustomerLocation" end @customer.locations
Friday, April 22, 2011

Method Implementations
• Implement to_s
class Product < ActiveRecord::Base def to_s name end end

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

Friday, April 22, 2011

Be Consistent with Order
For Example (My Personal Preference): 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)
Friday, April 22, 2011

Model Modularity
class Order < ActiveRecord::Base include OrderWorkflow include OrderPayment ... end # app/models/order_workflow.rb module OrderWorkflow end # app/models/order_workflow.rb module OrderPayment end

Friday, April 22, 2011

Model Modularity
class Order < ActiveRecord::Base include Order::Workflow include Order::Payment ... end # app/models/order/workflow.rb module Order::Workflow end # app/models/order/payment.rb module Order::Payment end

Friday, April 22, 2011

Model Modularity
class Order < ActiveRecord::Base include Order::Workflow include Order::Payment ... end # app/models/order/workflow.rb module Order::Workflow end # app/models/order/payment.rb module Order::Payment end

# spec/models/order_spec.rb describe Order do end # spec/models/order/workflow_spec.rb describe Order, "Workflow" do end # spec/models/order/payment_spec.rb describe Order, "Payment" do end

Friday, April 22, 2011

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 ... end end
Friday, April 22, 2011

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

Friday, April 22, 2011

Model Namespacing
class Enterprise::Base < ActiveRecord::Base establish_connection "enterprise_#{Rails.env}" self.abstract_class = true def self.model_name ActiveSupport::ModelName.new(self.name.split("::").last) end end 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

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

Friday, April 22, 2011

Small Methods with Verbose Names

• Easier to Test • Better Bug Isolation
from Traces

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 ... end end

• Increases Self-

Documentation Dramatically

Friday, April 22, 2011

Inquiry Methods
class Article < ActiveRecord::Base def published? published_at and published_at >= Time.zone.now end end

Friday, April 22, 2011

Composed Of
class 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 end end

Friday, April 22, 2011

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

Conditional Validations
class 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 end end @customer.managing = true @customer.attributes = params[:customer] @customer.save

Friday, April 22, 2011

Conditional Callbacks
class Customer has_many :locations before_create :create_initial_locations attr_accessor :importing protected def create_initial_locations unless importing ... end end end @customer.importing = true

Friday, April 22, 2011

Conditional Callbacks
module Importable def importing @importing = true yield self @importing = nil end end class Customer < ActiveRecord::Base include Importable ... end @customer.importing(&:save)

Friday, April 22, 2011

De-normalization Patterns

Friday, April 22, 2011

Delegation
class 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 => true end @order_item_detail.order
Friday, April 22, 2011

“Initial” Related Model
class 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? end end @ticket = Ticket.new(:initial_comment => "First") @ticket.save @ticket.comments.count # => 1

Friday, April 22, 2011

Nested Attributes
class Bundle < ActiveRecord::Base has_many :products accepts_nested_attributes_for :products, :allow_destroy => true end Bundle.create( :name => "My Bundle", :products_attributes => [ {:name => "One", :price => 1}, {:name => "Two", :price => 2} ] )

Friday, April 22, 2011

Array Virtual Attribute for has_many
class 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 end end
Friday, April 22, 2011

Array Virtual Attribute for has_many
class 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 @order_item = @order.items.build( end :quantity => 2, :engravings => ["Hello Ben", "Hello John"] ... ) end end @order_item.save @order_item.details.count

# => 2

Friday, April 22, 2011

Security & Constraints

Friday, April 22, 2011

Always use attr_accessible!
Or maybe attr_protected...
class User < ActiveRecord::Base end class UsersController < ActionController::Base def create @user = User.create(params[:user]) end end # POST /users # { # 'first_name' => 'Ben', # 'last_Name' => 'Hughes', # 'admin' => '1' # }

Friday, April 22, 2011

Always use attr_accessible!
Perhaps with Ryan Bates’ trusted_params...
class User < ActiveRecord::Base attr_accessible :first_name, :last_name end class UsersController < ActionController::Base def create params[:user].trust if admin? params[:user].trust(:spam, :important) if moderator? @user = User.create(params[:user]) end end

Friday, April 22, 2011

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 end end # GET /users/1 # fields_to_show => [ # 'first_name', # 'last_name', # 'destroy' !!!!! # ]

Friday, April 22, 2011

Gotchas

!
Friday, April 22, 2011

Callback Return Values
class User < ActiveRecord::Base before_save :do_something protected def do_something AnotherClass.do_something(self) end end

Friday, April 22, 2011

Use :select with Caution
class User < ActiveRecord::Base def name "#{first_name} #{last_name}" end end

User.select('last_name, anniversary').each do |user| user.name end # ActiveRecord::MissingAttributeError: missing attribute: first_name

Friday, April 22, 2011

Careful with after_initialize
class User < ActiveRecord::Base def after_initialize self.role = Role.find_by_name("Member") unless role end end User.all

Friday, April 22, 2011

Scopes and NonDeterministic 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

That’s it! Questions/Comments?
Ben Hughes @rubiety http://benhugh.es

Friday, April 22, 2011

You're Reading a Free Preview

Download
scribd
/*********** DO NOT ALTER ANYTHING BELOW THIS LINE ! ************/ var s_code=s.t();if(s_code)document.write(s_code)//-->