1. 什么是 Active Record?
Active Record 是 MVC 中的 M 部分——模型——它是负责表示数据和业务逻辑的系统层。Active Record 帮助您创建和使用其属性需要持久存储到数据库的 Ruby 对象。
Active Record 和 Active Model 有什么区别?可以使用 不需要 数据库支持的 Ruby 对象来建模数据。Active Model 在 Rails 中通常用于此目的,使 Active Record 和 Active Model 都成为 MVC 中 M 的一部分,以及您自己的普通 Ruby 对象。
“Active Record”一词也指一种软件架构模式。Rails 中的 Active Record 是该模式的一种实现。它也是一种称为对象关系映射系统的描述。以下部分解释了这些术语。
1.1. Active Record 模式
Active Record 模式由 Martin Fowler 在《企业应用架构模式》一书中描述为“一个对象,它封装了数据库访问,并向该数据添加了领域逻辑,它封装了数据库表中的一行”。Active Record 对象同时包含数据和行为。Active Record 类与底层数据库的记录结构非常匹配。这样,用户可以轻松地从数据库中读取和写入数据,如下面的示例所示。
1.2. 对象关系映射
对象关系映射(通常称为 ORM)是一种将编程语言的丰富对象连接到关系数据库管理系统 (RDBMS) 中的表的 技术。在 Rails 应用程序中,这些是 Ruby 对象。使用 ORM,Ruby 对象的属性以及对象之间的关系可以轻松地从数据库中存储和检索,而无需直接编写 SQL 语句。总的来说,ORM 最大限度地减少了您必须编写的数据库访问代码量。
要充分理解 Active Record,对关系数据库管理系统 (RDBMS) 和结构化查询语言 (SQL) 的基本知识有所帮助。请参阅此 SQL 教程(或此 RDBMS 教程),或者如果您想了解更多信息,请通过其他方式学习它们。
1.3. 作为 ORM 框架的 Active Record
Active Record 使我们能够使用 Ruby 对象执行以下操作
- 表示模型及其数据。
- 表示模型之间的关联。
- 通过相关模型表示继承层次结构。
- 在模型持久化到数据库之前验证它们。
- 以面向对象的方式执行数据库操作。
2. Active Record 中的约定优于配置
在使用其他编程语言或框架编写应用程序时,可能需要编写大量配置代码。对于一般的 ORM 框架尤其如此。但是,如果您遵循 Rails 采用的约定,那么在创建 Active Record 模型时,您将编写很少或不编写配置代码。
Rails 采用的理念是,如果您大多数时间都以相同的方式配置应用程序,那么这种方式应该成为默认方式。只有在无法遵循约定 的情况下才需要显式配置。
要利用 Active Record 中的约定优于配置,需要遵循一些命名和模式约定。如果需要,可以覆盖命名约定。
2.1. 命名约定
Active Record 使用此命名约定在模型(由 Ruby 对象表示)和数据库表之间进行映射
Rails 会将您的模型类名复数化以查找相应的数据库表。例如,名为 Book 的类映射到名为 books 的数据库表。Rails 的复数化机制非常强大,能够复数化(和单数化)英语中的规则词和不规则词。这使用了 Active Support pluralize 方法。
对于由两个或更多单词组成的类名,模型类名将遵循 Ruby 的 UpperCamelCase 命名约定。在这种情况下,数据库表名将是 snake_case 名称。例如
BookClub是模型类,单数,每个单词的首字母大写。book_clubs是匹配的数据库表,复数,单词之间用下划线分隔。
以下是模型类名和相应表名的一些示例
| 模型 / 类 | 表 / 模式 |
|---|---|
Article |
articles |
LineItem |
line_items |
Product |
products |
Person |
people |
2.2. 模式约定
Active Record 也对数据库表中的列名使用约定,具体取决于这些列的用途。
- 主键 - 默认情况下,Active Record 将使用名为
id的整数列作为表的主键(PostgreSQL、MySQL 和 MariaDB 为bigint,SQLite 为integer)。当使用Active Record 迁移创建表时,此列将自动创建。 - 外键 - 这些字段应按照
singularized_table_name_id模式命名(例如,order_id,line_item_id)。这些是 Active Record 在模型之间创建关联时将查找的字段。
还有一些可选的列名将为 Active Record 实例添加附加功能
created_at- 记录首次创建时自动设置为当前日期和时间。updated_at- 记录创建或更新时自动设置为当前日期和时间。lock_version- 为模型添加乐观锁定。type- 指定模型使用单表继承。(association_name)_type- 存储多态关联的类型。(table_name)_count- 用于缓存关联上所属对象的数量。例如,如果Article有许多Comment,则articles表中的comments_count列将缓存每篇文章的现有评论数量。
虽然这些列名是可选的,但它们被 Active Record 保留。在命名表的列时,请避免使用保留关键字。例如,type 是一个保留关键字,用于指定使用单表继承 (STI) 的表。如果您不使用 STI,请使用不同的词语来准确描述您正在建模的数据。
3. 创建 Active Record 模型
生成 Rails 应用程序时,将在 app/models/application_record.rb 中创建一个抽象的 ApplicationRecord 类。ApplicationRecord 类继承自 ActiveRecord::Base,它将一个普通的 Ruby 类转换为 Active Record 模型。
ApplicationRecord 是应用程序中所有 Active Record 模型的基类。要创建新模型,只需继承 ApplicationRecord 类即可
class Book < ApplicationRecord
end
这将创建一个 Book 模型,映射到数据库中的 books 表,其中表中的每一列都映射到 Book 类的属性。Book 的实例可以表示 books 表中的一行。包含 id、title 和 author 列的 books 表可以使用如下 SQL 语句创建
CREATE TABLE books (
id int(11) NOT NULL auto_increment,
title varchar(255),
author varchar(255),
PRIMARY KEY (id)
);
然而,这并不是您在 Rails 中通常的做法。Rails 中的数据库表通常使用Active Record 迁移而不是原始 SQL 创建。上述 books 表的迁移可以这样生成
$ bin/rails generate migration CreateBooks title:string author:string
如果您没有为字段指定类型(例如,title 而不是 title:string),Rails 将默认为 string 类型。
结果如下
# Note:
# The `id` column, as the primary key, is automatically created by convention.
# Columns `created_at` and `updated_at` are added by `t.timestamps`.
# db/migrate/20240220143807_create_books.rb
class CreateBooks < ActiveRecord::Migration[8.1]
def change
create_table :books do |t|
t.string :title
t.string :author
t.timestamps
end
end
end
该迁移创建了 id、title、author、created_at 和 updated_at 列。此表的每一行都可以由具有相同属性的 Book 类的实例表示:id、title、author、created_at 和 updated_at。您可以像这样访问图书的属性
irb> book = Book.new
=> #<Book:0x00007fbdf5e9a038 id: nil, title: nil, author: nil, created_at: nil, updated_at: nil>
irb> book.title = "The Hobbit"
=> "The Hobbit"
irb> book.title
=> "The Hobbit"
您可以使用命令 bin/rails generate model Book title:string author:string 生成 Active Record 模型类和匹配的迁移。这会创建 app/models/book.rb、db/migrate/20240220143807_create_books.rb 文件以及一些用于测试的文件。
3.1. 创建命名空间模型
Active Record 模型默认放置在 app/models 目录下。但您可能希望通过将相似模型放置在自己的文件夹和命名空间下来组织模型。例如,order.rb 和 review.rb 位于 app/models/book 下,分别具有 Book::Order 和 Book::Review 类名。您可以使用 Active Record 创建命名空间模型。
在 Book 模块尚不存在的情况下,generate 命令将创建所有内容,如下所示
$ bin/rails generate model Book::Order
invoke active_record
create db/migrate/20240306194227_create_book_orders.rb
create app/models/book/order.rb
create app/models/book.rb
invoke test_unit
create test/models/book/order_test.rb
create test/fixtures/book/orders.yml
如果 Book 模块已存在,您将被要求解决冲突
$ bin/rails generate model Book::Order
invoke active_record
create db/migrate/20240305140356_create_book_orders.rb
create app/models/book/order.rb
conflict app/models/book.rb
Overwrite /Users/bhumi/Code/rails_guides/app/models/book.rb? (enter "h" for help) [Ynaqdhm]
命名空间模型生成成功后,Book 和 Order 类如下所示
# app/models/book.rb
module Book
def self.table_name_prefix
"book_"
end
end
# app/models/book/order.rb
class Book::Order < ApplicationRecord
end
在 Book 中设置 table_name_prefix 将允许 Order 模型的数据库表名为 book_orders,而不是普通的 orders。
另一种可能性是您已经有一个要保留在 app/models 中的 Book 模型。在这种情况下,您可以在 generate 命令期间选择 n 以不覆盖 book.rb。
这仍然允许 Book::Order 类使用命名空间表名,而无需 table_name_prefix
# app/models/book.rb
class Book < ApplicationRecord
# existing code
end
Book::Order.table_name
# => "book_orders"
4. 覆盖命名约定
如果您需要遵循不同的命名约定或需要将 Rails 应用程序与旧版数据库一起使用怎么办?没问题,您可以轻松覆盖默认约定。
由于 ApplicationRecord 继承自 ActiveRecord::Base,因此您的应用程序的模型将拥有许多有用的方法。例如,您可以使用 ActiveRecord::Base.table_name= 方法来自定义应该使用的表名
class Book < ApplicationRecord
self.table_name = "my_books"
end
如果您这样做,您将必须在测试定义中手动定义托管 fixtures (my_books.yml) 的类名,使用 set_fixture_class 方法
# test/models/book_test.rb
class BookTest < ActiveSupport::TestCase
set_fixture_class my_books: Book
fixtures :my_books
# ...
end
也可以使用 ActiveRecord::Base.primary_key= 方法覆盖应作为表主键的列
class Book < ApplicationRecord
self.primary_key = "book_id"
end
Active Record 不建议使用名为 id 的非主键列。 使用名为 id 的列而不是单列主键会使访问列值变得复杂。应用程序将必须使用 id_value 别名属性来访问非 PK id 列的值。
如果您尝试创建名为 id 且不是主键的列,Rails 将在迁移期间抛出错误,例如:您不能重新定义 'my_books' 上的主键列 'id'。 要定义自定义主键,请将 { id: false } 传递给 create_table。
5. CRUD:读写数据
CRUD 是我们用于操作数据的四个动词的缩写:Create(创建)、Read(读取)、Update(更新)和 Delete(删除)。Active Record 自动创建方法,允许您读取和操作存储在应用程序数据库表中的数据。
Active Record 通过使用这些抽象数据库访问细节的高级方法,使执行 CRUD 操作变得无缝。请注意,所有这些便捷方法都会生成对底层数据库执行的 SQL 语句。
以下示例展示了一些 CRUD 方法以及生成的 SQL 语句。
5.1. 创建
Active Record 对象可以从哈希、块创建,或者在创建后手动设置其属性。new 方法将返回一个新的、未持久化的对象,而 create 将把对象保存到数据库并返回它。
例如,给定一个具有 title 和 author 属性的 Book 模型,create 方法调用将创建一个对象并将新记录保存到数据库
book = Book.create(title: "The Lord of the Rings", author: "J.R.R. Tolkien")
# Note that the `id` is assigned as this record is committed to the database.
book.inspect
# => "#<Book id: 106, title: \"The Lord of the Rings\", author: \"J.R.R. Tolkien\", created_at: \"2024-03-04 19:15:58.033967000 +0000\", updated_at: \"2024-03-04 19:15:58.033967000 +0000\">"
而 new 方法将实例化一个对象,但 不会 将其保存到数据库
book = Book.new
book.title = "The Hobbit"
book.author = "J.R.R. Tolkien"
# Note that the `id` is not set for this object.
book.inspect
# => "#<Book id: nil, title: \"The Hobbit\", author: \"J.R.R. Tolkien\", created_at: nil, updated_at: nil>"
# The above `book` is not yet saved to the database.
book.save
book.id # => 107
# Now the `book` record is committed to the database and has an `id`.
如果提供了块,create 和 new 都会将新对象传递给该块进行初始化,但只有 create 会将生成的对象持久化到数据库
book = Book.new do |b|
b.title = "Metaprogramming Ruby 2"
b.author = "Paolo Perrotta"
end
book.save
book.save 和 Book.create 产生的 SQL 语句大致如下
/* Note that `created_at` and `updated_at` are automatically set. */
INSERT INTO "books" ("title", "author", "created_at", "updated_at") VALUES (?, ?, ?, ?) RETURNING "id" [["title", "Metaprogramming Ruby 2"], ["author", "Paolo Perrotta"], ["created_at", "2024-02-22 20:01:18.469952"], ["updated_at", "2024-02-22 20:01:18.469952"]]
最后,如果您想不带回调或验证地插入多条记录,可以直接使用 insert 或 insert_all 方法将记录插入数据库
Book.insert(title: "The Lord of the Rings", author: "J.R.R. Tolkien")
Book.insert_all([{ title: "The Lord of the Rings", author: "J.R.R. Tolkien" }])
5.2. 读取
Active Record 提供了一个丰富的 API,用于访问数据库中的数据。您可以查询单条记录或多条记录,按任何属性过滤它们,对它们进行排序、分组,选择特定字段,以及做任何您可以用 SQL 做的事情。
# Return a collection with all books.
books = Book.all
# Return a single book.
first_book = Book.first
last_book = Book.last
book = Book.take
上述操作产生以下 SQL
-- Book.all
SELECT "books".* FROM "books"
-- Book.first
SELECT "books".* FROM "books" ORDER BY "books"."id" ASC LIMIT ? [["LIMIT", 1]]
-- Book.last
SELECT "books".* FROM "books" ORDER BY "books"."id" DESC LIMIT ? [["LIMIT", 1]]
-- Book.take
SELECT "books".* FROM "books" LIMIT ? [["LIMIT", 1]]
我们还可以使用 find_by 和 where 查找特定的书籍。find_by 返回一条记录,而 where 返回一个记录列表
# Returns the first book with a given title or `nil` if no book is found.
book = Book.find_by(title: "Metaprogramming Ruby 2")
# Alternative to Book.find_by(id: 42). Will throw an exception if no matching book is found.
book = Book.find(42)
上述操作产生以下 SQL
-- Book.find_by(title: "Metaprogramming Ruby 2")
SELECT "books".* FROM "books" WHERE "books"."title" = ? LIMIT ? [["title", "Metaprogramming Ruby 2"], ["LIMIT", 1]]
-- Book.find(42)
SELECT "books".* FROM "books" WHERE "books"."id" = ? LIMIT ? [["id", 42], ["LIMIT", 1]]
# Find all books by a given author, sort by created_at in reverse chronological order.
Book.where(author: "Douglas Adams").order(created_at: :desc)
产生以下 SQL
SELECT "books".* FROM "books" WHERE "books"."author" = ? ORDER BY "books"."created_at" DESC [["author", "Douglas Adams"]]
还有更多 Active Record 方法可以读取和查询记录。您可以在Active Record 查询指南中了解更多信息。
5.3. 更新
一旦检索到 Active Record 对象,就可以修改其属性并将其保存到数据库。
book = Book.find_by(title: "The Lord of the Rings")
book.title = "The Lord of the Rings: The Fellowship of the Ring"
book.save
一种简写方式是使用哈希将属性名称映射到所需值,如下所示
book = Book.find_by(title: "The Lord of the Rings")
book.update(title: "The Lord of the Rings: The Fellowship of the Ring")
update 产生以下 SQL
/* Note that `updated_at` is automatically set. */
UPDATE "books" SET "title" = ?, "updated_at" = ? WHERE "books"."id" = ? [["title", "The Lord of the Rings: The Fellowship of the Ring"], ["updated_at", "2024-02-22 20:51:13.487064"], ["id", 104]]
这在一次更新多个属性时非常有用。与 create 类似,使用 update 会将更新后的记录提交到数据库。
如果您想不带回调或验证地批量更新多条记录,可以直接使用 update_all 更新数据库
Book.update_all(status: "already own")
5.4. 删除
同样,一旦检索到 Active Record 对象,就可以销毁它,从而将其从数据库中删除。
book = Book.find_by(title: "The Lord of the Rings")
book.destroy
destroy 产生以下 SQL
DELETE FROM "books" WHERE "books"."id" = ? [["id", 104]]
如果您想批量删除多条记录,可以使用 destroy_by 或 destroy_all 方法
# Find and delete all books by Douglas Adams.
Book.destroy_by(author: "Douglas Adams")
# Delete all books.
Book.destroy_all
此外,如果您想不带回调或验证地删除多条记录,可以直接使用 delete 和 delete_all 方法从数据库中删除记录
Book.find_by(title: "The Lord of the Rings").delete
Book.delete_all
6. 验证
Active Record 允许您在模型写入数据库之前验证其状态。有几种方法可以实现不同类型的验证。例如,验证属性值是否不为空,是否唯一,是否尚未在数据库中,是否遵循特定格式等等。
save、create 和 update 等方法在将模型持久化到数据库之前会验证模型。如果模型无效,则不执行任何数据库操作。在这种情况下,save 和 update 方法返回 false。create 方法仍然返回对象,可以检查其是否存在错误。所有这些方法都有一个叹号对应方法(即 save!、create! 和 update!),它们更严格,因为当验证失败时,它们会引发 ActiveRecord::RecordInvalid 异常。一个简单的示例如下
class User < ApplicationRecord
validates :name, presence: true
end
irb> user = User.new
irb> user.save
=> false
irb> user.save!
ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
create 方法总是返回模型,无论其有效性如何。然后您可以检查此模型是否有任何错误。
irb> user = User.create
=> #<User:0x000000013e8b5008 id: nil, name: nil>
irb> user.errors.full_messages
=> ["Name can't be blank"]
您可以在Active Record 验证指南中了解更多关于验证的信息。
7. 回调
Active Record 回调允许您将代码附加到模型生命周期中的某些事件。这使您可以通过在这些事件发生时执行代码来为模型添加行为,例如创建新记录、更新记录、销毁记录等等。
class User < ApplicationRecord
after_create :log_new_user
private
def log_new_user
puts "A new user was registered"
end
end
irb> @user = User.create
A new user was registered
您可以在Active Record 回调指南中了解更多关于回调的信息。
8. 迁移
Rails 提供了一种通过迁移来管理数据库模式更改的便捷方式。迁移以领域特定语言编写,并存储在文件中,这些文件针对 Active Record 支持的任何数据库执行。
这是一个创建名为 publications 的新表的迁移
class CreatePublications < ActiveRecord::Migration[8.1]
def change
create_table :publications do |t|
t.string :title
t.text :description
t.references :publication_type
t.references :publisher, polymorphic: true
t.boolean :single_issue
t.timestamps
end
end
end
请注意,上述代码与数据库无关:它将在 MySQL、MariaDB、PostgreSQL、SQLite 和其他数据库中运行。
Rails 跟踪已提交到数据库的迁移,并将它们存储在同一数据库中名为 schema_migrations 的相邻表中。
要运行迁移并创建表,您需要运行 bin/rails db:migrate,要回滚并删除表,则运行 bin/rails db:rollback。
您可以在Active Record 迁移指南中了解更多关于迁移的信息。
9. 关联
Active Record 关联允许您定义模型之间的关系。关联可用于描述一对一、一对多和多对多关系。例如,“作者拥有多本书”这样的关系可以定义如下
class Author < ApplicationRecord
has_many :books
end
Author 类现在拥有向作者添加和删除书籍的方法,等等。
您可以在Active Record 关联指南中了解更多关于关联的信息。