1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

拥抱 Rails 4_3

Last updated at Posted at 2013-03-25

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
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?