You are on page 1of 41

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:


ProductCategoryAssignment over ProductCategory
ProductBundleMember over BundleProduct

Friday, April 22, 2011


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:


address_2 over address2

• Reserve _id For True Foreign Keys:


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 # spec/models/order_spec.rb
describe Order do
# app/models/order/workflow.rb
module Order::Workflow end

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

# app/models/order/payment.rb end
module Order::Payment
# spec/models/order/payment_spec.rb
describe Order, "Payment" do
end
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
class FileImport < ActiveRecord::Base
has_attached_file :file

• Better Bug Isolation


validate :ensure_file_is_valid

protected
from Traces def ensure_file_is_valid

• Increases Self-
ensure_rows_exist
ensure_at_least_two_columns
ensure_one_column_contains_unique_values
Documentation end

Dramatically def ensure_rows_exist


...
end

def ensure_at_least_two_columns
...
end

def ensure_one_column_contains_unique_values
...
end
end
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
end @order_item = @order.items.build(
: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 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


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

Friday, April 22, 2011