ActiveRecord

Active Record 是 MVC 中的 M(模型),处理数据和业务逻辑。Active Record 负责创建和使用需要持久存入数据库中的数据。Active Record 实现了 Active Record 模式,是一种对象关系映射系统。

Active Record 模式出自 Martin Fowler 的《企业应用架构模式》一书。在 Active Record 模式中,对象中既有持久存储的数据,也有针对数据的操作。Active Record 模式把数据存取逻辑作为对象的一部分,处理对象的用户知道如何把数据写入数据库,以及从数据库中读出数据。

对象关系映射(ORM)是一种技术手段,把程序中的对象和关系型数据库中的数据表连接起来。使用 ORM,程序中对象的属性和对象之间的关系可以通过一种简单的方法从数据库获取,无需直接编写 SQL 语句,也不过度依赖特定的数据库种类。

那么这种映射关系是:

ActiveRecord DB
Model(类) Table(表)
Model的实例对象 Row(行)
Model的实例对象的属性 Cloumn(列)

Active Record 提供了很多功能,其中最重要的几个如下:

表示模型和其中的数据;
表示模型之间的关系;
通过相关联的模型表示继承关系;
持久存入数据库之前,验证模型;
以面向对象的方式处理数据库操作;

约定大于配置

编写很多配置代码。大多数的 ORM 框架都是这样。但是,如果遵循 Rails 的约定,创建 Active Record 模型时不用做多少配置(有时甚至完全不用配置)。Rails 的理念是,如果大多数情况下都要使用相同的方式配置程序,那么就应该把这定为默认的方法。所以,只有常规的方法无法满足要求时,才要额外的配置。

命名约定

默认情况下,Active Record 使用一些命名约定,查找模型和数据表之间的映射关系。Rails 把模型的类名转换成复数,然后查找对应的数据表。例如,模型类名为 Book,数据表就是 books。Rails 提供的单复数变形功能很强大,常见和不常见的变形方式都能处理。如果类名由多个单词组成,应该按照 Ruby 的约定,使用驼峰式命名法,这时对应的数据表将使用下划线分隔各单词。因此:

数据表名:复数,下划线分隔单词(例如 book_clubs)
模型类名:单数,每个单词的首字母大写(例如 BookClub)
模型 / 类 数据表 / 模式
Post posts
LineItem line_items
Deer deers
Mouse mice
Person people

模式约定

根据字段的作用不同,Active Record 对数据表中的字段命名也做了相应的约定:

外键 - 使用 singularized_table_name_id 形式命名,例如 item_id,order_id。创建模型关联后,Active Record 会查找这个字段;
主键 - 默认情况下,Active Record 使用整数字段 id 作为表的主键。使用 Active Record 迁移创建数据表时,会自动创建这个字段;

还有一些可选的字段,能为 Active Record 实例添加更多的功能:

created_at - 创建记录时,自动设为当前的时间戳;
updated_at - 更新记录时,自动设为当前的时间戳;
lock_version - 在模型中添加乐观锁定功能;
type - 让模型使用单表继承;
(association_name)_type - 多态关联的类型;
(table_name)_count - 缓存关联对象的数量。例如,posts 表中的 comments_count 字段,缓存每篇文章的评论数;

说明: 虽然这些字段是可选的,但在 Active Record 中是被保留的。如果想使用相应的功能,就不要把这些保留字段用作其他用途。例如,type 这个保留字段是用来指定数据表使用“单表继承”(STI)的,如果不用 STI,请使用其他的名字,例如“context”,这也能表明该字段的作用。

创建model

app/models文件下面,创建user.rb文件:

class User < ActieveRecord::Base

end

大家看这个类,User作为类名,而不是Users,这是因为Rails的约定大于配置原则,你定义为user.rb和User类名,Rails会自动关联users表,否则Rails将找不到对应的表。

当然,你也可以在model中通过一个方法指定一个表名:

class User < ActiveRecord::Base
  self.table_name = 'accounts'
end

但是,除了是集成遗留的数据库,否则不要这么做。

默认的id主键

大家注意到没有?在migration文件中,我们并没有指定数据库表的id,Rails会把id默认当作主键。

Rails Console

创建好Rails 这个User model之后,我们可以进入Rails的控制台来操作下数据库表。

$ rails c

c是consle的简写,你用rails consle也照样可以进入,默认是development环境,通过指定RAILS_ENV=production可以指定为生产环境。

rails console是一个类似于irb的Shell UI,但是加载了Rails项目中的所有文件。所以你可以在里面进行代码调试。

数据操作

我们在这一节中,都是和Rails的active record组件打交道,包括我们的migration,包括创建model。

之前说过了,ActiveRecord是Rails的ORM框架,它是一个独立的组件,你也可以在非Rails环境下去使用它。

我们在控制台里先体验一下:

$ rails c
Loading development environment (Rails 3.2.18)
2.1.1 :001 > User.first
  User Load (0.7ms)  SELECT `users`.* FROM `users` LIMIT 1
 => nil
 2.1.1 :002 > User
 => User(id: integer, name: string, email: string, password_digest: string, gender: string, description: string, introduce: text, created_at: datetime, updated_at: datetime, phone: string)

可以看到我使用的是Ruby2.1.1版本,加载的是Rails 3.2.18的开发环境,当然Rails4也是一样的。

我们执行了User.first方法,可以看到,控制台打印出了对应是sql语句。

SELECT `users`.* FROM `users` LIMIT 1

了解mysql的人,一看便知,这是查询users表的第一条数据。

详解ActiveRecord

通过上面的步骤,从migration到用model操作数据,我想大家大概了解了ActiveRecord的作用。

ActiveRecord将数据库表和Ruby的类映射了起来,而这个类就叫做model, 这种映射,正是通过我不断重复的约定大于配置原则形成的。

数据库表中每一行记录,对应Rails的model来说,就是对应model类的一个对象,数据库表的每个字段,则是这个对象的属性。

继续在控制台里试验这些代码:

2.1.1 :001 > user = User.new
=> #<User id: nil, name: nil, email: nil, password_digest: nil, gender: nil, description: nil, introduce: nil, created_at: nil, updated_at: nil, phone: nil>

2.1.1 :002 > user.name
=> nil

2.1.1 :003 > user.name = "blackanger"
=> "blackanger"

2.1.1 :004 > user.save
(1.2ms)  BEGIN
  SQL (0.8ms)  INSERT INTO `users` (`created_at`, `description`, `email`, `gender`, `introduce`, `name`, `password_digest`, `phone`, `updated_at`) VALUES ('2014-06-15 11:27:14', NULL, NULL, NULL, NULL, 'blackanger', NULL, NULL, '2014-06-15 11:27:14')
   (14.2ms)  COMMIT
 => true

可以看到,当创建一个新对象user的时候,我们相当于在创建一行数据表行记录,但是这个时候我们和数据库是没有交互的,只是model 对象层面上。

我们给user.name赋值为"blackanger", 相当于设置这行数据的name字段值为 "blackanger"。这个时候还是和数据库没有交互。

直到我们调用,user.save方法,看到打印出了sql执行的记录。

这个时候我们看到,给数据库插入了一行记录,而这行记录正是按我们的user对象所表达的那样。

id, created_at和updated_at的值,都是ActiveRecord帮你自动创建了。

2.1.1 :001 > User.first
User Load (1.1ms)  SELECT `users`.* FROM `users` LIMIT 1
 => #<User id: 1, name: "blackanger", email: nil, password_digest: nil, gender: nil, description: nil, introduce: nil, created_at: "2014-06-15 11:27:14", updated_at: "2014-06-15 11:27:14", phone: nil>
因为我们只为user设置了一个属性值,name,所以这里只有name有值,其他的都为nil,意思就是没有值。

Arel

我们可以看到,ActiveRecord,不仅映射了对象和数据库表数据间的关系,而且还可以生成对应的sql语句。

这真的是太神奇了。

这所有的魔法,都因为ActiveRecord里的一个组件: Arel。

Arel是从Rails3.0被引进的。对于其详细的语法感兴趣的可以查看源码。

Arel是基于SQL AST管理的Ruby实现。

AST意味着什么? AST是语法树,也就是说Arel是基于SQL 的语法树实现的,非常科学,这也是它强大的地方。

对于Rails来说,它是一种内部DSL,它提供简单的方法,就可以生成复杂的Sql查询语句。并且它还支持多种数据库驱动,包括Mysql, Postgresql等。

你可以用Arel自己实现一个ORM也没问题。

ActiveRecord,对Arel进行了进一步封装,实现了一个ActiveRecord::Relation类,这样,当我们使用where查询语句的时候, 我们每次查询都会返回一个ActiveRecord::Relation对象,并把每次的查询语句记录在这个对象中,只有当真正使用这些数据的时候,才去数据库进行查询。

Model Associations 对象关联

我们创建好了model,就该设置model之间的关联了。

model之间的关联,在数据库层面,是表之间通过主键、外键建立的关系。

表之间的关系,无非就是三种:一对一, 一对多, 多对多。

这三种表关系映射到model层面, 那就是类之间的关系。 Rails完美的支持了这三种关系。

比如下面是一个多对多关系:

class UserBook < ActiveRecord::Base
  belongs_to :user
  belongs_to :book
end

class UserArticle < ActiveRecord::Base
  belongs_to :user
  belongs_to :article
end

class User < ActiveRecord::Base
  has_many :user_books
  has_many :books, through: :user_books
end

class Book < ActiveRecord::Base
  has_many :user_books
  has_many :users, through: :user_books
end

如果你想设置一对一,也可以用has_one方法。

那么在你这样设置了model之间的关系之后, has_many和belongs_to方法实际上会自动帮你在model中添加一些方法,来方便你操作数据库,拿上面的例子来说:

user = User.first
user.books

我们可以通过两行代码来查找第一个用户的所有的书,是不是很简单。

回调

ActiveRecord也提供一些回调,来方便你在数据操作完毕之后进行一些其他动作。比如:

class User < ActiveRecord::Base
  has_many :user_books
  has_many :books, through: :user_books

  after_crate do |user|
    user.passpword = Digest::MD5.hexdigest(user.passpword + "#{salt}")
  end
end

我们可以在创建用户之后,为用户加密密码。

迁移

Rails 提供了一个 DSL 用来处理数据库模式,叫做“迁移”。 迁移的代码存储在特定的文件中,通过 rake 调用,可以用在 Active Record 支持的所有数据库上。下面这个迁移会新建一个数据表:

class CreatePublications < ActiveRecord::Migration
  def change
    create_table :publications do |t|
      t.string :title
      t.text :description
      t.references :publication_type
      t.integer :publisher_id
      t.string :publisher_type
      t.boolean :single_issue

      t.timestamps
    end
    add_index :publications, :publication_type_id
  end
end

Rails 会跟踪哪些迁移已经应用到数据库中,还提供了回滚功能。创建数据表要执行 rake db:migrate 命令;回滚操作要执行 rake db:rollback 命令。