rails best practices_slides

Post on 12-Sep-2014

986 Views

Category:

Education

1 Downloads

Preview:

Click to see full reader

DESCRIPTION

 

TRANSCRIPT

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]=hacked@by.me" 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

top related