Skip to content

Instantly share code, notes, and snippets.

@jhjguxin
Forked from nightire/Changes in Rails 4_1.md
Created March 24, 2013 14:28
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jhjguxin/5232167 to your computer and use it in GitHub Desktop.
Save jhjguxin/5232167 to your computer and use it in GitHub Desktop.

Routes

小心地使用 Match(Rails 3 已实现)

Rails 3 提供了 match 方法供我们自定义 routes,然而我们要小心使用它以避免“跨站脚本攻击”(XSS Attack)。比如像这样的 routes:

注:(r3 代表 Rails 3,r4 代表 Rails 4)

# routes.rb
match '/books/:id/purchase', to: 'books@purchase'

用户可以很轻松地使用 XSS Attack,比如使用这样一个链接:

<a href="http://yoursite.com/books/4/purchase">Get It Free!</a>

你绝对不想看到这种情况,所以你要限制客户端可以访问此资源的方式。例如:

match '/books/:id?purchase', to: 'books@purchase', via: :post # :all 代表匹配所有的 HTTP methods

# 或者
post '/books/:id?purchase', to: 'books@purchase'

否则你就会收到如下错误提示:

You should not use the match method in your router without specifying an HTTP method. (RuntimeError)


新的 HTTP Verb:patch

过去我们使用 put 来完成对资源的更新请求,然而 put 本身是对整个资源(数据集合)进行更新,若要实现部分资源的更新(单个数据,或是几个产生变化的数据实体),put 就有点过重了,此时 patch 会更加合适。

patch 并不是什么新东西,此前就一直存在于 HTTP 1.1 协议规范之中,只不过这一次 Rails 4 把它正式的引入进来。在 Rails 4 中,putpatch 都指向 controller#update,在更新部分资源时(比如 @book)会使用 patch,生成类似下例中的页面元素:

<form action="/books/20" method="post">
  <div style="margin:0;padding:0;display:inline">
  <input name="utf8" type="hidden" value="&#x2713;" />
  <input name="_method" type="hidden" value="patch" /> <!-- 关键就是这一行了 -->
  </div>
</form>

同时还增加了一个 #patch 方法,可以在合适的时候使用:

test "update book with PATCH verb" do
  patch :update, id: @book, book: { title: @book.title }
  assert_redirected_to book_url(@book)
end

Concerns for Routing

Concerns(关注点)是一种组织代码结构的方式,用来帮助开发者将复杂的逻辑和重复代码梳理清楚,我们在 Rails 4 中多次看到对于 Concerns 的设计和实现。先看一段老代码:

resources :messages do
  resources :comments
  resources :categories
  resources :tags
end

resources :posts do
  resources :comments
  resources :categories
  resources :tags
end

resources :articles do
  resources :comments
  resources :categories
  resources :tags
end

像这样的代码存在许多的重复,Rails 4 允许我们重构它:

concern :sociable do
  resources :comments
  resources :categories
  resources :tags
end

resources :messages, concerns: :sociable
resources :posts, concerns: :sociable
resources :articles, concerns: :sociable

可以通过传递参数来实现对个例的特化:

concern :sociable do |options|
  resources :comments, options
  resources :categories, options
  resources :tags, options
end

resources :messages, concerns: :sociable
resources :posts, concerns: :sociable
resources :articles do
  concerns :sociable, only: :create
end

甚至我们可以抽取出来变成单独的类:

# app/concerns/sociable.rb
class Sociable
  def self.call(mapper, options)
    mapper.resources :comments, options
    mapper.resources :categories, options
    mapper.resources :tags, options
  end
end

# config/routes.rb
concern :sociable, Sociable

resources :messages, concerns: :sociable
resources :posts, concerns: :sociable
resources :articles do
  concerns :sociable, only: :create
end

抛弃 Ruby 1.8.x

我们都听说 Rails 4 需要 Ruby 的版本不能小于 1.9.3,不过这一点所引起的变化通常都十分微妙,不容易让人注意到。

聒噪的 nil

1.8.x 时代,nil.id 是合法的(一切都是对象!),但是不合理,经常惹人厌。于是 1.9.2 之后,逐渐使用 object_id 来代替,使用旧的 id 方法会抛出运行时错误:

RuntimeError: Called id for nil, which would mistakenly be 4 -- if you really wanted the id of nil, use object_id

Rails 3 无法永远摆脱这恼人的提示,因为它要同时兼容 1.8 和 1.9,于是一旦碰上可能会出现的 nil.id 就会看到上面那个错误

在 Rails 4 的世界里,手起刀落,喀嚓~~~ 从此 nil 不再聒噪,世界终于清净了……

NoMethodError: undefined method `id' for nil:NilClass

线程安全

线程安全的处理在 Rails 3 中已有,不过默认是关闭的:

# config/environments/production.rb
MyApp::Application.configure do
  # Enable threaded mode
  # config.threadsafe!
end

这个方法在 Rails 4 中不推荐使用,新的线程安全机制在默认情况下就已经开启:

# config/environments/production.rb
MyApp::Application.configure do
  config.cache_classes = true # 阻止类在请求中重新载入,并保证 Rack::Lock 不包含在中间件堆栈中
  config.eager_load = true # 在新线程创建前加载全部代码
end

拥抱 Rails 4_2

ActiveRecord

Finders

Book.find(:all, conditions: { author: 'Albert Yu' })

这种方法已经用了很久了吧?在 Rails 4 中,你会看到如下警告:

DEPRECATION WARNING: Calling #find(:all) is deprecated. Please call #all directly instead. You have also used finder options. These are also deprecated. Please build a scope instead of using finder options.

实际上,老式的 finders 已经被抽取成了 activerecord-deprecated_finders gem,你要还想用就得自己安装它。

在 Rails 4 中,推荐这样用:

Book.where(author: 'Albert Yu')

没人不爱它!而且还没完,同样的变化还有:

Book.find_all_by_title('Rails 4') # r3 way
Book.find_last_by_author('Albert Yu') # r3 way

Book.where(title: 'Rails 4') # r4 way
Book.where(author: 'Albert Yu').last # r4 way

动态的 find_by 也不例外:

Book.find_by_title('Rails 4') # 接收单个参数的用法在 r3 & r4 都可以
Book.find_by(title: 'Rails4') # 不过 r4 更偏爱这样写

Book.find_by_title('Rails 4', conditions: { author: 'Albert Yu' }) # 这就不好了,得改
Book.find_by(title: 'Rails4', author: 'Albert Yu') # Wow! 太棒了!

统一使用 find_by 不仅有更好的一致性,而且更便于接收 hash 参数:

book_param = { title: 'Rails 4', author: 'Albert Yu' }
Book.find_by(book_param)

find_by 方法的内部实现其实很简单:

# activerecord/lib/active_record/relation/finder_methods.rb
def find_by(*args)
  where(*args).take
end

这意味着这样用也没有问题:

Book.find_by("published_on < ?", 3.days.ago)

find_or_*

这两种方法不再推荐使用了:

Book.find_or_initialize_by_title('Rails 4')
Book.find_or_create_by_title('Rails 4')

会抛出如下警告:

DEPRECATION WARNING: This dynamic method is deprecated. Please use e.g. Post.find_or_initialize_by(name: 'foo') instead.

DEPRECATION WARNING: This dynamic method is deprecated. Please use e.g. Post.find_or_create_by(name: 'foo') instead.

让我们从善如流:

Book.find_or_initialize_by(title: 'Rails 4')
Book.find_or_create_by(title: 'Rails 4')

还有一种容易让人迷惑的用法

Book.where(title: 'Rails 4').first_or_create
# 若找不到…
Book.where(title: 'Rails 4').create

这方法在 Rails 3 和 Rails 4 里都可以用,它先是查询是否有符合条件的记录,若没有就以该条件创建一个。听起来还不错,然而当存在这样的代码时,其表现就不是你想的那样了:

class Book < ActiveRecord::Base
  after_create :foo
  
  def foo
    books = books.where(author: 'Albert Yu')
    ...
  end
end

产生的 SQL 是:

SELECT "books".* FROM "books" WHERE "books"."title" = 'Rails 4' AND "books"."author" = 'Albert Yu'

注意,这里的 after_create 回调原本是在创建一条记录后立刻返回__所有作者是 Albert Yu 的记录__,但最终的结果却是__所有标题是 Rails 4 并且作者是 Albert Yu 的记录__。这是因为触发该回调函数的方法调用已经有了 title: 'Rails 4' 的作用域,于是产生了作用域叠加。

Rails 4 里推荐这样来做:

Book.find_or_create_by(title: 'Rails 4')
# 若找不到…
Book.create(title: 'Rails 4')

这样就不会产生叠加副作用,真正的 SQL 语句如下:

SELECT "books".* FROM "books" WHERE "books"."author" = 'admin'

#update & #update_column

是不是经常被 #update_attributes#update_attribute 还有 #update_column 搞晕?好消息来了——Rails 4 重新整理了属性更新的方法,现在的方式简单明了:

@book.update(post_params) # 会触发验证

@book.update_columns(post_params) # 构建 SQL 语句,直接执行于数据库层,不会触发验证

就这俩,不会搞错了吧?以前的方式也还能用,但是不排除会被废弃。既然 Rails 4 提供了更好用的方法,那就不要再犹豫了。

Model.all

也不是所有的变化都那么显而易见的令人愉悦,一部分人大概会对接下来的变化感到不适应。以前普遍认为不要直接使用 Model.all,因为这会产生很严重的性能问题,开发者更倾向于先对 Model 进行 scope:

def index
  @books = Book.scoped
  if params[:recent]
    @books = @books.recent
  end
end

然而,Rails 4 会抛出如下警告:

DEPRECATION WARNING: Model.scoped is deprecated. Please use Model.all instead.

WTF?Model.all 又回来了?

没错。不过你不用担心,Rails 4 里的 Model.all 不会立即执行对数据库的查询,而仅仅是返回一个 ActiveRecord::Relation,你可以继续进行链式调用:

def index
  @books = Book.all # 我不会碰数据库的哦,直到你告诉我下一个条件…
  if params[:recent]
    @books = @books.recent # 这时候我才会行动
  end
end

当然,这并不是说不能用 scoped model 了,只不过是多了一层防范措施,以减少初学者不小心造成的性能问题。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment