rails最佳实践
--codeschool笔记
FAT MODEL, SKINNY CONTROLLER
代码争取放在model中,因为model是功能模块,更易于测试,而且更底层,易于复用。
controller是业务逻辑,对错后知后觉.而且想想把业务逻辑和功能都放在一起,那是多么杯具的事件。
Scope it out
bad_code:
XX_controller:
@tweets = Tweet.find(
:all,
:conditions => {:user_id => current_user.id},
:order => 'created_at desc',
:limit => 10
)
好一点,但还是行:
@tweets = current_user.tweets.order('created_at desc').limit(10)
good_code:
tweets_controller.rb
@tweets = current_user.tweets.recent.limit(10)
models/tweet.rb
scope :recent, order('created_at desc' )
这个重构的原则就是不要在controller里出现与实现功能相关的代码如created_at desc ,这个代码属于功能性代码,而recent这个scope则能很好的表达order('created_at desc' )这个功能组合 。
使用:default_scope 个人认为这个不是很好,因为default这种有辐射性质的函数很难控制
使用lambda{}
scope :trending, lambda { where('started_trending > ?', 1.day.ago ).order( 'mentions desc' ) }
上面会查询一次,后面再用是取值,把前面取到的词返回,加lambda解决这个问题
lambda { |var = nil| xxxx } 使用默认值:
scope :trending, lambda { |num = nil| where('started_trending > ?', 1.day.ago ).order( 'mentions desc' ).limit(num) }
@trending = Topic.trending(5)
@trending = Topic.trending
unscoped:
default_scope order ('created_at desc' )
@tweets = current_user.tweets.unscoped.order(:status).limit(10)
tweets_controller.rb
BAD:
t = Tweet.new
t.status = "RT #{@tweet.user.name}: #{@tweet.status}"
t.original_tweet = @tweet
t.user = current_user
t.save
GOOD:
current_user.tweets.create(
:status => "RT #{@tweet.user.name}: #{@tweet.status}",
:original_tweet => @tweet
)
fantastic filters
BAD
before_filter :get_tweet, :only => [:edit, :update, :destroy]
GOOD:
@tweet = get_tweet( params[:id])
private
def get_tweet (tweet_id)
Tweet.find(tweet_id)
end
:except => [:index, :create]
Nested attributes
@user = User.new(params[:user])
@user.save
has_one :account_setting, :dependent => :destroy
accepts_nested_attributes_for :account_setting
<%= form_for(@user) do |f| %>
...
<%= f. fields_for :account_setting do |a| %>
def new
@user = User.new (:account_setting => AccountSetting.new)
end
Models without the database
class ContactForm
include ActiveModel::Validations
include ActiveModel::Conversion #<%= form_for @contact_form
attr_accessor :name, :email, :body
validates_presence_of :name, :email, :body
def initialize(attributes = {})
attributes.each do |name, value|
send("#{name}=", value) #ContactForm.new(params[:contact_form])
end
end
def persisted?
false
end
end
<%= form_for @contact_form , :url => send_email_path do |f| %>
@contact_form = ContactForm.new
def new
@contact_form = ContactForm.new
end
def send_email
@contact_form = ContactForm.new(params[:contact_form])
if @contact_form.valid?
Notifications.contact_us( @contact_form ).deliver
redirect_to root_path, :notice => "Email sent, we'll get back to you"
else
render :new
end
end
really Rest
UsersController
subscribe_mailing_list
unsubscribe_mailing_list
SubscriptionsController
create
destroy
Enter the Presenters
@presenter = Tweets::IndexPresenter.new(current_user)
/conditionsnfig/application.rb
config.autoload_paths += [config.root.join("app/presenters")]
/app/presenters/tweets/index_presenter.rb
class Tweets::IndexPresenter
extend ActiveSupport::Memoizable
def initialize(user)
@user = user
end
def followers_tweets
@user.followers_tweets.limit(20)
end
def recent_tweet
@recent_tweet ||= @user.tweets.first
end
def trends
@user.trend_option == "worldwide"
if trends
Trend.worldwide.by_promoted.limit(10)
else
Trend.filter_by(@user.trend_option).limit(10)
end
end
memoize :recent_tweet, :followers_tweet, ...
end
Memoization
extend ActiveSupport::Memoizable
memoize :recent_tweet, :followers_tweet,
def expensive(num)
# lots of processing
end
memoize :expensive
expensive(2)
expensive(2)
reject sql injection
BAD:
User.where("name = #{params[:name]}")
GOOD:
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))
Rails 3 responder syntax
respond_to :html, :xml, :json
def index
@users = User.all
respond_with(@users)
end
def show
@user = User.find(params[:id])
respond_with(@user)
end
Loving your indices #index索引的复数
经常desc的属性,可以加index,index就是用插入时间换查询时间
protecting your attributes
bad:
attr_protected :is_admin
good:
attr_accessible :email, :password, :password_confirmation
default values
change_column_default :account_settings, :time_zone, 'EST'
change_column :account_settings, :time_zone, :string, nil
Proper use of callbacks
RENDING_PERIOD = 1.week
before_create :set_trend_ending
private
def set_trend_ending
self.finish_trending = TRENDING_PERIOD .from_now
end
Rails date helpers
Date Helpers:
1.minute
2.hour
3.days
4.week
5.months
6.year
Modifiers:
beginning_of_day #end
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
improved validation
/lib/appropriate_validator.rb
class AppropriateValidator < ActiveRecord::EachValidator
def validate_each(record, attribute, value)
unless ContentModerator.is_suitable?(value)
record.errors.add( attribute, 'is inappropriate')
end
end
end
/app/models/topic.rb
validates :name, :appropriate => true
Sowing the Seeds
topics =[ {:name=> "Rails for Zombies", :mentions => 1023},
{ :name=> "Top Ruby Jobs", :mentions => 231},
{:name=> "Ruby5", :mentions => 2312}]
Topic.destroy_all
Topic.create do |t|
t.name = attributes[:name]
t.mentions = attributes[:mentions]
end
不够好
topics.each do |attributes|
Topic.find_or_initialize_by_name( attributes[:name]).tap do |t|
t.mentions = attributes[:mentions]
t.save!
end
end
N+1 is not for fun
self.followers.recent.collect{ |f| f.user.name }.to_sentence
self.followers.recent.includes(:user).collect{ |f| f.user.name }.to_sentence
Select followers where user_id=1
Select users where user_id in (2,3,4,5)
Bullet gem
https://github.com/flyerhzm/bullet
To find all your n+1 queries
counter_cache Money
pluralize( tweet.retweets.length , "ReTweet")
pluralize( tweet.retweets.count , "ReTweet")
pluralize( tweet.retweets.size , "ReTweet")
class Tweet < ActiveRecord::Base
belongs_to :original_tweet,
:class_name => 'Tweet',
:foreign_key => :tweet_id,
:counter_cache => : retweets_count
has_many :retweets,
:class_name => 'Tweet',
:foreign_key => :tweet_id
end
add_column :tweets,:retweets_count,:integer,:default => 0
Batches of find_each
Tweet .find_each(:batch_size => 200) do |tweet|
p "task for #{tweet}"
end
数量大的时候很有用 pulls batches of 1,000 at a time default
Law of Demeter
delegate :location_on_tweets, :public_email,
:to => :account_setting,
:allow_nil => true
Head to to_s
def to_s
"#{first_name} #{last_name}"
end
to_param-alama ding dong
/post/2133
/post/rails-best-practices
/post/2133-rails-best-practices
class Topic < ActiveRecord::Base
def to_param
"#{id}-#{name.parameterize}"
end
end
<%= link_to topic.name, topic %>
/post/2133-rails-best-practices
{:id => "2133-rails-best-practices"}
Topic.find(params[:id])
call to_i
Topic.find(2133)
No queries in your view!
Helper Skelter
index.html.erb
<%= follow_box("Followers", @followers_count , @recent_followers) %>
<%= follow_box("Following", @following_count , @recent_following) %>
tweets_helper.rb
def follow_box(title, count, recent)
content_tag :div, :class => title.downcase do
raw(
title +
content_tag(:span, count) +
recent.collect do |user|
link_to user do
image_tag(user.avatar.url(:thumb))
end
end .join
)
end
end
Partial sanity
<%= render 'trending', :area => @user.trending_area,:topics => @trending %>
<% topics.each do |topic| %>
<li>
<%= link_to topic.name, topic %>
<% if topic.promoted? %>
<%= link_to image_tag('promoted.jpg'), topic %>
<% end %>
</li>
<% end %>
<% topics.each do |topic| %>
<%= render topic %>
<% end %>
<li>
<%= link_to topic.name, topic %>
<% if topic.promoted? %>
<%= link_to image_tag('promoted.jpg'), topic %>
<% end %>
</li>
empty string things
@user.email.blank? @user.email.present? = @user.email?
<%= @user.city || @user.state || "Unknown" %>
<%= @user.city.presence || @user.state.presence || "Unknown" %>
<%= @user.city ? @user.city.titleize : "Unknown" %>
<%= @user.city.try(:titleize) || "Unknown" %>
rock your block helpers
<% @presenter.tweets.each do |tweet| %>
<div id="tweet_<%= tweet.id %>"
class="<%= 'favorite' if tweet.is_a_favorite?(current_user) %>">
<%= tweet.status %>
</div>
<% end %>
/app/views/tweets/index.html.erb
<% @presenter.tweets.each do |tweet| %>
<%= tweet_div_for(tweet, current_user) do %>
<%= tweet.status %>
<% end %>
<% end %>
/app/helpers/tweets_helper.rb
def tweet_div_for(tweet, user, &block)
klass = 'favorite' if tweet.is_a_favorite?(user)
content_tag tweet, :class => klass do
yield
end
end
Yield to the content_for
<% if flash[:notice] %>
<span style="color: green"><%= flash[:notice] %></span>
<% end %>
<%= yield :sidebar %>
<% content_for(:sidebar) do %>
... html here ...
<% end %>
/app/views/layouts/applica5on.html.erb
<%= yield :sidebar %>
<% if flash[:notice] %>
<span style="color: green"><%= flash[:notice] %></span>
<% end %>
<%= yield %>
/app/controllers/tweets_controller.rb
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' %>
meta Yield
/app/views/layouts/applica5on.html.erb
<title>Twitter <%= yield(:title) %></title>
<meta name="description"
content="<%= yield(:description) || "The best way ..." %>">
<meta name ="keywords"
content="<%= yield(:keywords) || "social,tweets ..." %>">
/app/views/tweets/show.html.erb
<%
content_for(:title, @tweet.user.name)
content_for(:description, @tweet.status)
content_for(:keywords, @tweet.hash_tags.join(","))
%>