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 最小化了您必须编写的数据库访问代码量。
了解关系数据库管理系统 (RDBMS) 和结构化查询语言 (SQL) 的基本知识有助于全面理解 Active Record。如果您想了解更多信息,请参考 本 SQL 教程(或 本 RDBMS 教程)或通过其他方式学习它们。
1.3 Active Record 作为 ORM 框架
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
表中的一行。可以使用类似于下面的SQL语句来创建具有id
、title
和author
列的books
表。
CREATE TABLE books (
id int(11) NOT NULL auto_increment,
title varchar(255),
author varchar(255),
PRIMARY KEY (id)
);
但是,这并不是您在Rails中通常的做事方式。Rails中的数据库表通常使用Active Record Migrations创建,而不是使用原始SQL。上面的books
表的迁移可以像这样生成。
$ bin/rails generate migration CreateBooks title:string author: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.0]
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
目录下。但您可能希望通过将类似模型放在它们自己的文件夹和命名空间下,来组织您的模型。例如,app/models/book
下的order.rb
和review.rb
,分别使用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
。
另一种可能性是您已经有一个Book
模型,您想保留在app/models
中。在这种情况下,您可以在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
如果您这样做,您将必须使用您的测试定义中的set_fixture_class
方法手动定义承载fixture(my_books.yml
)的类名。
# 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将在迁移期间抛出错误,例如:you can't redefine the primary key column 'id' on 'my_books'.
To define a custom primary key, pass { id: false } to 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"]]
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 Query指南中了解有关它们的更多信息。
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
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 Validations指南中了解有关验证的更多信息。
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 Callbacks指南中了解有关回调的更多信息。
8 迁移
Rails提供了一种通过迁移来管理数据库模式更改的便捷方式。迁移是用特定领域语言编写的,并存储在文件中,这些文件将针对Active Record支持的任何数据库执行。
以下是一个创建名为publications
的新表的迁移
class CreatePublications < ActiveRecord::Migration[8.0]
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 Migrations指南中了解有关迁移的更多信息。
9 关联
Active Record关联允许您定义模型之间的关系。关联可用于描述一对一、一对多和多对多关系。例如,可以定义“作者有多本书”这样的关系,如下所示
class Author < ApplicationRecord
has_many :books
end
现在,Author
类具有向作者添加和删除书籍的方法,以及更多其他方法。
您可以在Active Record Associations指南中了解有关关联的更多信息。