URL
EMBED
Page 0:
Page 1: Advanced Active Record Techniques
Best Practice Refactoring
Chad Pytel cpytel@thoughtbot.com
Page 2: Advanced Active Record Techniques
Best Practice Refactoring
Chad Pytel cpytel@thoughtbot.com
Page 3: Simple Active Record Techniques
Best Practice Refactoring
Chad Pytel cpytel@thoughtbot.com
Page 4: Smart Active Record Techniques
Best Practice Refactoring
Chad Pytel cpytel@thoughtbot.com
Page 5: L A E LD Record Techniques R RActive O Practice Refactoring Best W
When Database Records Stop Being Polite... And Start Getting Real.
Page 6: This presentation is not going to teach you how to do test driven development.
Page 7: In order to refactor your application must be backed by a good test suite.
Page 8: This presentation is going to show you code that you might recognize.
Page 9: This presentation is going to show you how to make that code better.
Page 10: Moving Code from the Controller to the Models
Page 11: An Offending Controller
class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.publication_id = @publication.id @article.reporter_id = current_user.id begin Article.transaction do @version = @article.create_version!(params[:version], current_user) end rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid index render :action => :index and return false end redirect_to article_path(@publication, @article) end
Page 12: class ArticlesController < ApplicationController #... def update old_state = @article.state year = params[:article].delete("to_be_published_at(1i)") month = params[:article].delete("to_be_published_at(2i)") day = params[:article].delete("to_be_published_at(3i)") hour = params[:article].delete("to_be_published_at(4i)") minute = params[:article].delete("to_be_published_at(5i)") to_be_published_at = (year and month and day and hour and minute ? Time.mktime(year, month, day, hour, minute) : nil) saved = Article.transaction do if params[:send] or params[:new_version] or params[:subedit] or params[:back] or params[:send_later] or @article.state == "Published" or current_user != @article.current_version.writer @version = @article.create_version!(params[:article], current_user) else @version = @article.current_version @version.attributes = params[:article] @version.writer_id = current_user.id @version.written_at = Time.now end # update existing corrections @version.corrections = params[:corrections] # add correction for published article if old_state == "Published" if params[:correction] and !params[:correction][:correction].blank? @correction = @version.corrections.build(params[:correction]) @correction.person = current_user.name end end @version.authors = params[:authors] @version.categories = params[:categories] @version.tags = params[:tags] @version.references = params[:references] @version.relateds = params[:relateds] @version.links = params[:links]
Page 13: state_map = { "Raw" => {:send => "Edit"}, "Edit Check" => {:send => "Edit"}, "Edit" => {:send => "Published", :back => "Edit Check", :subedit => "Sub Edit", :send_later => "Publish Ready"}, "Sub Check" => {:send => "Published", :back => "Edit Check", :subedit => "Sub Edit", :send_later => "Publish Ready"}, "Sub Edit" => {:subedit => "Sub Check"}, "Publish Ready" => {:send_later => "Publish Ready"} } # if everything else is valid, then save the state if params[:send] and @version.valid? @version.state = state_map[old_state][:send] elsif params[:send_later] and @version.valid? @version.state = state_map[old_state][:send_later] elsif params[:subedit] and @version.valid? @version.state = state_map[old_state][:subedit] elsif params[:back] and @version.valid? @version.state = state_map[old_state][:back] end # default and fallback @version.state ||= @article.state # set publish dates if needed if @version.state == "Published" published_at = Time.now @article.first_published_at ||= published_at @article.last_published_at = published_at elsif @version.state == "Publish Ready" @article.to_be_published_at = to_be_published_at if to_be_published_at end if !params[:pdf].blank? @article.pdf = params[:pdf] @article.print = true end @article.save! @version.save! end rescue false
Page 14: if saved post_to_api(:staging) email_notifications(@article, old_state, @version.state) if @version.state == "Published" # this may become a special republish api call that doesn't have to do as much work if a correction has been specified (meaning it's a republish) post_to_api(:live) end if params[:send] and @version.state == "Edit" flash[:success] = "Article saved and ready for editing." elsif params[:subedit] if @version.state == "Sub Check" flash[:success] = "Article sent back for editing." elsif @version.state == "Sub Edit" flash[:success] = "Article sent to sub editors." end elsif params[:back] if @version.state == "Edit Check" flash[:success] = "Article sent back to reporter for further work." end elsif @version.state == "Publish Ready" flash[:success] = "Article has been marked for publishing at a later date." elsif @version.state == "Published" and old_state != "Published" flash[:success] = "Article saved and published to live server." elsif @version.state == "Published" flash[:success] = "Article has been republished." end # default and fallback flash[:success] ||= "Article updated." # this is temporary, as dispatcher is set up only on staging environment for now if RAILS_ENV == "staging" flash[:success] += " Changes may not be immediately available for preview on foxtrot." end redirect_to article_path(@publication, @article) and return true else load_article_data render :action => 'edit' and return false end end
Page 15: An Offending Controller
class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.publication_id = @publication.id @article.reporter_id = current_user.id begin Article.transaction do @version = @article.create_version!(params[:version], current_user) end rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid index render :action => :index and return false end redirect_to article_path(@publication, @article) end
Page 16: An Offending Controller
class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.publication_id = @publication.id @article.reporter_id = current_user.id begin Article.transaction do @version = @article.create_version!(params[:version], current_user) end rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid index render :action => :index and return false end redirect_to article_path(@publication, @article) end
Page 17: An Offending Controller
class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.publication_id = @publication.id @article.reporter_id = current_user.id begin Article.transaction do @version = @article.create_version!(params[:version], current_user) end rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid index render :action => :index and return false end redirect_to article_path(@publication, @article) end
Page 18: An Offending Controller
class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.publication_id = @publication.id @article.reporter_id = current_user.id begin Article.transaction do @version = @article.create_version!(params[:version], current_user) end rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid index render :action => :index and return false end redirect_to article_path(@publication, @article) end
Page 19: A Better Controller
class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.publication_id = @publication.id @article.reporter_id = current_user.id if @article.save redirect_to article_path(@publication, @article) else index render :action => :index end end
Page 20: Here is How We Get There
The Current create_version! Method
def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.article_id = self.id version.written_at = Time.now version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end
Page 21: Here is How We Get There
The Current create_version! Method
def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.article_id = self.id version.written_at = Time.now version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end
Page 22: Here is How We Get There
The Current create_version! Method
def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.article_id = self.id version.written_at = Time.now version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end
Page 23: Here is How We Get There
The Current create_version! Method
def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.article_id = self.id version.written_at = Time.now version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end
Page 24: Here is How We Get There
The Current create_version! Method
def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.article_id = self.id version.written_at = Time.now version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end
Page 25: Here is How We Get There
The Current create_first_version! Method
def create_first_version!(attributes, user) version = self.versions.build(attributes) version.written_at = Time.now version.writer_id = user.id version.state ||= "Raw" version.version = 1 self.save! self.update_attribute(:current_version_id, version.id) version end
Page 26: Here is How We Get There
Identifying Things to Refactor: Similar Code
def create_first_version!(attributes, user) version = self.versions.build(attributes) version.written_at = Time.now version.writer_id = user.id version.state ||= "Raw" version.version = 1 self.save! self.update_attribute(:current_version_id, version.id) version end def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.article_id = self.id version.written_at = Time.now version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end
Page 27: Here is How We Get There
Identifying Things to Refactor: Similar Code
def create_first_version!(attributes, user) version = self.versions.build(attributes) version.written_at = Time.now version.writer_id = user.id version.state ||= "Raw" version.version = 1 self.save! self.update_attribute(:current_version_id, version.id) version end def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.article_id = self.id version.written_at = Time.now version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end
Page 28: Here is How We Get There
Unnecessary Code
def create_first_version!(attributes, user) version = self.versions.build(attributes) version.written_at = Time.now version.writer_id = user.id version.state ||= "Raw" version.version = 1 self.save! self.update_attribute(:current_version_id, version.id) version end def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.article_id = self.id version.written_at = Time.now version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end
Page 29: Here is How We Get There
Use Active Record’s Built-in Functionality
def create_first_version!(attributes, user) version = self.versions.build(attributes) version.written_at = Time.now version.writer_id = user.id version.state ||= "Raw" version.version = 1 self.save! self.update_attribute(:current_version_id, version.id) version end def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.written_at = Time.now version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end
Page 30: Here is How We Get There
Use Active Record’s Built-in Functionality
def create_first_version!(attributes, user) version = self.versions.build(attributes) version.written_at = Time.now version.writer_id = user.id version.state ||= "Raw" version.version = 1 self.save! self.update_attribute(:current_version_id, version.id) version end def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.written_at = Time.now version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end
Page 31: Here is How We Get There
Manually Setting Default Values
def create_first_version!(attributes, user) version = self.versions.build(attributes) version.writer_id = user.id version.state ||= "Raw" version.version = 1 self.save! self.update_attribute(:current_version_id, version.id) version end def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.article_id = self.id version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end
Page 32: Here is How We Get There
Manually Setting Default Values
def create_first_version!(attributes, user) version = self.versions.build(attributes) version.writer_id = user.id version.state ||= "Raw" version.version = 1 self.save! self.update_attribute(:current_version_id, version.id) version end def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.article_id = self.id version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end
Page 33: Here is How We Get There
Set Default Values in the Database
class AddRawDefaultToState < ActiveRecord::Migration def self.up change_column_default :article_versions, :state, "Raw" end def self.down execute "ALTER TABLE article_versions ALTER state DROP DEFAULT" end end
Page 34: Here is How We Get There
Remove Default Fixed in 2.1
class AddRawDefaultToState < ActiveRecord::Migration def self.up change_column_default :article_versions, :state, "Raw" end def self.down change_column_default :article_versions, :state, nil end end
Page 35: Here is How We Get There
Fodder for Callbacks
def create_first_version!(attributes, user) version = self.versions.build(attributes) version.writer_id = user.id version.version = 1 self.save! self.update_attribute(:current_version_id, version.id) version end def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end
Page 36: Here is How We Get There
Fodder for Callbacks
def create_first_version!(attributes, user) version = self.versions.build(attributes) version.writer_id = user.id version.version = 1 self.save! self.update_attribute(:current_version_id, version.id) version end def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end
Page 37: Here is How We Get There
Fodder for Callbacks
def create_first_version!(attributes, user) version = self.versions.build(attributes) version.writer_id = user.id version.version = 1 self.save! self.update_attribute(:current_version_id, version.id) version end def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current def version if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } self.current_version.version end end version = self.versions.build(attributes) version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end
Page 38: Here is How We Get There
A Callback on ArticleVersion
class ArticleVersion < ActiveRecord::Base before_validation_on_create :set_version_number private def set_version_number self.version = (article.current_version ? article.current_version.version : 0) + 1 end
Page 39: Here is How We Get There
A Callback on ArticleVersion
class ArticleVersion < ActiveRecord::Base before_validation_on_create :set_version_number private def set_version_number self.version = (article.current_version ? article.current_version.version : 0) + 1 end
Page 40: Here is How We Get There
Now Code is Identical
def create_first_version!(attributes, user) version = self.versions.build(attributes) version.writer_id = user.id self.save! self.update_attribute(:current_version_id, version.id) version end def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.writer_id = user.id self.save! self.update_attribute(:current_version_id, version.id) version end
Page 41: Here is How We Get There
Now Code is Identical
def create_first_version!(attributes, user) version = self.versions.build(attributes) version.writer_id = user.id self.save! self.update_attribute(:current_version_id, version.id) version end def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.writer_id = user.id self.save! self.update_attribute(:current_version_id, version.id) version end
Page 42: Here is How We Get There
Now Code is Identical
def create_first_version!(attributes, user) version = self.versions.build(attributes) version.writer_id = user.id self.save! self.update_attribute(:current_version_id, version.id) version end def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.writer_id = user.id self.save! self.update_attribute(:current_version_id, version.id) version end
Page 43: Here is How We Get There
Identify Another Callback
def create_version!(attributes, user) unless self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each do |rel| rel.update_attribute(:current, false) end end end version = self.versions.build(attributes) version.writer_id = user.id self.save! self.update_attribute(:current_version_id, version.id) version end
Page 44: Here is How We Get There
Identify Another Callback
def create_version!(attributes, user) unless self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each do |rel| rel.update_attribute(:current, false) end end end version = self.versions.build(attributes) version.writer_id = user.id self.save! self.update_attribute(:current_version_id, version.id) version end
Page 45: Here is How We Get There
Expressive Callback Names
class ArticleVersion < ActiveRecord::Base before_validation_on_create :set_version_number before_create :mark_related_links_not_current private def set_version_number self.version = (article.current_version ? article.current_version.version : 0) + 1 end def mark_related_links_not_current unless article.versions.empty? # mark old related links as not current if article.current_version.relateds.any? article.current_version.relateds.each do |rel| rel.update_attribute(:current, false) end end end end
Page 46: Here is How We Get There
Expressive Callback Names
class ArticleVersion < ActiveRecord::Base before_validation_on_create :set_version_number before_create :mark_related_links_not_current private def set_version_number self.version = (article.current_version ? article.current_version.version : 0) + 1 end def mark_related_links_not_current unless article.versions.empty? # mark old related links as not current if article.current_version.relateds.any? article.current_version.relateds.each do |rel| rel.update_attribute(:current, false) end end end end
Page 47: Here is How We Get There
Do What You Mean
class ArticleVersion < ActiveRecord::Base before_validation_on_create :set_version_number before_create :mark_related_links_not_current private def set_version_number self.version = (article.current_version ? article.current_version.version : 0) + 1 end def mark_related_links_not_current unless article.versions.empty? if article.current_version.relateds.any? article.current_version.relateds.each do |rel| rel.update_attribute(:current, false) end end end end
Page 48: Here is How We Get There
Do What You Mean
class ArticleVersion < ActiveRecord::Base before_validation_on_create :set_version_number before_create :mark_related_links_not_current private def set_version_number self.version = (article.current_version ? article.current_version.version : 0) + 1 end def mark_related_links_not_current unless article.versions.empty? if article.current_version.relateds.any? article.current_version.relateds.each do |rel| rel.update_attribute(:current, false) end end end end
Page 49: Here is How We Get There
More Unnecessary Code
class ArticleVersion < ActiveRecord::Base before_validation_on_create :set_version_number before_create :mark_related_links_not_current private def set_version_number self.version = (article.current_version ? article.current_version.version : 0) + 1 end def mark_related_links_not_current if article.current_version if article.current_version.relateds.any? article.current_version.relateds.each do |rel| rel.update_attribute(:current, false) end end end end
Page 50: Here is How We Get There
More Unnecessary Code
class ArticleVersion < ActiveRecord::Base before_validation_on_create :set_version_number before_create :mark_related_links_not_current private def set_version_number self.version = (article.current_version ? article.current_version.version : 0) + 1 end def mark_related_links_not_current if article.current_version if article.current_version.relateds.any? article.current_version.relateds.each do |rel| rel.update_attribute(:current, false) end end end end
Page 51: Here is How We Get There
Minor Law of Demeter Violation
class ArticleVersion < ActiveRecord::Base before_validation_on_create :set_version_number before_create :mark_related_links_not_current private def set_version_number self.version = (article.current_version ? article.current_version.version : 0) + 1 end def mark_related_links_not_current if article.current_version article.current_version.relateds.each do |rel| rel.update_attribute(:current, false) end end end
Page 52: Here is How We Get There
Minor Law of Demeter Violation
class ArticleVersion < ActiveRecord::Base before_validation_on_create :set_version_number before_create :mark_related_links_not_current private def set_version_number self.version = (article.current_version ? article.current_version.version : 0) + 1 end def mark_related_links_not_current if article.current_version article.current_version.relateds.each do |rel| rel.update_attribute(:current, false) end end end
Page 53: Here is How We Get There
Conditional Callbacks
class ArticleVersion < ActiveRecord::Base before_validation_on_create :set_version_number before_create :mark_related_links_not_current private def current_version article.current_version end def set_version_number self.version = (current_version ? current_version.version : 0) + 1 end def mark_related_links_not_current if current_version current_version.relateds.each do |rel| rel.update_attribute(:current, false) end end end
Page 54: Here is How We Get There
Conditional Callbacks
class ArticleVersion < ActiveRecord::Base before_validation_on_create :set_version_number before_create :mark_related_links_not_current private def current_version article.current_version end def set_version_number self.version = (current_version ? current_version.version : 0) + 1 end def mark_related_links_not_current if current_version current_version.relateds.each do |rel| rel.update_attribute(:current, false) end end end
Page 55: Here is How We Get There
Conditional Callback
class ArticleVersion < ActiveRecord::Base before_validation_on_create :set_version_number before_create :mark_related_links_not_current, :if => :current_version private def current_version article.current_version end def set_version_number self.version = (current_version ? current_version.version : 0) + 1 end def mark_related_links_not_current current_version.relateds.each do |rel| rel.update_attribute(:current, false) end end
Page 56: Here is How We Get There
The New create_version! Method
def create_version!(attributes, user) version = self.versions.build(attributes) version.writer_id = user.id self.save! self.update_attribute(:current_version_id, version.id) version end
Page 57: Here is How We Get There
The New create_version! Method
def create_version!(attributes, user) version = self.versions.build(attributes) version.writer_id = user.id self.save! self.update_attribute(:current_version_id, version.id) version end
Page 58: Here is How We Get There
Callback to Update the Current Version
class ArticleVersion < ActiveRecord::Base before_validation_on_create :set_version_number before_create :mark_related_links_not_current, :if => :current_version after_create :set_current_version_on_article private def set_current_version_on_article article.update_attribute :current_version_id, self.id end
Page 59: Here is How We Get There
The New create_version! Method
def create_version!(attributes, user) version = self.versions.build(attributes) version.writer_id = user.id self.save! version end
Page 60: Here is How We Get There
The New create_version! Method
def create_version!(attributes, user) version = self.versions.build(attributes) version.writer_id = user.id self.save! version end
Page 61: Here is How We Get There
The Current Create Action
class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.publication_id = @publication.id @article.reporter_id = current_user.id begin Article.transaction do @version = @article.create_version!(params[:version], current_user) end rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid index render :action => :index and return false end redirect_to article_path(@publication, @article) end
Page 62: Here is How We Get There
class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.publication_id = @publication.id @article.reporter_id = current_user.id @version = self.versions.build(attributes) @version.writer_id = current_user.id begin Article.transaction do @version = @article.create_version!(params[:version], current_user) end rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid index render :action => :index and return false end redirect_to article_path(@publication, @article) end
Page 63: Here is How We Get There
The New EMPTY create_version! Method
def create_version!(attributes, user) self.save! version end
Page 64: Here is How We Get There
Remove the Transaction
class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.publication_id = @publication.id @article.reporter_id = current_user.id @version = @article.versions.build(params[:version]) @version.writer_id = current_user.id begin Article.transaction do @article.save! end rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid index render :action => :index and return false end redirect_to article_path(@publication, @article) end
Page 65: Here is How We Get There
Remove the Transaction
class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.publication_id = @publication.id @article.reporter_id = current_user.id @version = @article.versions.build(params[:version]) @version.writer_id = current_user.id begin Article.transaction do @article.save! end rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid index render :action => :index and return false end redirect_to article_path(@publication, @article) end
Page 66: Here is How We Get There
Change to the Non-Bang Save
class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.publication_id = @publication.id @article.reporter_id = current_user.id @version = @article.versions.build(params[:version]) @version.writer_id = current_user.id begin @article.save! rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid index render :action => :index and return false end redirect_to article_path(@publication, @article) end
Page 67: Here is How We Get There
Change to the Non-Bang Save
class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.publication_id = @publication.id @article.reporter_id = current_user.id @version = @article.versions.build(params[:version]) @version.writer_id = current_user.id begin @article.save! rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid index render :action => :index and return false end redirect_to article_path(@publication, @article) end
Page 68: Here is How We Get There
class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.publication_id = @publication.id @article.reporter_id = current_user.id @version = @article.versions.build(params[:version]) @version.writer_id = current_user.id if @article.save redirect_to article_path(@publication, @article) else index render :action => :index end end
Page 69: Here is How We Get There
The Final Create Action
class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.publication = @publication @article.reporter = current_user @version = @article.versions.build(params[:version]) @version.writer = current_user if @article.save redirect_to article_path(@publication, @article) else index render :action => :index end end
Page 70: Phew! What Did We Learn?
• • •
Even a 15 Line Controller Action is too long. Exceptions should be exceptional. Your models should use callbacks to add complex behavior.
Page 71: Too Much Domain Knowledge
if params[:send] and @version.state == "Edit" flash[:success] = "Article saved and ready for editing." elsif params[:subedit] if @version.state == "Sub Check" flash[:success] = "Article sent back for editing." elsif @version.state == "Sub Edit" flash[:success] = "Article sent to sub editors." end elsif params[:back] if @version.state == "Edit Check" flash[:success] = "Article sent back to reporter for further work." end elsif @version.state == "Publish Ready" flash[:success] = "Article has been marked for publishing at a later date." elsif @version.state == "Published" and old_state != "Published" flash[:success] = "Article saved and published to live server." elsif @version.state == "Published" flash[:success] = "Article has been republished." end
Page 72: Too Much Domain Knowledge
if params[:send] and @version.edit? flash[:success] = "Article saved and ready for editing." elsif params[:subedit] if @version.sub_check? flash[:success] = "Article sent back for editing." elsif @version.sub_edit? flash[:success] = "Article sent to sub editors." end elsif params[:back] if @version.edit_check? flash[:success] = "Article sent back to reporter for further work." end elsif @version.publish_ready? flash[:success] = "Article has been marked for publishing at a later date." elsif @version.published? and old_state != ArticleVersion::STATES[:published] flash[:success] = "Article saved and published to live server." elsif @version.published? flash[:success] = "Article has been republished." end
Page 73: Too Much Domain Knowledge
if params[:send] and @version.edit? flash[:success] = "Article saved and ready for editing." elsif params[:subedit] if @version.sub_check? flash[:success] = "Article sent back for editing." elsif @version.sub_edit? flash[:success] = "Article sent to sub editors." end elsif params[:back] if @version.edit_check? flash[:success] = "Article sent back to reporter for further work." end elsif @version.publish_ready? flash[:success] = "Article has been marked for publishing at a later date." elsif @version.published? and old_state != ArticleVersion::STATES[:published] flash[:success] = "Article saved and published to live server." elsif @version.published? flash[:success] = "Article has been republished." end
Page 74: Too Much Domain Knowledge
class ArticleVersion < ActiveRecord::Base STATES = { :edit => 'Edit', :edit_check => 'Edit Check', :sub_edit => 'Sub Edit', :publish_ready => 'Publish Ready', :published => 'Published' } STATES.each do |key, value| define_method "#{key}?", { self.state == "#{value}" } end
Page 75: What Did We Learn?
• • •
Tackle large refactorings iteratively Push as much business logic into the model as possible In lots of code, simple DRY principles will lead a lot of refactoring
Page 76: Finders in the Controller
class ArticlesController < ApplicationController def index @articles = Article.find_all_by_state(Article::STATES[:published], :order => "created_at DESC") end end
Page 77: Move it into the Model
class ArticlesController < ApplicationController def index @articles = Article.published end end class Article < ActiveRecord::Base def self.published find_all_by_state(STATES[:published], :order => "created_at DESC") end end
Page 78: Move it into the Model
has_finder
class ArticlesController < ApplicationController def index @articles = Article.published.ordered end end class Article < ActiveRecord::Base has_finder :published, :conditions => {:state => STATES[:published]} has_finder :ordered, :order => "created_at DESC" end
Page 79: Move it into the Model
named_scope
class ArticlesController < ApplicationController def index @articles = Article.published.ordered end end class Article < ActiveRecord::Base named_scope :published, :conditions => {:state => STATES[:published]} named_scope :ordered, :order => "created_at DESC" end
Page 80: More named_scope
named_scope: passing in arguments
class ArticlesController < ApplicationController def index @articles = Article.by_state(params[:state]).ordered end end class Article < ActiveRecord::Base named_scope :ordered, :order => "created_at DESC" named_scope :by_state, lambda { |state| :conditions => {:state => state} } end
Page 81: Common Domain Patterns
Page 82: User Roles
Page 83: User Roles
The User Model
class User < ActiveRecord::Base has_and_belongs_to_many :roles, :uniq => true def has_role?(role_in_question) self.roles.find(:first, :conditions => { :name => role_in_question }) ? true : false end def has_roles?(roles_in_question) roles_in_question = self.roles.find(:all, :conditions => ["name in (?)", roles_in_question]) roles_in_question.length > 0 end def can_post? self.has_roles?(['admin', 'editor', 'associate editor', 'research writer']) end def can_review_posts? self.has_roles?(['admin', 'editor', 'associate editor']) end def can_edit_content? self.has_roles?(['admin', 'editor', 'associate editor']) end def can_edit_post?(post) self == post.user || self.has_roles?(['admin', 'editor', 'associate editor']) end end
Page 84: User Roles
The Role Model
class Role < ActiveRecord::Base has_and_belongs_to_many :users validates_presence_of :name validates_uniqueness_of :name def name=(value) write_attribute("name", value.downcase) end def self.[](name) # Get a role quickly by using: Role[:admin] self.find(:first, :conditions => ["name = ?", name.id2name]) end def add_user(user) self.users << user end def delete_user(user) self.users.delete(user) end end
Page 85: User Roles
Thoughtless Code
class User < ActiveRecord::Base has_and_belongs_to_many :roles, :uniq => true def has_role?(role_in_question) self.roles.find(:first, :conditions => { :name => role_in_question }) ? true : false end def has_roles?(roles_in_question) roles_in_question = self.roles.find(:all, :conditions => ["name in (?)", roles_in_question]) roles_in_question.length > 0 end def can_post? self.has_roles?(['admin', 'editor', 'associate editor', 'research writer']) end def can_review_posts? self.has_roles?(['admin', 'editor', 'associate editor']) end def can_edit_content? self.has_roles?(['admin', 'editor', 'associate editor']) end def can_edit_post?(post) self == post.user || self.has_roles?(['admin', 'editor', 'associate editor']) end end
Page 86: User Roles
Thoughtless Code
class User < ActiveRecord::Base has_and_belongs_to_many :roles, :uniq => true def has_role?(role_in_question) self.roles.find(:first, :conditions => { :name => role_in_question }) ? true : false end def has_roles?(roles_in_question) roles_in_question = self.roles.find(:all, :conditions => ["name in (?)", roles_in_question]) roles_in_question.length > 0 end def can_post? self.has_roles?(['admin', 'editor', 'associate editor', 'research writer']) end def can_review_posts? self.has_roles?(['admin', 'editor', 'associate editor']) end def can_edit_content? self.has_roles?(['admin', 'editor', 'associate editor']) end def can_edit_post?(post) self == post.user || self.has_roles?(['admin', 'editor', 'associate editor']) end end
Page 87: User Roles
Thoughtless Code
class User < ActiveRecord::Base has_and_belongs_to_many :roles, :uniq => true def has_role?(role_in_question) self.roles.find(:first, :conditions => { :name => role_in_question }) ? true : false end def has_roles?(roles_in_question) roles_in_question = self.roles.find(:all, :conditions => ["name in (?)", roles_in_question]) roles_in_question.length > 0 end def can_post? self.has_roles?(['admin', 'editor', 'associate editor', 'research writer']) end def can_review_posts? self.has_roles?(['admin', 'editor', 'associate editor']) end def can_edit_content? self.has_roles?(['admin', 'editor', 'associate editor']) end def can_edit_post?(post) self == post.user || self.has_roles?(['admin', 'editor', 'associate editor']) end end
Page 88: User Roles
What We’ve Done
class User < ActiveRecord::Base end
Page 89: User Roles
What We’ve Done class User < ActiveRecord::Base end
• Get rid of the role model • Make an admin, editor, and writer
boolean on the user model
• Active Record gives us nice
admin?, editor?, writer? methods on User, and the UI for giving users roles is straightforward
Page 90: User Roles
Dealing with More Roles in the Future
• •
If down the road, you need one more role, add one more boolean Two more roles: add the Role model back, but don’t use has and belongs to many
Page 91: User Roles
Adding the Role Model Back In
class User < ActiveRecord::Base has_many :roles end class Role < ActiveRecord::Base TYPES = %w(admin editor writer guest) validates_inclusion_of :name, :in => TYPES class << self TYPES.each do |role_type| define_method "#{role_type}?" do roles.exists?(:name => role_type) end end end end
Page 92: What Did We Learn?
• • •
Don’t Build Beyond Requirements Don’t Jump To a New Model Prematurely No UI to Add == No Model
Page 93: Addresses
Page 94: Addresses
A Normal Association
class User < ActiveRecord::Base validates_presence_of :billing_address_id, :shipping_address_id belongs_to :billing_address, class_name => "Address", :foreign_key => "billing_address_id" belongs_to :shipping_address, class_name => "Address", :foreign_key => "shipping_address_id" has_many :orders end class Order < ActiveRecord::Base validates_presence_of :billing_address_id, :shipping_address_id belongs_to :billing_address, class_name => "Address", :foreign_key => "billing_address_id" belongs_to :shipping_address, class_name => "Address", :foreign_key => "shipping_address_id" belongs_to :user end class Address < ActiveRecord::Base validates_presence_of :address_one, :city, :state_province, :postal_code, :country validates_format_of :postal_code, :with => /\d{5}(-\d{4})?/, :if => Proc.new { |address| address.country == "US" } validates_format_of :postal_code, :with => /[a-zA-Z]\d[a-zA-Z] \d[a-zA-Z]\d/, :if => Proc.new { |address| address.country == "CA" } end
Page 95: Addresses
class User < ActiveRecord::Base validates_presence_of :billing_address_one, :billing_city, :billing_state, :billing_zip, :billing_country validates_presence_of :shipping_address_one, :shipping_city, :shipping_state, :shipping_zip, :shipping_country validates_format_of :billing_zip, :with => /\d{5}(-\d{4})?/, :if => Proc.new { |user| user.billing_country == "US" } validates_format_of :shipping_zip, :with => /\d{5}(-\d{4})?/, :if => Proc.new { |user| user.shipping_country == "US" } validates_format_of :billing_zip, :with => /[a-zA-Z]\d[a-zA-Z] \d[a-zA-Z]\d/, :if => Proc.new { |user| user.billing_country == "CA" } validates_format_of :shipping_zip, :with => /[a-zA-Z]\d[a-zA-Z] \d[a-zA-Z]\d/, :if => Proc.new { |user| user.shipping_country == "CA" } end class Order < ActiveRecord::Base validates_presence_of :billing_address_one, :billing_city, :billing_state, :billing_zip, :billing_country validates_presence_of :shipping_address_one, :shipping_city, :shipping_state, :shipping_zip, :shipping_country validates_format_of :billing_zip, :with => /\d{5}(-\d{4})?/, :if => Proc.new { |order| order.billing_country == "US" } validates_format_of :shipping_zip, :with => /\d{5}(-\d{4})?/, :if => Proc.new { |order| order.shipping_country == "US" } validates_format_of :billing_zip, :with => /[a-zA-Z]\d[a-zA-Z] \d[a-zA-Z]\d/, :if => Proc.new { |order| order.billing_country == "CA" } validates_format_of :shipping_zip, :with => /[a-zA-Z]\d[a-zA-Z] \d[a-zA-Z]\d/, :if => Proc.new { |order| order.shipping_country == "CA" } end
No Address Model
Page 96: Addresses
An Address Module
class User < ActiveRecord::Base include Addressable end class Order < ActiveRecord::Base include Addressable end module Addressable def self.included(base) base.validates_presence_of :billing_address_one, :billing_city, :billing_state, :billing_zip, :billing_country base.validates_presence_of :shipping_address_one, :shipping_city, :shipping_state, :shipping_zip, :shipping_country base.validates_format_of :billing_zip, :with => /\d{5}(-\d{4})?/, :if => Proc.new { |klass| klass.billing_country == "US" } base.validates_format_of :shipping_zip, :with => /\d{5}(-\d{4})?/, :if => Proc.new { |klass| klass.shipping_country == "US" } base.validates_format_of :billing_zip, :with => /[a-zA-Z]\d[a-zA-Z] \d[a-zA-Z]\d/, :if => Proc.new { |klass| klass.billing_country == "CA" } base.validates_format_of :shipping_zip, :with => /[a-zA-Z]\d[a-zA-Z] \d[a-zA-Z]\d/, :if => Proc.new { |klass| klass.shipping_country == "CA" } end end
Page 97: Addresses
An Address Mixin
class User < ActiveRecord::Base has_address :shipping has_address :billing has_many :orders end module Addressable class << self def included(base) base.extend ClassMethods end end module ClassMethods def has_address(name = "") name = "#{name}_" unless name.blank? validates_presence_of "#{name}address_one", "#{name}city", "#{name}state_province", "#{name}postal_code", "#{name}country" validates_format_of "#{name}postal_code", :with => /\d{5}(-\d{4})?/, :if => Proc.new { |user| user["#{name}country"] == "US" } validates_format_of "#{name}postal_code", :with => /[a-zA-Z]\d[a-zA-Z] \d[a-zA-Z]\d/, :if => Proc.new { |user| user["#{name}country"] == "CA" } end end end ActiveRecord::Base.send :include, Addressable
Page 98: class User < ActiveRecord::Base has_address :shipping has_address :billing has_many :orders end module Addressable class << self def included(base) base.extend ClassMethods end end
Addresses
composed_of
module ClassMethods def has_address(name) name << “_” unless name.blank? validates_presence_of "#{name}address_one", "#{name}city", "#{name}state_province", "#{name}postal_code", "#{name}country" validates_format_of "#{name}postal_code", :with => /\d{5}(-\d{4})?/, :if => Proc.new { |user| user["#{name}country"] == "US" } validates_format_of "#{name}postal_code", :with => /[a-zA-Z]\d[a-zA-Z] \d[a-zA-Z]\d/, :if => Proc.new { |user| user["#{name}country"] == "CA" } composed_of "#{name}address", :class_name => “Address”, :mapping => [ ["#{name}address_one", "address_one"], ["#{name}address_two", "address_two"], ["#{name}city", "city"], ["#{name}state_province", "state_province"], ["#{name}postal_code", "postal_code"] ["#{name}country", "country"] ] end end end ActiveRecord::Base.send :include, Addressable
Page 99: Addresses
composed_of
user.billing_address = Address.new("41 Winter St.", "Floor 3", "Boston", "MA", "02108", "US") user.shipping_address = Address.new("407 Broome St.", "5th Floor", "New York", "NY", "10013", "US") user.billing_address == user.shipping_address # => false
Page 100: Bad Code Happens to Good People
Page 101: This Code Worked
Page 102: Refactoring != Bug Fixing
Page 103: Always Be Refactoring
Page 104: Questions?
Chad Pytel cpytel@thoughtbot.com Recommend Me On Working With Rails http://www.workingwithrails.com/person/5509
Page 105: