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