Download - Rails best practices_slides
1
controllers In space
2
level 1
sad Code
Happy Code
controllers In space
3
LEVEL 1 controllers In Space
Example
4
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 endend
end
else
5
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 endend
end
else...
tweet.retweet_by(current_user)
/app/models/tweet.rb
class Tweet < ActiveRecord::Base def retweet_by(retweeter)
endend
retweeter.id).present?
if
6
@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
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.agoorder 'mentions desc'limit 5
where( ).( ).( )
orderlimit
current_userdef index @tweets =
8
LEVEL 1 controllers In Space
Scope it out/app/controllers/tweets_controller.rb
...end
( ).( )'created_at desc'order
limit 10
current_userdef index @tweets = .tweets.
Scope to the user
9
LEVEL 1 controllers In Space
Scope it out/app/controllers/tweets_controller.rb
...end
(
( )
'created_at desc'order
limit 10current_userdef index @tweets = .tweets.
/app/models/tweet.rb
...end
)
recent.
scopeclass Tweet < ActiveRecord::Base
:recent,
10
default_
LEVEL 1 controllers In Space
Scope it out/app/controllers/tweets_controller.rb
...end
(
( )
'created_at desc'
limit 10current_userdef index @tweets = .tweets.
/app/models/tweet.rb
...end
)scopeclass Tweet < ActiveRecord::Base
order
11
limit(5)
LEVEL 1 controllers In Space
Scope it out/app/controllers/tweets_controller.rb
...end
/app/models/topic.rb
...end
scopeclass Topic < ActiveRecord::Base
:trending,
def index@trending = Topic.
'started_trending > ?', 1.day.agoorder '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
limit(5)
LEVEL 1 controllers In Space
Scope it out/app/controllers/tweets_controller.rb
...end
/app/models/topic.rb
...end
scopeclass Topic < ActiveRecord::Base
:trending,
def index@trending = Topic.
'started_trending > ?', 1.day.agoorder 'mentions desc'where(
(
trending
)).lambda {
}
.
13
||num
LEVEL 1 controllers In Space
Scope it out/app/controllers/tweets_controller.rb
...end
/app/models/topic.rb
...end
scopeclass Topic < ActiveRecord::Base
:trending,
def index@trending = Topic.
'started_trending > ?', 1.day.agoorder 'mentions desc'where(
( )).lambda {
}.
limit(num)
(5)trending
@trending = Topic.trending(5)
@trending = Topic.trending
wrong number of args, 0 for 1
14
|num
LEVEL 1 controllers In Space
Scope it out/app/controllers/tweets_controller.rb
...end
/app/models/topic.rb
...end
scopeclass Topic < ActiveRecord::Base
:trending,
def index@trending = Topic.
'started_trending > ?', 1.day.agoorder 'mentions desc'where(
( )).lambda {
}.
limit(num)
(5)trending
@trending = Topic.trending(5)
@trending = Topic.trending
| = nil
Ruby 1.9 FTW!
15
default_
LEVEL 1 controllers In Space
Scope it out
('created_at desc'
/app/models/tweet.rb
...end
)scopeclass 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
LEVEL 1 controllers In Space
Scope it out/app/controllers/tweets_controller.rb
t = Tweet.newt.status = "RT #{@tweet.user.name}: #{@tweet.status}"t.original_tweet = @tweet
current_user has many tweets....
t.user = current_usert.save
current_user"RT #{@tweet.user.name}: #{@tweet.status}"status
original_tweet @tweet:: =>)
,=>.tweets.create(
17
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
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
LEVEL 1 controllers In Space
@tweet = Tweet.find(params[:id])
/app/controllers/tweets_controller.rb
class TweetsController < ApplicationController
end
def edit
def update
def destroyend
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
@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
LEVEL 1 controllers In Space
What should they be used for?
authorization
fantastic filters
Loggingwizards
22
LEVEL 1 controllers In Space
fantastic filters/app/controllers/tweets_controller.rb
class TweetsController < ApplicationControllerbefore_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
level 2controller command
24
LEVEL 1 CONTROLLING YOUR CONTROLLERS
Example
25
/app/models/user.rb
class User < ActiveRecord::Base has_one :account_setting, :dependent => :destroyend
<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 command26
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 command27
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 command28
/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 command29
Models without the database
LEVEL 2 controller command30
/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 command31
/app/controllers/contact_us_controller.rb
Models without the database
class ContactUsController < ApplicationController
def new end
def send_email
).deliverflash[:notice] = "Email sent, we'll get back to you"redirect_to root_path
end endend
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'elseNotifications.contact_us(
LEVEL 2 controller command32
@contact_form.valid?!
Models without the database
class ContactUsController < ApplicationController
def new
end
def send_email
).deliver
end endend
if
@contact_form = ContactForm.new(params[:contact_form])
@contact_form
@contact_form = ContactForm.new
/app/controllers/contact_us_controller.rb
render :action => 'new'elseNotifications.contact_us(flash[:notice] = "Email sent, we'll get back to you"redirect_to root_path
LEVEL 2 controller command33
@contact_form.valid?
Models without the database
class ContactUsController < ApplicationController
def new
end
def send_email
).deliver
end endend
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 command34
@contact_form.valid?
Models without the database
class ContactUsController < ApplicationController
def new
end
def send_email
).deliver
end endend
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 command35
render
@contact_form.valid?
Models without the database
class ContactUsController < ApplicationController
def new
end
def send_email
).deliver
end endend
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 command36
/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 command37
/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::Validationsinclude 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 command38
Models without the database
LEVEL 2 controller command39
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 command40
/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 command41
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 command42
Enter the Presenters
LEVEL 2 controller command43
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 command44
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 command45
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 command46
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)elseTrend.filter_by(@user.
@user.
def recent_tweet@user
end
def trends
LEVEL 2 controller command47
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.firstdef recent_tweet
end
Two objects!
@user
LEVEL 2 controller command48
Enter the Presenters/app/presenters/tweets/index_presenter.rb
def initialize(user) @user = user end
class Tweets::IndexPresenter
@recent_tweet ||= .tweets.firstdef 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 command49
Enter the Presenters/app/presenters/tweets/index_presenter.rb
def initialize(user) @user = user end
class Tweets::IndexPresenter
.tweets.firstdef 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 command50
Memoizationone is better than the other
value is not stored if false or nil is returned
||=
extend ActiveSupport::Memoizablememoize :recent_tweet, :followers_tweet, ...
def expensive(num) # lots of processingend
memoize :expensive
expensive(2)expensive(4)
expensive(2)expensive(4)
loaded from cache
LEVEL 2 controller command51
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 command52
/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 command53
/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 command54
level 3
Model Mayhem
55
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 endend
56
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
endend
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
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
LEVEL 3 Model mayhem59
LEVEL 3 Model mayhem60
LEVEL 3 Model mayhem
$ curl -d "user[login]=hacked&user[is_admin]=true&user[password]=password&user[password_confirmation]=password&user[email][email protected]" http://url_not_shown/users
user[is_admin]=true
61
protecting your attributes/app/models/user.rb
class User < ActiveRecord::Base attr_protected :is_adminend
/app/models/user.rb
class User < ActiveRecord::Base attr_accessible :email, :password, :password_confirmationend
whitelists are better for security
LEVEL 3 Model mayhem62
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 mayhem63
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 endend
/db/migrate/20110119150620_add_default_5me_zone_account_se7ngs.rb
LEVEL 3 Model mayhem64
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
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
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::BaseTRENDING_PERIOD =
TRENDING_PERIOD
67
Proper use of callbacks
LEVEL 3 Model mayhem
Crea;ng an object
before_validationafter_validationbefore_saveafter_savebefore_createaround_createafter_create
Upda;ng an object Dele;ng an object
before_validationafter_validationbefore_saveafter_savebefore_updatearound_updateafter_update
before_destroyafter_destroyaround_destroy
68
Rails date helpers
LEVEL 3 Model mayhem
Date Helpers
1.minute2.hour3.days4.week5.months6.year
Modifiers More Modifiers
beginning_of_daybeginning_of_weekbeginning_of_monthbeginning_of_quarterbeginning_of_year
2.weeks.ago3.weeks.from_now
next_weeknext_monthnext_year
* singular or plural
* end can be used
* prev can be used
69
Proper use of callbacks
LEVEL 3 Model mayhem70
Proper use of callbacks
LEVEL 3 Model mayhem
/app/models/following.rb
class Following < ActiveRecord::Base after_create :send_follower_notification
def send_follower_notificationifqueue_new_follower_email
endend
end
self.followed_user.receive_emails?
71
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
endend
:if => :followed_can_receive_emails?queue_new_follower_email,
72
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
improved validation
LEVEL 3 Model mayhem
.errors.add(:name, 'is inappropriate')selfunless ContentModerator.is_suitable?( .name)
selfend
/app/models/topic.rb
end
end
def validate
class Topic < ActiveRecord::Base
74
improved validation
LEVEL 3 Model mayhem
.errors.add(:name, 'is inappropriate')
/app/models/topic.rb
end
end
class Topic < ActiveRecord::Basevalidate :appropriate_content
private
def appropriate_contentselfunless ContentModerator.is_suitable?( .name)
selfend
75
improved validation
LEVEL 3 Model mayhem
/app/models/topic.rb
endend
class Topic < ActiveRecord::Basevalidates :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)
endrecord attribute
Don’t forget to require this/lib isn’t auto-‐loaded by default
76
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 endend
Topic.create(Topic.create(Topic.create(
)):name => "Ruby5", :mentions => 2312
:name => "Top Ruby Jobs", :mentions => 231:name => "Rails for Zombies", :mentions => 1023)
77
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 :mentionsend
/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
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] endend
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 = [{ },{ },{ }
]
Topic.destroy_all
Dangerous if there are lots of relationships
t.mentions = attributes[:mentions] endend
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. attributes[:name]
topics = [{ },{ },{ }
]
find_or_initialize_by_name( ).tap do |t| t.mentions = attributes[:mentions]
endend
t.save!
81
Level 4Model Bert
82
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 endend
=> "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 Bert83
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 endend
.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/bulletBullet gem
To find all your n+1 queries
LEVEL 4 Model Bert84
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 ReTweets1 ReTweets0 ReTweets
Bad English
LEVEL 4 Model Bert85
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 ReTweets1 ReTweet0 ReTweets
LEVEL 4 Model Bert86
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=X2. Populate an array of tweet objects3. Call length on that array
For Each tweetLots of unneeded objects
pluralize(
LEVEL 4 Model Bert87
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=X2. do a count query for retweets
For Each tweet
possibly 10+ count queriesLEVEL 4 Model Bert
88
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=XFor Each tweet
There is no step 2
with counter_cache
89
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_idend
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 Bert90
counter_cache Money
true:counter_cache =>
/app/models/tweet.rb
has_many :retweets, :class_name => 'Tweet', :foreign_key => :tweet_idend
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 Bert91
counter_cache Money
:counter_cache =>
/app/models/tweet.rb
has_many :retweets, :class_name => 'Tweet', :foreign_key => :tweet_idend
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 Bert92
counter_cache Money
t.retweets.length pull all recordsthen calls .length
pull all recordsthen calls .length
t.retweets.count count query count query
t.retweets.size count queryno query
look at cache
Without Cache Counter With Cache Counter
LEVEL 4 Model Bert93
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 Bert94
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 Bert95
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 Bert96
Law of DemeterEach unit should have limited knowledge about other units
“don’t talk to strangers”
tweet user account settings
XLEVEL 4 Model Bert
97
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 endend
LEVEL 4 Model Bert98
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 endend
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 MethodsLEVEL 4 Model Bert
99
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 settingsX
self.user.location_on_tweets
ERROR!! account_setting is nil!
LEVEL 4 Model Bert100
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 settingsX
self.user.location_on_tweets
:allow_nil => true
,,
Returns nil when Account_Settings is missing
LEVEL 4 Model Bert101
Head to to_s /app/models/user.rb
<%= @user.display_name %>
class User < ActiveRecord::Base def display_name "#{first_name} #{last_name}" endend
LEVEL 4 Model Bert102
Head to to_s /app/models/user.rb
<%= @user %>
class User < ActiveRecord::Base def to_s "#{first_name} #{last_name}" endend
LEVEL 4 Model Bert103
to_param-alama ding dong
/app/models/topic.rb
/post/2133
class Topic < ActiveRecord::Base def to_param "#{id}-#{name.parameterize}" endend
/post/rails-best-practices
SEO Friendly URLS
/post/2133-rails-best-practices
LEVEL 4 Model Bert104
to_param-alama ding dong/app/models/topic.rb
class Topic < ActiveRecord::Base def to_param "#{id}-#{name.parameterize}" endend
/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 Bert105
Level 5Froggy Views
106
The example
LEVEL 5 Froggy Views107
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
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
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
LEVEL 5 Froggy Views
/app/views/tweets/index.html.erb
@followers_count @recent_followers@following_count @recent_following
<%= follow_box(<%= follow_box(
"Followers", , ) %>"Following", , ) %>FollowersFollowing
/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
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
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.downcasetitle count
recent.each do |user|str +=
user.avatar.url(:thumb)link_to user do
image_tag(end
)
)
content_tag :div, :class => dostr = )
end
raw(str
+ content_tag(:span,
Annoying str variable
/app/helpers/tweets_helper.rb
Helper Skelter
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
titlecount
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
The Example
LEVEL 5 Froggy Views115
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
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
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
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
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
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
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
LEVEL 5 Froggy Views
empty string things
<% if @user.email.blank? %>
<% unless @user.email? %>
<% if @user.email.present? %>
<% if @user.email? %>
123
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
LEVEL 5 Froggy Views
empty string things
<%= @user.city || @user.state || "Unknown" %>
<%= @user.city.presence || @user.state.presence || "Unknown" %>
125
LEVEL 5 Froggy Views
empty string things
city is nil
undefined method `titleize' for nil:NilClass
<%= @user.city.titleize %>|| "Unknown"
126
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
The example
LEVEL 5 Froggy Views128
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
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 endend
, :class =>
id="tweet_<%= tweet.id %>"
130
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
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
LEVEL 5 Froggy Views
/app/controllers/tweets_controller.rb
what if all actions in the tweet controller need the sidebar?
class TweetsController < ApplicationControllerlayout '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
LEVEL 5 Froggy Views
The example
134
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(",") endend
/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
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
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