Skip to content

Instantly share code, notes, and snippets.

@hjijin
Forked from nightire/Changes in Rails 4_1.md
Created October 31, 2013 01:50
Show Gist options
  • Save hjijin/7243275 to your computer and use it in GitHub Desktop.
Save hjijin/7243275 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 CSRF Attack,比如使用这样一个链接:

CodeSchool 的 Rail 4 教程里写的是 XSS Attack,经查证和问询,证明这是 CodeSchool 的失误,应该会很快改正过来,再次先做一个修正,并向受到误导的朋友致歉

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

这会使用 GET 去请求这个资源,你绝对不想看到这种情况(你希望的是 POST),所以你要限制客户端可以访问此资源的方式。例如:

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

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 了,只不过是多了一层防范措施,以减少初学者不小心造成的性能问题。

ActiveRecord

Scopes

顺着上一节的话题,我们继续讲讲 Scopes。在 Rails 4 当中,eager-evaluated scopes 不再推荐使用了,因为通常没搞清对象(obejct)的热心(eager)帮助往往会帮倒忙!

附注:eager 这个词在这里不好翻译,其原意是有“热情”、“渴望”的含义,在这里代表“在预先不知道查询请求的对象时就先做查询”。这一点固然有积极的意义,但有时候也会带来意料不到的结果。请看下文:

举个例子:

scope :sold, where(state: 'sold')
default_scope where(state: 'available')

这样定义 scope 就称之为 eager-evaluated,因为在进行查询之前并不知道具体要调用该查询的对象是谁。在 Rails 4 中,以上代码会抛出警告:

DEPRECATION WARNING: Using #scope without passing a callable object is deprecated

DEPRECATION WARNING: Calling #default_scope without a block is deprecated

按照提示所说,你需要在定义 scope 的时候传递一个 proc 对象,所以修正的方法也很简单:

scope :sold, -> { where(state: 'sold') }
default_scope -> { where(state: 'available') }

为什么呢?看一个实际的例子就明白了:

scope :recent, where(published_at: 2.weeks.ago)

这段代码的问题就在于 2.weeks.ago 的求值只会在这个 class 载入时发生一次,以后再调用的时候你还是会得到一模一样的值。

scope :recent, -> { where(published_at: 2.weeks.ago) }
scope :recent_red, recent.where(color: 'red')

转变成 proc 对象后,当你再次调用它就会重新求值(上例第二行,recent_red 调用 recent,recent 会重新求值),于是此问题就解决了。

当然,你应该在所有的 scopes 里应用这一原则,因此上例最终应写成:

scope :recent, -> { where(published_at: 2.weeks.ago) }
scope :recent_red, -> { recent.where(color: 'red') }

这个变化可能不是那么新鲜,毕竟多数开发者在 Rails 3 的时候就是这么处理的,Rails 4 只是对未处理过的 scopes 报出警告而已,算是一个小小的变化。


Relation#not

#not 是一个新方法,而且非常好用。我们先来看一段代码:

Book.where('author != ?', author)

你知道这个查询有什么问题么?大部分情况下它工作良好,但如果 author = nil 的话,Rails 会产生如下 SQL 语句:

SELECT "posts".* FROM "posts" WHERE (author != NULL)

它能用,但是最后括号里的部分不符合 SQL 的语法规则,这会让许多“代码洁癖”患者感到寝食难安的!(玩笑)所以他们通常会写出如下无可奈何的临时解决方案:

if author
  Book.where('author != ?', author)
else
  Book.where('author IS NOT NULL')
end

现在,同样的需求在 Rails 4 里可以这样写:

Book.where.not(author: author)

该查询生成的 SQL 语句非常标准:

SELECT "posts".* FROM "posts" WHERE (author IS NOT NULL)

Relation#none

#none 也是和 #not 一样棒的新方法,考察一下这段代码:

Class User < ActiveRecord::Base
  def visible_posts # 查询可见的帖子...
    case role # ...基于用户的角色
    when 'Country Manager'
      Post.where(country: country)
    when 'Reviewer'
      Post.published
    when 'Bad User'
      ???
    end
  end
end

那么,对于 Bad User 我们要求不返回任何帖子,你要怎么做?比较直觉性的做法就是返回一个空数组 [],但是对于下面的代码来说:

@posts = current_user.visible_posts
@posts.recent

会报错:

NoMethodError: undefined method `recent' for []:Array

本着“头疼医头,脚疼医脚”的精神……你可以这么搞:

@posts = current_user.visible_posts

if @posts.any?
  @posts.recent
else
  []
end

但是这太丑了,不是么?你必须要检查查询数组里有没有东西,然后在明知没有的情况下再返回代表“没有”的空数组……多愚蠢啊~为什么 Rails 不能帮我们检查是否“没有”呢?在 Rails 4 里这变成了可能:

Class User < ActiveRecord::Base
  def visible_posts
    case role
    when 'Country Manager'
      Post.where(country: country)
    when 'Reviewer'
      Post.published
    when 'Bad User'
      Post.none # 空即是空,无便是无……
    end
  end
end

上例中的 Post.none 并不只是返回空数组,而是返回一个不去碰数据库的 ActiveRecord::Relation,你可以获得如下的查询:

@posts = current_user.visible_posts
@posts.recent # 根据前文的条件,这个方法会产生三个可能的查询:

# 1
Post.where(country: country).recent

# 2
Post.published.recent

# 3
Post.none.recent # 不会报错,这是对的查询

Relation#order

#order 方法现在产生了一些新的变化,主要是针对生成的 SQL 语句,以下简明列举:

class User < ActiveRecord::Base
  default_scope -> { order(:name) }
end

User.order("created_at DESC")

以上代码在 3 和 4 里产生了有所区别的 SQL:

/*in r3*/
SELECT * FROM users ORDER BY name asc, created_at desc

/*in r4*/
SELECT * FROM users ORDER BY created_at desc, name asc

另外,现在可以用 symbol 来代表排序的查询条件了:

# in r3
User.order('created_at DESC')
User.order(:name, 'created_at DESC')

# in r4
User.order(created_at: :desc)
User.order(:name, created_at: :desc)

这么做的好处还是为了增强一致性,并且利于使用 hash 传入查询条件。


Relation#references

说到字符串形式的查询条件,在 Rails 4 中对于这样的代码:

Post.includes(:comments).where("comments.name = 'foo'")

会抛出警告:

DEPRECATION WARNING: It looks like you are eager loading table(s) (one of: posts, comments) that are referenced in a string SQL snippet. (...)

所以你必须对字符串形式的查询显式声明其引用的表是哪一个,就像这样:

Post.includes(:comments).where("comments.name = 'foo'").references(:comments)

然而对于 hash 形式的条件传递,就不需要特意声明了:

Post.includes(:comments).where(comments: { name: 'foo' })
# or
Post.includes(:comments).where('comments.name' => 'foo' })

像下面这样没有条件的查询,尽管是字符串也无需声明 references

Post.includes(:comments).order('comments.name')

Relation#pluck

pluck 方法现在可以接受多个参数了(每个参数代表数据库表中的一个字段):

Person.pluck(:id, :name)

现在将会返回包含两个字段的记录了,一个小小的但是很有用的改进。


Relation#unscope

Post.comments.except(:order)

像上面这一句代码,你以为会排除 order 的排序,但却不尽然。因为如果 Comment 的 default_scope 是带有 order 的话,except 并无法改变 Post.comments 的查询结果。幸好 Rails 4 中多了一个新方法:

Post.comments.unscope(:order) == Post.comments.order

这样会确保你想要的结果,而不必担心 default_scope 所造成的影响。另外,unscope 方法是支持多个参数的。


Partial inserts

当向数据库插入新的记录的时候,Rails 会对比缺省值,然后只把发生变化的字段放进 INSERT 语句里,剩下的部分由数据库自动填充。这一变化会使得增加记录效率更高,移除数据库字段也会更加安全。

ActiveModel

ActiveModel::Model

Rails 3 中增加了 ActiveModel 使得我们可以创建和 ActiveRecord 一样的模型,拥有几乎全部功能却不需要和数据库关联,就像这样:

class SupportTicket
  include ActiveModel::Conversion
  include ActiveModel::Validations
  extend ActiveModel::Naming

  attr_accessor :title, :description

  validates_presence_of :title
  validates_presence_of :description
end

于是,你可以为其生成关系表单,做条件验证等等,非常方便。在 Rails 4 中,对 ActiveModel 做了小小的改进,现在你可以直接 include 它的“精简版”:

class SupportTicket
  include ActiveModel::Model

  attr_accessor :title, :description

  validates_presence_of :title
  validates_presence_of :description
end

ActiveModel::Model 是一个“混编模组”:

# activemodel/lib/active_model/model.rb
def self.included(base)
  base.class_eval do
    extend ActiveModel::Naming
    extend ActiveModel::Translation
    include ActiveModel::Validations
    include ActiveModel::Conversion
  end
end

Easy and clear!

Association in Rails 4

相比 Rails 3,Rails 4 里的 Association 返回的不再是数组而是一个集合代理(CollectionProxy),这一变化是好是坏应该说莫衷一是,具体产生的影响由于演示起来篇幅过长,所以请移步这篇博客

总结起来就是输出到客户端的关系数据会有所变化,会影响到 JSON API,不过在适应了规则之后,前端工程师处理这些小变化应该是没什么问题的。

Others

Migration Helper

#create_join_table

Migration 文件里新添加了一个 Helper method, 专门用于为 HABTM 关系创建关联表:

create_join_table :categories, :products, :id => false do |f|
  f.integer :categories_id, :null => false
  f.integer :products_id, :null => false
end

现在主键会自己初始化为 nil,除非你用别的值覆盖它。

self.disable_ddl_transaction!

如果你选用的数据库支持 DDL Transaction,那么所有的数据库迁移会被包裹在一个事务中完成;然而某些 SQL 命令无法在事物内部成功执行,这会造成迁移的失败。在 Rails 4 中,你可以把这些造成失败的命令抽取出来放在一个单独的 migration 里,然后使用这个方法来禁止事务处理:

class ChangeSth < ActiveRecord::Migration
  self.disable_ddl_transaction!
  def change
    # some SQLs those can not execute in a transaction
  end
end

Schema Cache Dump

在产品环境中,Rails 应用在初始化的时候会把所有 model 的数据库模式(schema)载入至一个 schema cache(模式缓存)中。对那些拥有庞大数量的 models 的应用程序而言,Rails 4 提供了 schema cache dump(模式缓存转储)的新功能,用来加速应用程序的启动。你可以使用这个 rake task:

$ RAILS_ENV=production bundle exec rake db:schema:cache:dump

这会生成一个 db/schema_cache.dump 文件,Rails 用它来加载 SchemaCache 实例的内部状态。

你可以选择关闭这个功能,编辑 config/production.rb 文件,添加这一行:

config.active_record.use_schema_cache_dump = false

如果你要清除 schema cache,执行:

$ RAILS_ENV=production bundle exec rake db:schema:cache:clear
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment