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