Rails best practices_slides

  • 2,486 views
Uploaded on

 

More in: Education , Technology
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Be the first to comment
No Downloads

Views

Total Views
2,486
On Slideshare
0
From Embeds
0
Number of Embeds
2

Actions

Shares
Downloads
39
Comments
0
Likes
3

Embeds 0

No embeds

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide

Transcript

  • 1. 1
  • 2. controllers In space 2
  • 3. level 1 sad Code Happy Code controllers In space 3
  • 4. LEVEL 1 controllers In Space Example 4
  • 5. elsif .retweets.where(:user_id =>tweet tweet.user == current_userif current_user.id).present? LEVEL 1 controllers In Space FAT MODEL, SKINNY CONTROLLER /app/controllers/tweets_controller.rb class TweetsController < ApplicationController def retweet tweet = Tweet.find(params[:id]) flash[:notice] = flash[:notice] = "Sorry, you can't retweet your own tweets" "You already retweeted!" t = Tweet.new t.status = "RT #{tweet.user.name}: #{tweet.status}" t.original_tweet = tweet t.user = current_user t.save flash[:notice] = "Succesfully retweeted" redirect_to tweet end end end else 5
  • 6. LEVEL 1 controllers In Space elsif .retweets.where(:user_id =>self self.user == retweeter FAT MODEL, SKINNY CONTROLLER /app/controllers/tweets_controller.rb class TweetsController < ApplicationController def retweet tweet = Tweet.find(params[:id]) flash[:notice] = "Sorry, you can't retweet your own tweets" "You already retweeted!" "Succesfully retweeted" redirect_to tweet end end end else ... tweet.retweet_by(current_user) /app/models/tweet.rb class Tweet < ActiveRecord::Base def retweet_by(retweeter) end end retweeter.id).present? if 6
  • 7. @trending = Topic.find( :all, :conditions => ["started_trending > ?", 1.day.ago], :order => 'mentions desc', :limit => 5 ) LEVEL 1 controllers In Space Scope it out /app/controllers/tweets_controller.rb def index @tweets = Tweet.find( :all, :conditions => {:user_id => current_user.id}, : : 'created_at desc',order => => 10limit ... end ) 7
  • 8. LEVEL 1 controllers In Space Scope it out /app/controllers/tweets_controller.rb def index @tweets = Tweet. ... end where( ). ( ). ( ) :user_id => current_user.id 'created_at desc'order limit 10 @trending = Topic. 'started_trending > ?', 1.day.ago order 'mentions desc' limit 5 where( ). ( ). ( ) order limit current_user def index @tweets = 8
  • 9. LEVEL 1 controllers In Space Scope it out /app/controllers/tweets_controller.rb ... end ( ). ( ) 'created_at desc'order limit 10 current_user def index @tweets = .tweets. Scope to the user 9
  • 10. LEVEL 1 controllers In Space Scope it out /app/controllers/tweets_controller.rb ... end ( ( ) 'created_at desc'order limit 10current_user def index @tweets = .tweets. /app/models/tweet.rb ... end ) recent. scope class Tweet < ActiveRecord::Base :recent, 10
  • 11. default_ LEVEL 1 controllers In Space Scope it out /app/controllers/tweets_controller.rb ... end ( ( ) 'created_at desc' limit 10current_user def index @tweets = .tweets. /app/models/tweet.rb ... end )scope class Tweet < ActiveRecord::Base order 11
  • 12. limit(5) LEVEL 1 controllers In Space Scope it out /app/controllers/tweets_controller.rb ... end /app/models/topic.rb ... end scope class Topic < ActiveRecord::Base :trending, def index @trending = Topic. 'started_trending > ?', 1.day.ago order 'mentions desc' where( ( trending ) ). where('started_trending > ?', '12-01-2010 14:02') Will only work once where('started_trending > ?', '12-01-2010 14:02') 1 2 same time! . 12
  • 13. limit(5) LEVEL 1 controllers In Space Scope it out /app/controllers/tweets_controller.rb ... end /app/models/topic.rb ... end scope class Topic < ActiveRecord::Base :trending, def index @trending = Topic. 'started_trending > ?', 1.day.ago order 'mentions desc' where( ( trending ) ).lambda { } . 13
  • 14. ||num LEVEL 1 controllers In Space Scope it out /app/controllers/tweets_controller.rb ... end /app/models/topic.rb ... end scope class Topic < ActiveRecord::Base :trending, def index @trending = Topic. 'started_trending > ?', 1.day.ago order 'mentions desc' where( ( ) ).lambda { } . limit(num) (5)trending @trending = Topic.trending(5) @trending = Topic.trending wrong number of args, 0 for 1 14
  • 15. |num LEVEL 1 controllers In Space Scope it out /app/controllers/tweets_controller.rb ... end /app/models/topic.rb ... end scope class Topic < ActiveRecord::Base :trending, def index @trending = Topic. 'started_trending > ?', 1.day.ago order 'mentions desc' where( ( ) ).lambda { } . limit(num) (5)trending @trending = Topic.trending(5) @trending = Topic.trending |= nil Ruby 1.9 FTW! 15
  • 16. default_ LEVEL 1 controllers In Space Scope it out ('created_at desc' /app/models/tweet.rb ... end )scope class Tweet < ActiveRecord::Base order How do we override default scope? order(:status).limit(10)@tweets = current_user.tweets.unscoped. @tweets = current_user.tweets.order(:status).limit(10) 16
  • 17. LEVEL 1 controllers In Space Scope it out /app/controllers/tweets_controller.rb t = Tweet.new t.status = "RT #{@tweet.user.name}: #{@tweet.status}" t.original_tweet = @tweet current_user has many tweets.... t.user = current_user t.save current_user "RT #{@tweet.user.name}: #{@tweet.status}"status original_tweet @tweet : : => ) ,=> .tweets.create( 17
  • 18. LEVEL 1 controllers In Space fantastic filters /app/controllers/tweets_controller.rb class TweetsController < ApplicationController end def edit def update def destroy @tweet = Tweet.find(params[:id]) end @tweet = Tweet.find(params[:id]) @tweet = Tweet.find(params[:id]) end end ... ... ... 18
  • 19. LEVEL 1 controllers In Space /app/controllers/tweets_controller.rb class TweetsController < ApplicationController end def edit def update def destroy @tweet = Tweet.find(params[:id]) end @tweet = Tweet.find(params[:id])@tweet = Tweet.find(params[:id]) end end ... ... ... def get_tweet end before_filter :get_tweet, :only => [:edit, :update, :destroy] fantastic filters 19
  • 20. LEVEL 1 controllers In Space @tweet = Tweet.find(params[:id]) /app/controllers/tweets_controller.rb class TweetsController < ApplicationController end def edit def update def destroy end end end ... ... ... def get_tweet end before_filter :get_tweet, :only => [:edit, :update, :destroy] private Why are you hiding instance variables? Tweet.find(params[:id])@tweet =@tweet =@tweet = get_tweetget_tweetget_tweet fantastic filters 20
  • 21. @tweet = LEVEL 1 controllers In Space class TweetsController < ApplicationController end def edit def update def destroy end end end def get_tweet end private get_tweet @tweet = get_tweet @tweet = get_tweet Keeping  parameters  in  ac.ons params[:id])( params[:id])( params[:id])( (tweet_id) fantastic filters tweet_id)Tweet.find( 21
  • 22. LEVEL 1 controllers In Space What should they be used for? authorization fantastic filters Logging wizards 22
  • 23. LEVEL 1 controllers In Space fantastic filters /app/controllers/tweets_controller.rb class TweetsController < ApplicationController before_filter :auth, :only => [:edit, :update, :destroy] :except => [:index, :create] class ApplicationController < ActionController::Base before_filter :require_login class SessionsController < ApplicationController skip_before_filter :require_login, :only => [:new, :create] But what about the login page itself? Global  Filters 23
  • 24. level 2 controller command 24
  • 25. LEVEL 1 CONTROLLING YOUR CONTROLLERS Example 25
  • 26. /app/models/user.rb class User < ActiveRecord::Base has_one :account_setting, :dependent => :destroy end <div class="field"> <%= a.label :public_email %><br /> <%= a.check_box :public_email %> </div> <div class="field"> <%= a.label :show_media %><br /> <%= a.check_box :show_media %> </div> <div class="field"> <%= a.label :protect_tweets %><br /> <%= a.check_box :protect_tweets %> </div> <% end %> <%= fields_for :account_setting do |a| %> Nested attributes /app/views/users/edit.html.erb LEVEL 2 controller command 26
  • 27. Nested attributes /app/controllers/users_controller.rb @account_setting.save @user = User.new(params[:user]) @account_setting = AccountSetting.new(params[:account_setting]) class UsersController < ApplicationController def create if @user.save @account_setting.user = @user redirect_to(@user, :notice => 'User was successfully created.') else render :action => "new" end end end LEVEL 2 controller command 27
  • 28. Nested attributes /app/controllers/users_controller.rb @user = User.new(params[:user]) class UsersController < ApplicationController def create if @user.save redirect_to(@user, :notice => 'User was successfully created.') else render :action => "new" end end end using  Nested  A8ributes LEVEL 2 controller command 28
  • 29. /app/models/user.rb Nested attributes /app/views/users/edit.html.erb end class UsersController < ApplicationController def new @user = User.new end end /app/controllers/users_controller.rb (:account_setting => AccountSetting.new) accepts_nested_attributes_for :account_setting <%= ... f. class User < ActiveRecord::Base has_one :account_setting, :dependent => :destroy fields_for :account_setting do |a| %> <%= form_for(@user) do |f| %> LEVEL 2 controller command 29
  • 30. Models without the database LEVEL 2 controller command 30
  • 31. /app/views/contact_us/new.html.erb Models without the database <h1>Contact Us</h1> <%= form_for :contact, :url => send_email_path do |f| %> <div class="field"> <%= f.label :name %><br /> <%= f.text_field :name %> </div> <div class="field"> <%= f.label :email %><br /> <%= f.text_field :email %> </div> <div class="field"> <%= f.label :body %><br /> <%= f.text_area :body %> </div> <div class="actions"> <%= f.submit %> </div> <% end %> LEVEL 2 controller command 31
  • 32. /app/controllers/contact_us_controller.rb Models without the database class ContactUsController < ApplicationController def new end def send_email ).deliver flash[:notice] = "Email sent, we'll get back to you" redirect_to root_path end end end name, email, body name.blank? || email.blank? || body.blank? flash.now[:notice] = "Please fill out all fields" if name = params[:contact][:name] email = params[:contact][:email] body = params[:contact][:body] render :action => 'new' else Notifications.contact_us( LEVEL 2 controller command 32
  • 33. @contact_form.valid?! Models without the database class ContactUsController < ApplicationController def new end def send_email ).deliver end end end if @contact_form = ContactForm.new(params[:contact_form]) @contact_form @contact_form = ContactForm.new /app/controllers/contact_us_controller.rb render :action => 'new' else Notifications.contact_us( flash[:notice] = "Email sent, we'll get back to you" redirect_to root_path LEVEL 2 controller command 33
  • 34. @contact_form.valid? Models without the database class ContactUsController < ApplicationController def new end def send_email ).deliver end end end if @contact_form = ContactForm.new(params[:contact_form]) @contact_form @contact_form = ContactForm.new /app/controllers/contact_us_controller.rb render :action => 'new' else Notifications.contact_us( flash[:notice] = "Email sent, we'll get back to you" redirect_to root_path Use the positive inflection LEVEL 2 controller command 34
  • 35. @contact_form.valid? Models without the database class ContactUsController < ApplicationController def new end def send_email ).deliver end end end if @contact_form = ContactForm.new(params[:contact_form]) @contact_form @contact_form = ContactForm.new /app/controllers/contact_us_controller.rb render :action => 'new' else Notifications.contact_us( use the redirect notice syntax :notice "Email sent, we'll get back to you"redirect_to root_path, => LEVEL 2 controller command 35
  • 36. render @contact_form.valid? Models without the database class ContactUsController < ApplicationController def new end def send_email ).deliver end end end if @contact_form = ContactForm.new(params[:contact_form]) @contact_form @contact_form = ContactForm.new /app/controllers/contact_us_controller.rb else Notifications.contact_us( shorten the render :notice "Email sent, we'll get back to you"redirect_to root_path, => new: LEVEL 2 controller command 36
  • 37. /app/views/contact_us/new.html.erb Models without the database <%= form_for , :url => send_email_path do |f| %> <h1>Contact Us</h1> @contact_form LEVEL 2 controller command 37
  • 38. /app/models/contact_form.rb Models without the database class ContactForm attr_accessor :name, :email, :body end validates_presence_of :name, :email, :body include ActiveModel::Validations include ActiveModel::Conversion def initialize(attributes = {}) attributes.each do |name, value| send("#{name}=", value) end end def persisted? false end <%= form_for @contact_form ContactForm.new(params[:contact_form]) LEVEL 2 controller command 38
  • 39. Models without the database LEVEL 2 controller command 39
  • 40. really Rest class UsersController def < ApplicationController subscribe_mailing_list current_user.subscribe(params[:id]) redirect_to current_user, :notice => "You've been subscribed" end def unsubscribe_mailing_list current_user.unsubscribe(params[:id]) redirect_to current_user, :notice => "You have been unsubscribed" end end /app/controllers/users_controller.rb LEVEL 2 controller command 40
  • 41. /app/controllers/subscrip5ons_controller.rb really Rest class SubscriptionsController def < ApplicationController create current_user.subscribe(params[:id]) redirect_to current_user, :notice => "You've been subscribed" end def destroy current_user.unsubscribe(params[:id]) redirect_to current_user, :notice => "You have been unsubscribed" end end LEVEL 2 controller command 41
  • 42. really Rest Use your best judgement More than 2 levels is bad /users/1/posts/2/comments/3 Not using REST is okay /config/routes.rb get "contact_us/new" post "contact_us/send_email", :as => "send_email" LEVEL 2 controller command 42
  • 43. Enter the Presenters LEVEL 2 controller command 43
  • 44. Enter the Presenters /app/controllers/tweets_controller.rb @followers_tweets = current_user.followers_tweets.limit(20) @recent_tweet = current_user.tweets.first @following = current_user.following.limit(5) @followers = current_user.followers.limit(5) @recent_favorite = current_user.favorite_tweets.first @recent_listed = current_user.recently_listed.limit(5) if current_user.trend_option == "worldwide" @trends = Trend.worldwide.by_promoted.limit(10) else @trends = Trend.filter_by(current_user.trend_option).limit(10) end .... def index end LEVEL 2 controller command 44
  • 45. Enter the Presenters /app/controllers/tweets_controller.rb def index end @presenter = Tweets::IndexPresenter.new(current_user) /config/applica5on.rb config.autoload_paths += [config.root.join("app/presenters")] /app/presenters/tweets/index_presenter.rb def initialize(user) @user = user end class Tweets::IndexPresenter LEVEL 2 controller command 45
  • 46. Enter the Presenters /app/presenters/tweets/index_presenter.rb def initialize(user) @user = user end class Tweets::IndexPresenter def index end ... Old  Controller @followers_tweets = current_user.followers_tweets.limit(20) @recent_tweet = current_user.tweets.first if .trend_option == "worldwide"current_user .trend_option).limit(10) end current_user @trends = Trend.worldwide.by_promoted.limit(10) else @trends = Trend.filter_by( LEVEL 2 controller command 46
  • 47. Enter the Presenters /app/presenters/tweets/index_presenter.rb def initialize(user) @user = user end class Tweets::IndexPresenter end .followers_tweets.limit(20) .tweets.first if .trend_option == "worldwide" .trend_option).limit(10) end def followers_tweets @user. end Trend.worldwide.by_promoted.limit(10) else Trend.filter_by(@user. @user. def recent_tweet @user end def trends LEVEL 2 controller command 47
  • 48. Enter the Presenters /app/presenters/tweets/index_presenter.rb def initialize(user) @user = user end class Tweets::IndexPresenter /app/views/tweets/index.html.erb <%= @presenter.recent_tweet.created_at %> <%= @presenter.recent_tweet.body %> /app/controllers/tweets_controller.rb def index end @presenter = Tweets::IndexPresenter.new(current_user) .tweets.first def recent_tweet end Two objects! @user LEVEL 2 controller command 48
  • 49. Enter the Presenters /app/presenters/tweets/index_presenter.rb def initialize(user) @user = user end class Tweets::IndexPresenter @recent_tweet ||= .tweets.first def recent_tweet @user /app/views/tweets/index.html.erb <%= @presenter.recent_tweet.created_at %> <%= @presenter.recent_tweet.body %> /app/controllers/tweets_controller.rb def index @presenter = Tweets::IndexPresenter.new(current_user) end end Memoized One object! LEVEL 2 controller command 49
  • 50. Enter the Presenters /app/presenters/tweets/index_presenter.rb def initialize(user) @user = user end class Tweets::IndexPresenter .tweets.first def recent_tweet @user /app/views/tweets/index.html.erb <%= @presenter.recent_tweet.created_at %> <%= @presenter.recent_tweet.body %> /app/controllers/tweets_controller.rb def index @presenter = Tweets::IndexPresenter.new(current_user) end end extend ActiveSupport::Memoizable memoize :recent_tweet, :followers_tweet, ... LEVEL 2 controller command 50
  • 51. Memoization one is better than the other value is not stored if false or nil is returned ||= extend ActiveSupport::Memoizable memoize :recent_tweet, :followers_tweet, ... def expensive(num) # lots of processing end memoize :expensive expensive(2) expensive(4) expensive(2) expensive(4) loaded from cache LEVEL 2 controller command 51
  • 52. reject sql injection User.where("name = #{params[:name]}") User.where("name = ?", params[:name]) Tweet.where("created_at >= :start_date AND created_at <= :end_date", {:start_date => params[:start_date], :end_date => params[:end_date]}) Tweet.where(:created_at => (params[:start_date].to_date)..(params[:end_date].to_date)) User.where(:name => params[:name]) LEVEL 2 controller command 52
  • 53. /app/controllers/users_controller.rb Rails 3 responder syntax do |format| format.html # show.html.erb format.xml { render :xml => @user } end respond_to do |format| format.html format.xml { render :xml => @users.to_xml } end respond_to def index @users = User.all end ... end def show @user = User.find(params[:id]) class UsersController < ApplicationController LEVEL 2 controller command 53
  • 54. /app/controllers/users_controller.rb Rails 3 responder syntax respond_to def index @users = User.all end ... end def show @user = User.find(params[:id]) class UsersController < ApplicationController :html, :xml, :json respond_with(@users) respond_to respond_with(@user) LEVEL 2 controller command 54
  • 55. level 3 Model Mayhem 55
  • 56. LEVEL 3 Model mayhem Loving your indices current_user.tweets class AddIndexesToTables < ActiveRecord::Migration def self.up add_index :tweets, :user_id end def self.down remove_index :tweets, :user_id end end 56
  • 57. LEVEL 3 Model mayhem current_user.tweets.order('created_at desc').limit(10) Topic.where("started_trending > ?", 1.day.ago).order('mentions desc').limit(5) class AddIndexesToTables < ActiveRecord::Migration def self.up add_index :tweets, [:user_id, :created_at] end def self.down end end remove_index :tweets, [:user_id, :created_at] remove_index :topics, [:started_trending, :mentions] add_index :topics, [:started_trending, :mentions] If these queries are run a great deal Loving your indices 57
  • 58. Use your best judgement More indices, more time it takes to reindex If a 2 second query runs 5 times a week, LEVEL 3 Model mayhem who cares? Loving your indices 58
  • 59. LEVEL 3 Model mayhem 59
  • 60. LEVEL 3 Model mayhem 60
  • 61. LEVEL 3 Model mayhem $ curl -d "user[login]=hacked&user[is_admin]=true&user[password] =password&user[password_confirmation]=password&user[email]=hacked@by.me" http://url_not_shown/users user[is_admin]=true 61
  • 62. protecting your attributes /app/models/user.rb class User < ActiveRecord::Base attr_protected :is_admin end /app/models/user.rb class User < ActiveRecord::Base attr_accessible :email, :password, :password_confirmation end whitelists are better for security LEVEL 3 Model mayhem 62
  • 63. default values /app/models/account_se7ng.rb before_create :set_default_timezone def set_default_timezone self.time_zone = "EST" end end class AccountSetting < ActiveRecord::Base belongs_to :user LEVEL 3 Model mayhem 63
  • 64. default values /app/models/account_se7ng.rb end class AccountSetting < ActiveRecord::Base belongs_to :user class AddDefaultTimeZoneToAccountSettings < ActiveRecord::Migration def self.up change_column_default :account_settings, :time_zone, 'EST' end def self.down change_column :account_settings, :time_zone, :string, nil end end /db/migrate/20110119150620_add_default_5me_zone_account_se7ngs.rb LEVEL 3 Model mayhem 64
  • 65. Proper use of callbacks /app/models/topic.rb LEVEL 3 Model mayhem Time.now + (60 * 60 * 24 * 7)self.finish_trending = end end class Topic < ActiveRecord::Base before_create :set_trend_ending private def set_trend_ending 65
  • 66. Proper use of callbacks /app/models/topic.rb LEVEL 3 Model mayhem 1.weekself.finish_trending = end end .from_now class Topic < ActiveRecord::Base before_create :set_trend_ending private def set_trend_ending 66
  • 67. Proper use of callbacks /app/models/topic.rb before_create :set_trend_ending private def set_trend_ending LEVEL 3 Model mayhem 1.week self.finish_trending = end end .from_now class Topic < ActiveRecord::Base TRENDING_PERIOD = TRENDING_PERIOD 67
  • 68. Proper use of callbacks LEVEL 3 Model mayhem Crea;ng  an  object before_validation after_validation before_save after_save before_create around_create after_create Upda;ng  an  object Dele;ng  an  object before_validation after_validation before_save after_save before_update around_update after_update before_destroy after_destroy around_destroy 68
  • 69. Rails date helpers LEVEL 3 Model mayhem Date  Helpers 1.minute 2.hour 3.days 4.week 5.months 6.year Modifiers More  Modifiers beginning_of_day beginning_of_week beginning_of_month beginning_of_quarter beginning_of_year 2.weeks.ago 3.weeks.from_now next_week next_month next_year * singular or plural * end can be used * prev can be used 69
  • 70. Proper use of callbacks LEVEL 3 Model mayhem 70
  • 71. Proper use of callbacks LEVEL 3 Model mayhem /app/models/following.rb class Following < ActiveRecord::Base after_create :send_follower_notification def send_follower_notification if queue_new_follower_email end end end self.followed_user.receive_emails? 71
  • 72. Proper use of callbacks LEVEL 3 Model mayhem /app/models/following.rb class Following < ActiveRecord::Base after_create : self.followed_user.receive_emails? def followed_can_receive_emails? queue_new_follower_email end end :if => :followed_can_receive_emails? queue_new_follower_email, 72
  • 73. Proper use of callbacks LEVEL 3 Model mayhem /app/models/following.rb class Following < ActiveRecord::Base after_create :queue_new_follower_email end :if => , Proc.new {|f| .followed_user.receive_emails?f } 73
  • 74. improved validation LEVEL 3 Model mayhem .errors.add(:name, 'is inappropriate') selfunless ContentModerator.is_suitable?( .name) self end /app/models/topic.rb end end def validate class Topic < ActiveRecord::Base 74
  • 75. improved validation LEVEL 3 Model mayhem .errors.add(:name, 'is inappropriate') /app/models/topic.rb end end class Topic < ActiveRecord::Base validate :appropriate_content private def appropriate_content selfunless ContentModerator.is_suitable?( .name) self end 75
  • 76. improved validation LEVEL 3 Model mayhem /app/models/topic.rb end end class Topic < ActiveRecord::Base validates :name, :appropriate => true end /lib/appropriate_validator.rb class AppropriateValidator < ActiveRecord::EachValidator def validate_each(record, attribute, value) .errors.add( , 'is inappropriate') unless ContentModerator.is_suitable?(value) end record attribute Don’t forget to require this /lib  isn’t  auto-­‐loaded  by  default 76
  • 77. Sowing the Seeds LEVEL 3 Model mayhem /db/migrate/20110114221048_create_topics.rb class CreateTopics < ActiveRecord::Migration def self.up create_table :topics do |t| t.string :name t.datetime :started_trending t.integer :mentions t.timestamps end end def self.down drop_table :topics end end Topic.create( Topic.create( Topic.create( ) ):name => "Ruby5", :mentions => 2312 :name => "Top Ruby Jobs", :mentions => 231 :name => "Rails for Zombies", :mentions => 1023) 77
  • 78. Sowing the Seeds LEVEL 3 Model mayhem /db/seeds.rb $ rake db:seed Run  from  command  line mentions Won’t be set! class Topic < ActiveRecord::Base attr_protected :mentions end /app/models/topic.rb Topic.create( Topic.create( Topic.create( ) ) ):name => "Ruby5", :mentions => 2312 :name => "Top Ruby Jobs", :mentions => 231 :name => "Rails for Zombies", :mentions => 1023 78
  • 79. Sowing the Seeds LEVEL 3 Model mayhem /db/seeds.rb :name => "Ruby5", :mentions => 2312 :name => "Top Ruby Jobs", :mentions => 231 :name => "Rails for Zombies", :mentions => 1023 topics.each do |attributes| Topic.create do |t| t.name = attributes[:name] topics = [ { }, { }, { } ] What if we want to be able to update the seed? t.mentions = attributes[:mentions] end end 79
  • 80. Sowing the Seeds LEVEL 3 Model mayhem /db/seeds.rb :name => "Ruby5", :mentions => 2312 :name => "Top Ruby Jobs", :mentions => 231 :name => "Rails for Zombies", :mentions => 1023 topics.each do |attributes| Topic.create do |t| t.name = attributes[:name] topics = [ { }, { }, { } ] Topic.destroy_all Dangerous if there are lots of relationships t.mentions = attributes[:mentions] end end 80
  • 81. Sowing the Seeds LEVEL 3 Model mayhem /db/seeds.rb :name => "Ruby5", :mentions => 2312 :name => "Top Ruby Jobs", :mentions => 231 :name => "Rails for Zombies", :mentions => 1023 topics.each do |attributes| Topic. attributes[:name] topics = [ { }, { }, { } ] find_or_initialize_by_name( ).tap do |t| t.mentions = attributes[:mentions] end end t.save! 81
  • 82. Level 4Model Bert 82
  • 83. N+1 is not for fun /app/models/user.rb class User def recent_followers self.followers.recent.collect{ |f| f.user.name }.to_sentence end end => "Gregg, Eric, Dray, and Nate" Select followers where user_id=1 Select user where id=2 Select user where id=3 Select user where id=4 Select user where id=5 LEVEL 4 Model Bert 83
  • 84. N+1 is not for fun /app/models/user.rb class User def recent_followers self.followers.recent .collect{ |f| f.user.name }.to_sentence end end .includes(:user) Select followers where user_id=1 Select users where user_id in (2,3,4,5) 2 queries instead of 5! h"ps://github.com/flyerhzm/bullet Bullet gem To find all your n+1 queries LEVEL 4 Model Bert 84
  • 85. counter_cache Money /app/views/tweets/index.html.erb <% @tweets.each do |tweet| %> <div class="tweet"> <%= tweet.status %> <span class="retweets"> <%= tweet.retweets.length ReTweets%> </span> </div> <% end %> 2 ReTweets 1 ReTweets 0 ReTweets Bad English LEVEL 4 Model Bert 85
  • 86. counter_cache Money /app/views/tweets/index.html.erb <% @tweets.each do |tweet| %> <div class="tweet"> <%= tweet.status %> <span class="retweets"> <%= tweet.retweets.length %> </span> </div> <% end %> pluralize( , "ReTweet") 2 ReTweets 1 ReTweet 0 ReTweets LEVEL 4 Model Bert 86
  • 87. counter_cache Money /app/views/tweets/index.html.erb <% @tweets.each do |tweet| %> <div class="tweet"> <%= tweet.status %> <span class="retweets"> <%= tweet.retweets.length %> </span> </div> <% end %> , "ReTweet") 1. Select all retweets where user_id=X 2. Populate an array of tweet objects 3. Call length on that array For Each tweet Lots of unneeded objects pluralize( LEVEL 4 Model Bert 87
  • 88. counter_cache Money /app/views/tweets/index.html.erb <% @tweets.each do |tweet| %> <div class="tweet"> <%= tweet.status %> <span class="retweets"> <%= tweet.retweets.count %> </span> </div> <% end %> pluralize( , "ReTweet") 1. Select all retweets where user_id=X 2. do a count query for retweets For Each tweet possibly 10+ count queries LEVEL 4 Model Bert 88
  • 89. counter_cache Money /app/views/tweets/index.html.erb <% @tweets.each do |tweet| %> <div class="tweet"> <%= tweet.status %> <span class="retweets"> <%= tweet.retweets.size %> </span> </div> <% end %> pluralize( , "ReTweet") 1. Select all retweets where user_id=X For Each tweet There is no step 2 with  counter_cache 89
  • 90. counter_cache Money Tweet Tweet retweets original_tweet has_many belongs_to original retweet /app/models/tweet.rb has_many :retweets, :class_name => 'Tweet', :foreign_key => :tweet_id end class Tweet < ActiveRecord::Base belongs_to :original_tweet, :class_name => 'Tweet', :foreign_key => :tweet_id class AddCountRetweets def self.up add_column :tweets, :retweets_count, :integer, :default => 0 end ... Our  migra;on retweets_count LEVEL 4 Model Bert 90
  • 91. counter_cache Money true:counter_cache => /app/models/tweet.rb has_many :retweets, :class_name => 'Tweet', :foreign_key => :tweet_id end class Tweet < ActiveRecord::Base belongs_to :original_tweet, :class_name => 'Tweet', :foreign_key => :tweet_id retweets_count , unable to find tweets_count LEVEL 4 Model Bert 91
  • 92. counter_cache Money :counter_cache => /app/models/tweet.rb has_many :retweets, :class_name => 'Tweet', :foreign_key => :tweet_id end class Tweet < ActiveRecord::Base belongs_to :original_tweet, :class_name => 'Tweet', :foreign_key => :tweet_id, retweets_count: current_user.tweets.create( :status => "RT #{self.user.name}: #{self.status}", :original_tweet => self ) UPDATE "tweets" SET "retweets_count" = "retweets_count" + 1 WHERE ("tweets"."id" = 42) will cause an insert AND LEVEL 4 Model Bert 92
  • 93. counter_cache Money t.retweets.length pull  all  records then  calls  .length pull  all  records then  calls  .length t.retweets.count count  query count  query t.retweets.size count  query no  query look  at  cache Without Cache Counter With Cache Counter LEVEL 4 Model Bert 93
  • 94. Batches of find_each /lib/tasks/long_running_task.rake Not so good if you have millions of tweets desc 'Task involving all tweets' task :tweet_task => :environment do Tweet.all.each do |tweet| p "task for #{tweet}" end end LEVEL 4 Model Bert 94
  • 95. Batches of find_each /lib/tasks/long_running_task.rake desc 'Task involving all tweets' task :tweet_task => :environment do Tweet each do |tweet| p "task for #{tweet}" end end .find_ pulls batches of 1,000 at a time LEVEL 4 Model Bert 95
  • 96. Batches of find_each /lib/tasks/long_running_task.rake desc 'Task involving all tweets' task :tweet_task => :environment do Tweet each do |tweet| p "task for #{tweet}" end end .find_ (:batch_size => 200) pulls batches of 200 at a time LEVEL 4 Model Bert 96
  • 97. Law of Demeter Each unit should have limited knowledge about other units “don’t talk to strangers” tweet user account settings X LEVEL 4 Model Bert 97
  • 98. Law of Demeter /app/models/tweet.rb The tweet shouldn’t know about account_setting! class Tweet < ActiveRecord::Base def location_data if self.user.account_setting.location_on_tweets self.location else "unavailable" end end end LEVEL 4 Model Bert 98
  • 99. Law of Demeter /app/models/tweet.rb class Tweet < ActiveRecord::Base def location_data if self.user.location_on_tweets self.location else "unavailable" end end end class User < ActiveRecord::Base has_one :account_setting, :dependent => :destroy end delegate :location_on_tweets, /app/models/user.rb :to => :account_setting :public_email, Additional Methods LEVEL 4 Model Bert 99
  • 100. Law of Demeter class User < ActiveRecord::Base has_one :account_setting, :dependent => :destroy end delegate :location_on_tweets, /app/models/user.rb :to => :account_setting :public_email, tweet user account settings X self.user.location_on_tweets ERROR!! account_setting is nil! LEVEL 4 Model Bert 100
  • 101. Law of Demeter class User < ActiveRecord::Base has_one :account_setting, :dependent => :destroy end delegate :location_on_tweets, /app/models/user.rb :to => :account_setting :public_email tweet user account settings X self.user.location_on_tweets :allow_nil => true , , Returns nil when Account_Settings is missing LEVEL 4 Model Bert 101
  • 102. Head to to_s /app/models/user.rb <%= @user.display_name %> class User < ActiveRecord::Base def display_name "#{first_name} #{last_name}" end end LEVEL 4 Model Bert 102
  • 103. Head to to_s /app/models/user.rb <%= @user %> class User < ActiveRecord::Base def to_s "#{first_name} #{last_name}" end end LEVEL 4 Model Bert 103
  • 104. to_param-alama ding dong /app/models/topic.rb /post/2133 class Topic < ActiveRecord::Base def to_param "#{id}-#{name.parameterize}" end end /post/rails-best-practices SEO Friendly URLS /post/2133-rails-best-practices LEVEL 4 Model Bert 104
  • 105. to_param-alama ding dong /app/models/topic.rb class Topic < ActiveRecord::Base def to_param "#{id}-#{name.parameterize}" end end /post/2133-rails-best-practices <%= link_to topic.name, topic %> Will  generate Topic.find(params[:id]) {:id => "2133-rails-best-practices"} Will  call  to_i Topic.find(2133) LEVEL 4 Model Bert 105
  • 106. Level 5 Froggy Views 106
  • 107. The example LEVEL 5 Froggy Views 107
  • 108. LEVEL 5 Froggy Views No queries in your view! /app/views/tweets/index.html.erb current_user.who_to_follow.limit(5)<% <li><%= f.name %> - <%= link_to "Follow", follow_user_path(f) %></li> <% end %> .each do |f| %> Query shouldn’t be in our view! 108
  • 109. LEVEL 5 Froggy Views No queries in your view! /app/views/tweets/index.html.erb <% <li><%= f.name %> - <%= link_to "Follow", follow_user_path(f) %></li> <% end %> .each do |f| %> /app/controllers/tweets_controller.rb class TweetsController < ApplicationController def index @who_to_follow = end end current_user.who_to_follow.limit(5) @who_to_follow 109
  • 110. LEVEL 5 Froggy Views Helper Skelter /app/views/tweets/index.html.erb @followers_count %></span> <% @recent_followers.each do |f| %> <a href="<%= user_path(f) %>"> <img src="<%= f.avatar.url(:thumb) %>" /> </a> <% end %> </div> <div class="following"> <div class="followers"> Followers <span><%= Following <span><%= @following_count %></span> <% @recent_following.each do |f| %> <a href="<%= user_path(f) %>"> <img src="<%= f.avatar.url(:thumb) %>" /> </a> <% end %> </div> 110
  • 111. LEVEL 5 Froggy Views /app/views/tweets/index.html.erb @followers_count @recent_followers @following_count @recent_following <%= follow_box( <%= follow_box( "Followers", , ) %> "Following", , ) %> Followers Following /app/helpers/tweets_helper.rb def follow_box(title, count, recent) end end str = "<div class="#{title.downcase ">" + "#{title}<span>#{count}</span>" recent.each do |user| str += "<a href="#{user_path(user)}">" str += "<img src="#{user.avatar.url(:thumb)}">" str += "</a>" Use proper link_to and image_tag += "</div>")raw(str } Helper Skelter 111
  • 112. LEVEL 5 Froggy Views /app/views/tweets/index.html.erb @followers_count @recent_followers @following_count @recent_following <%= follow_box( <%= follow_box( "Followers", , ) %> "Following", , ) %> def follow_box(title, count, recent) end end str = "<div class="#{title.downcase ">" + "#{title}<span>#{count}</span>" recent.each do |user| str += user.avatar.url(:thumb) link_to user do image_tag( end ) Use html helpers? += "</div>")raw(str /app/helpers/tweets_helper.rb Helper Skelter 112
  • 113. LEVEL 5 Froggy Views /app/views/tweets/index.html.erb @followers_count @recent_followers @following_count @recent_following <%= follow_box( <%= follow_box( "Followers", , ) %> "Following", , ) %> def follow_box(title, count, recent) end end title.downcase title count recent.each do |user| str += user.avatar.url(:thumb) link_to user do image_tag( end ) ) content_tag :div, :class => do str = ) end raw(str + content_tag(:span, Annoying str variable /app/helpers/tweets_helper.rb Helper Skelter 113
  • 114. LEVEL 5 Froggy Views /app/views/tweets/index.html.erb @followers_count @recent_followers @following_count @recent_following <%= follow_box( <%= follow_box( "Followers", , ) %> "Following", , ) %> def follow_box(title, count, recent) end end title.downcase title count recent. user.avatar.url(:thumb) link_to user do image_tag( end ) ) content_tag :div, :class => do ) end raw( + content_tag(:span, + collect do |user| .join /app/helpers/tweets_helper.rb Helper Skelter 114
  • 115. The Example LEVEL 5 Froggy Views 115
  • 116. LEVEL 5 Froggy Views Partial sanity /app/views/tweets/index.html.erb /app/views/tweets/_trending.html.erb <h3><%= @user.trending_area %></h3> <ul> <% @trending.each do |topic| %> <li> <%= link_to topic.name, topic %> <% if topic.promoted? %> <%= link_to image_tag('promoted.jpg'), topic %> <% end %> </li> <% end %> </ul> <%= render :partial => 'trending' %> <h2>Trends</h2> 116
  • 117. LEVEL 5 Froggy Views Partial sanity /app/views/tweets/index.html.erb /app/views/tweets/_trending.html.erb <h3><%= @user.trending_area %></h3> <ul> <% @trending.each do |topic| %> <li> <%= link_to topic.name, topic %> <% if topic.promoted? %> <%= link_to image_tag('promoted.jpg'), topic %> <% end %> </li> <% end %> </ul> <%= render 'trending' %> <h2>Trends</h2> There are instance variables in our partial! 117
  • 118. LEVEL 5 Froggy Views Partial sanity /app/views/tweets/index.html.erb /app/views/tweets/_trending.html.erb <h3><%= @user.trending_area %></h3> <ul> <% @trending .each do |topic| %> <li> <%= link_to topic.name, topic %> <% if topic.promoted? %> <%= link_to image_tag('promoted.jpg'), topic %> <% end %> </li> <% end %> </ul> <%= render 'trending' %> <h2>Trends</h2> , :area => , :topics => area topics 118
  • 119. LEVEL 5 Froggy Views Partial sanity /app/views/tweets/_trending.html.erb <h3><%= %></h3> <ul> <% .each do |topic| %> <li> <%= link_to topic.name, topic %> <% if topic.promoted? %> <%= link_to image_tag('promoted.jpg'), topic %> <% end %> </li> <% end %> </ul> area topics /app/views/topics/_topic.html.erb <%= render topic %>'topics/topic', :topic => 119
  • 120. LEVEL 5 Froggy Views Partial sanity /app/views/tweets/_trending.html.erb <h3><%= %></h3> <ul> <% .each do |topic| %> <li> <%= link_to topic.name, topic %> <% if topic.promoted? %> <%= link_to image_tag('promoted.jpg'), topic %> <% end %> </li> <% end %> </ul> area topics /app/views/topics/_topic.html.erb <%= render topic %> <ul> Using Class name to find partial 120
  • 121. LEVEL 5 Froggy Views Partial sanity /app/views/tweets/_trending.html.erb <h3><%= %></h3> <li> <%= link_to topic.name, topic %> <% if topic.promoted? %> <%= link_to image_tag('promoted.jpg'), topic %> <% end %> </li> </ul> area topics /app/views/topics/_topic.html.erb <%= render <ul> %>:partial => 'topics/topic', :collection => 121
  • 122. LEVEL 5 Froggy Views Partial sanity /app/views/tweets/_trending.html.erb <h3><%= %></h3> <li> <%= link_to topic.name, topic %> <% if topic.promoted? %> <%= link_to image_tag('promoted.jpg'), topic %> <% end %> </li> </ul> area topics /app/views/topics/_topic.html.erb <%= render <ul> %> 122
  • 123. LEVEL 5 Froggy Views empty string things <% if @user.email.blank? %> <% unless @user.email? %> <% if @user.email.present? %> <% if @user.email? %> 123
  • 124. LEVEL 5 Froggy Views empty string things <%= @user.city || @user.state || "Unknown" %> If city is empty “” it will print “” city = @user.city if @user.city.present? state = @user.state if @user.state.present? => “” <%= city || state || "Unknown" %> 124
  • 125. LEVEL 5 Froggy Views empty string things <%= @user.city || @user.state || "Unknown" %> <%= @user.city.presence || @user.state.presence || "Unknown" %> 125
  • 126. LEVEL 5 Froggy Views empty string things city is nil undefined method `titleize' for nil:NilClass <%= @user.city.titleize %>|| "Unknown" 126
  • 127. LEVEL 5 Froggy Views empty string things <%= @user.city.titleize %> <% if @user.city %> <% else %> Unknown <% end %> <%= @user.city ? @user.city.titleize : "Unknown" %> <%= @user.city.try(:titleize) || "Unknown" %> 127
  • 128. The example LEVEL 5 Froggy Views 128
  • 129. LEVEL 5 Froggy Views rock your block helpers <% @presenter.tweets.each do |tweet| %> <% end %> <div id="tweet_<%= tweet.id %>" class="<%= 'favorite' if tweet.is_a_favorite?(current_user) %>"> <%= tweet.status %> </div> /app/views/tweets/index.html.erb 129
  • 130. LEVEL 5 Froggy Views rock your block helpers <% @presenter.tweets.each do |tweet| %> <% end %> <%= tweet.status %> /app/views/tweets/index.html.erb <%= tweet_div_for(tweet, current_user) do %> <% end %> /app/helpers/tweets_helper.rb def tweet_div_for(tweet, user, &block) klass = 'favorite' if tweet.is_a_favorite?(user) content_tag tweet klass do yield end end , :class => id="tweet_<%= tweet.id %>" 130
  • 131. LEVEL 5 Froggy Views Yield to the content_for /app/views/layouts/applica5on.html.erb <!DOCTYPE html> <html> <body> <h1>Twitter</h1> Need to insert content here! <% if flash[:notice] %> <span style="color: green"><%= flash[:notice] %></span> <% end %> <%= yield %> </body> </html> 131
  • 132. LEVEL 5 Froggy Views Yield to the content_for /app/views/layouts/applica5on.html.erb <!DOCTYPE html> <html> <body> <h1>Twitter</h1> <%= yield :sidebar %> /app/views/tweets/index.html.erb <% content_for(:sidebar) do %> ... html here ... <% end %> <% if flash[:notice] %> <span style="color: green"><%= flash[:notice] %></span> <% end %> <%= yield %> </body> </html> 132
  • 133. LEVEL 5 Froggy Views /app/controllers/tweets_controller.rb what if all actions in the tweet controller need the sidebar? class TweetsController < ApplicationController layout 'with_sidebar' end /app/views/layouts/with_sidebar.html.erb <% content_for(:sidebar) do %> ... html here ... <% end %> <%= render :file => 'layouts/application' %> /app/views/layouts/applica5on.html.erb <%= yield :sidebar %> <% if flash[:notice] %> <span style="color: green"><%= flash[:notice] %></span> <% end %> <%= yield %> 1 2 3 133
  • 134. LEVEL 5 Froggy Views The example 134
  • 135. LEVEL 5 Froggy Views meta Yield /app/views/layouts/applica5on.html.erb cluttering your controller & polluting with view concerns class TweetsController < ApplicationController def show @tweet = Tweet.find(params[:id]) @title = @tweet.user.name @description = @tweet.status @keywords = @tweet.hash_tags.join(",") end end /app/controllers/tweets_controller.rb @description || "The best way ..." %>"> <meta name ="keywords" content="<%= @keywords || "social,tweets ..." %>"> ... <!DOCTYPE html> <html> <head> <title>Twitter <%= %></title> <meta name="description" content="<%= @title 135
  • 136. LEVEL 5 Froggy Views meta Yield /app/views/layouts/applica5on.html.erb <% content_for(:title, @tweet.user.name) content_for(:description, @tweet.status) content_for(:keywords, @tweet.hash_tags.join(",")) %> /app/views/tweets/show.html.erb <meta name ="keywords" content="<%= ... <!DOCTYPE html> <html> <head> <title>Twitter <%= %></title> <meta name="description" content="<%= yield(:title) yield(:description) yield(:keywords) || "The best way ..." %>"> || "social,tweets ..." %>"> 136
  • 137. LEVEL 5 Froggy Views meta Yield <% title @tweet.user.name description @tweet.status keywords @tweet.hash_tags.join(",") %> /app/views/tweets/show.html.erb /app/helpers/applica5on_helper.rb def title(title) content_for(:title, title) end def description(description) content_for(:description, description) end def keywords(keywords) content_for(:keywords, keywords) end 137