拥抱 Rails 4_3


## ActiveRecord

### Scopes

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

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

举个例子：

```ruby
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 对象，所以修正的方法也很简单：

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

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

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

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

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

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

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

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

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

---

### Relation#not

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

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

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

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

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

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

现在，同样的需求在 Rails 4 里可以这样写：

```ruby
Book.where.not(author: author)
```

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

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

---

### Relation#none

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

```ruby
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 我们要求不返回任何帖子，你要怎么做？比较直觉性的做法就是返回一个空数组 `[]`，但是对于下面的代码来说：

```ruby
@posts = current_user.visible_posts
@posts.recent
```

会报错：

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

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

```ruby
@posts = current_user.visible_posts

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

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

```ruby
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`，你可以获得如下的查询：

```ruby
@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 语句，以下简明列举：

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

User.order("created_at DESC")
```

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

```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 来代表排序的查询条件了：

```ruby
# 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 中对于这样的代码：

```ruby
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. (...)

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

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

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

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

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

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

---

### Relation#pluck

`pluck` 方法现在可以接受多个参数了（每个参数代表数据库表中的一个字段）：

```ruby
Person.pluck(:id, :name)
```

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

---

### Relation#unscope

```ruby
Post.comments.except(:order)
```

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

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

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

---

### Partial inserts

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

## ActiveModel

### ActiveModel::Model

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

```ruby
￼￼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 它的“精简版”：

```ruby
￼￼class SupportTicket
  include ActiveModel::Model

  attr_accessor :title, :description

  validates_presence_of :title
  validates_presence_of :description
end
```

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

```ruby
# 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），这一变化是好是坏应该说莫衷一是，具体产生的影响由于演示起来篇幅过长，所以请移步[这篇博客](http://blog.olivierlacan.com/posts/associations-in-rails-4/)

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

## Others

### Migration Helper

#### #create_join_table

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

```ruby
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 里，然后使用这个方法来禁止事务处理：

```ruby
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：

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

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

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

```ruby
config.active_record.use_schema_cache_dump = false
```

如果你要清除 schema cache，执行：

```bash
$ RAILS_ENV=production bundle exec rake db:schema:cache:clear
```