更多内容请访问 rubyonrails.org:

Active Record 基础

本指南介绍 Active Record。

阅读本指南后,您将了解

  • Active Record 如何融入模型-视图-控制器 (MVC) 范式。
  • 什么是对象关系映射和 Active Record 模式,以及它们如何在 Rails 中使用。
  • 如何使用 Active Record 模型来操作存储在关系数据库中的数据。
  • Active Record 架构命名约定。
  • 数据库迁移、验证、回调和关联的概念。

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 Supportpluralize 方法。

对于由两个或多个单词组成的类名,模型类名将遵循 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_idline_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语句来创建具有idtitleauthor列的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

该迁移创建了idtitleauthorcreated_atupdated_at列。该表的每一行都可以用具有相同属性的Book类实例来表示:idtitleauthorcreated_atupdated_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.rbdb/migrate/20240220143807_create_books.rb,以及一些用于测试目的的其他文件。

3.1 创建命名空间模型

Active Record模型默认情况下放置在app/models目录下。但您可能希望通过将类似模型放在它们自己的文件夹和命名空间下,来组织您的模型。例如,app/models/book下的order.rbreview.rb,分别使用Book::OrderBook::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]

命名空间模型生成成功后,BookOrder类看起来像这样

# 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方法手动定义承载fixturemy_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将保存该对象到数据库并返回它。

例如,给定一个具有titleauthor属性的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`.

最后,如果提供了块,createnew都会将新对象传递给该块以进行初始化,而只有create会将生成的对象持久化到数据库中

book = Book.new do |b|
  b.title = "Metaprogramming Ruby 2"
  b.author = "Paolo Perrotta"
end

book.save

book.saveBook.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_bywhere查找特定的书籍。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_bydestroy_all方法

# Find and delete all books by Douglas Adams.
Book.destroy_by(author: "Douglas Adams")

# Delete all books.
Book.destroy_all

6 验证

Active Record允许您在模型写入数据库之前验证模型的状态。有几种方法允许进行不同类型的验证。例如,验证属性值是否为空,是否唯一,是否已存在于数据库中,是否遵循特定格式,以及更多其他情况。

savecreateupdate等方法在将模型持久化到数据库之前会对其进行验证。如果模型无效,则不会执行任何数据库操作。在这种情况下,saveupdate方法将返回falsecreate方法仍然返回该对象,可以检查该对象是否有错误。所有这些方法都有一个感叹号的对应方法(即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指南中了解有关关联的更多信息。



返回顶部