发布于 2026-01-06 0 阅读
0

ActiveRecord 与 Ecto 对比(第二部分)

ActiveRecord 与 Ecto 对比(第二部分)

这是“ActiveRecord vs. Ecto”系列的第二部分,蝙蝠侠和蝙蝠女侠将就数据库查询展开争论,我们将苹果和橘子进行比较。

在《ActiveRecord 与 Ecto 对比(第一部分)》中探讨了数据库模式和迁移之后,本文将介绍 ActiveRecord 和 Ecto 如何帮助开发者查询数据库,以及在处理相同需求时两者的异同。此外,我们还将揭秘蝙蝠女侠在 1989 年至 2011 年间的真实身份。

种子数据

让我们开始吧!基于本系列第一篇文章中定义的数据库结构,假设usersinvoices中存储了以下数据:

用户

ID 全名 电子邮件 创建于* 更新于
1 贝蒂·凯恩 bette@kane.test 2018-01-01 10:01:00 2018-01-01 10:01:00
2 芭芭拉·戈登 barbara@gordon.test 2018-01-02 10:02:00 2018-01-02 10:02:00
3 卡珊德拉·凯恩 cassandra@cain.test 2018-01-03 10:03:00 2018-01-03 10:03:00
4 斯蒂芬妮·布朗 stephanie@brown.test 2018-01-04 10:04:00 2018-01-04 10:04:00

* ActiveRecord 的字段在 Ecto 中默认created_at命名。inserted_at

发票

ID 用户身份 付款方式 付费 创建于* 更新于
1 1 信用卡 2018-02-01 08:00:00 2018-01-02 08:00:00 2018-01-02 08:00:00
2 2 PayPal 2018-02-01 08:00:00 2018-01-03 08:00:00 2018-01-03 08:00:00
3 3 2018-01-04 08:00:00 2018-01-04 08:00:00
4 4 2018-01-05 08:00:00 2018-01-05 08:00:00

* ActiveRecord 的字段在 Ecto 中默认created_at命名。inserted_at

本文中的查询假设上述数据存储在数据库中,因此在阅读本文时请记住这一点。

使用主键查找项目

我们先从使用主键从数据库中获取一条记录开始。

ActiveRecord

irb(main):001:0> User.find(1)
User Load (0.4ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, full_name: "Bette Kane", email: "bette@kane.test", created_at: "2018-01-01 10:01:00", updated_at: "2018-01-01 10:01:00">
Enter fullscreen mode Exit fullscreen mode

异形

iex(3)> Repo.get(User, 1)
[debug] QUERY OK source="users" db=5.2ms decode=2.5ms queue=0.1ms
SELECT u0."id", u0."full_name", u0."email", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) [1]
%Financex.Accounts.User{
  __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
  email: "bette@kane.test",
  full_name: "Bette Kane",
  id: 1,
  inserted_at: ~N[2018-01-01 10:01:00.000000],
  invoices: #Ecto.Association.NotLoaded<association :invoices is not loaded>,
  updated_at: ~N[2018-01-01 10:01:00.000000]
}
Enter fullscreen mode Exit fullscreen mode

比较

这两种情况非常相似。ActiveRecord 依赖于模型类find的类方法User。这意味着每个 ActiveRecord 子类都有自己的find方法。

Ecto 采用不同的方法,它依赖于Repository概念作为映射层和领域之间的中介。使用 Ecto 时,模块User本身并不知道如何找到自身。这种职责由Repo模块承担,模块能够将自身映射到底层数据存储,在本例中为 Postgres。

对比 SQL 查询语句本身,我们可以发现一些差异:

  • ActiveRecord 会加载所有字段(users.*),而 Ecto 只加载定义中列出的字段schema
  • ActiveRecord 包含LIMIT 1查询参数,而 Ecto 则不包含。

正在获取所有项目

我们更进一步,从数据库中加载所有用户。

ActiveRecord

irb(main):001:0> User.all
User Load (0.5ms)  SELECT  "users".* FROM "users" LIMIT $1  [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<User id: 1, full_name: "Bette Kane", email: "bette@kane.test", created_at: "2018-01-01 10:01:00", updated_at: "2018-01-01 10:01:00">, #<User id: 2, full_name: "Barbara Gordon", email: "barbara@gordon.test", created_at: "2018-01-02 10:02:00", updated_at: "2018-01-02 10:02:00">, #<User id: 3, full_name: "Cassandra Cain", email: "cassandra@cain.test", created_at: "2018-01-03 10:03:00", updated_at: "2018-01-03 10:03:00">, #<User id: 4, full_name: "Stephanie Brown", email: "stephanie@brown.test", created_at: "2018-01-04 10:04:00", updated_at: "2018-01-04 10:04:00">]>
Enter fullscreen mode Exit fullscreen mode

异形

iex(4)> Repo.all(User)
[debug] QUERY OK source="users" db=2.8ms decode=0.2ms queue=0.2ms
SELECT u0."id", u0."full_name", u0."email", u0."inserted_at", u0."updated_at" FROM "users" AS u0 []
[
  %Financex.Accounts.User{
    __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
    email: "bette@kane.test",
    full_name: "Bette Kane",
    id: 1,
    inserted_at: ~N[2018-01-01 10:01:00.000000],
    invoices: #Ecto.Association.NotLoaded<association :invoices is not loaded>,
    updated_at: ~N[2018-01-01 10:01:00.000000]
  },
  %Financex.Accounts.User{
    __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
    email: "barbara@gordon.test",
    full_name: "Barbara Gordon",
    id: 2,
    inserted_at: ~N[2018-01-02 10:02:00.000000],
    invoices: #Ecto.Association.NotLoaded<association :invoices is not loaded>,
    updated_at: ~N[2018-01-02 10:02:00.000000]
  },
  %Financex.Accounts.User{
    __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
    email: "cassandra@cain.test",
    full_name: "Cassandra Cain",
    id: 3,
    inserted_at: ~N[2018-01-03 10:03:00.000000],
    invoices: #Ecto.Association.NotLoaded<association :invoices is not loaded>,
    updated_at: ~N[2018-01-03 10:03:00.000000]
  },
  %Financex.Accounts.User{
    __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
    email: "stephanie@brown.test",
    full_name: "Stephanie Brown",
    id: 4,
    inserted_at: ~N[2018-01-04 10:04:00.000000],
    invoices: #Ecto.Association.NotLoaded<association :invoices is not loaded>,
    updated_at: ~N[2018-01-04 10:04:00.000000]
  }
]
Enter fullscreen mode Exit fullscreen mode

比较

它遵循与上一节完全相同的模式。ActiveRecord 使用all类方法,而 Ecto 则依赖于存储库模式来加载记录。

SQL 查询语句中又存在一些差异:

使用条件进行查询

我们不太可能需要从表中获取所有记录。常见的需求是使用条件来筛选返回的数据。

让我们用这个例子列出所有invoices尚未支付的款项(WHERE paid_at IS NULL)。

ActiveRecord

irb(main):024:0> Invoice.where(paid_at: nil)
Invoice Load (18.2ms)  SELECT  "invoices".* FROM "invoices" WHERE "invoices"."paid_at" IS NULL LIMIT $1  [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Invoice id: 3, user_id: 3, payment_method: nil, paid_at: nil, created_at: "2018-01-04 08:00:00", updated_at: "2018-01-04 08:00:00">, #<Invoice id: 4, user_id: 4, payment_method: nil, paid_at: nil, created_at: "2018-01-05 08:00:00", updated_at: "2018-01-05 08:00:00">]>
Enter fullscreen mode Exit fullscreen mode

异形

iex(19)> where(Invoice, [i], is_nil(i.paid_at)) |> Repo.all()
[debug] QUERY OK source="invoices" db=20.2ms
SELECT i0."id", i0."payment_method", i0."paid_at", i0."user_id", i0."inserted_at", i0."updated_at" FROM "invoices" AS i0 WHERE (i0."paid_at" IS NULL) []
[
  %Financex.Accounts.Invoice{
    __meta__: #Ecto.Schema.Metadata<:loaded, "invoices">,
    id: 3,
    inserted_at: ~N[2018-01-04 08:00:00.000000],
    paid_at: nil,
    payment_method: nil,
    updated_at: ~N[2018-01-04 08:00:00.000000],
    user: #Ecto.Association.NotLoaded<association :user is not loaded>,
    user_id: 3
  },
  %Financex.Accounts.Invoice{
    __meta__: #Ecto.Schema.Metadata<:loaded, "invoices">,
    id: 4,
    inserted_at: ~N[2018-01-04 08:00:00.000000],
    paid_at: nil,
    payment_method: nil,
    updated_at: ~N[2018-01-04 08:00:00.000000],
    user: #Ecto.Association.NotLoaded<association :user is not loaded>,
    user_id: 4
  }
]
Enter fullscreen mode Exit fullscreen mode

比较

这两个例子中where都使用了关键字,它与 SQL 子句相关联WHERE。虽然生成的 SQL 查询非常相似,但两种工具实现查询的方式却存在一些重要差异。

ActiveRecord 会自动将paid_at: nil参数转换为paid_at IS NULLSQL 语句。为了使用 Ecto 获得相同的输出,开发人员需要通过调用 `.` 来更明确地表达他们的意图is_nil()

where另一个需要强调的区别是Ecto 中函数的“纯粹”行为。where单独调用该函数时,它不会与数据库交互。该where函数的返回值是一个Ecto.Query结构体:

iex(20)> where(Invoice, [i], is_nil(i.paid_at))
#Ecto.Query<from i in Financex.Accounts.Invoice, where: is_nil(i.paid_at)>
Enter fullscreen mode Exit fullscreen mode

Repo.all()只有在调用函数并将结构体作为参数传递时,才会访问数据库Ecto.Query。这种方法允许在 Ecto 中进行查询组合,这将在下一节中讨论。

查询组成

数据库查询最强大的特性之一就是组合性。它允许查询包含多个条件。

如果你要编写原始 SQL 查询,那么你很可能会用到某种类型的字符串连接。假设你有两个条件:

  1. not_paid = 'paid_at IS NOT NULL'
  2. paid_with_paypal = 'payment_method = "Paypal"'

要使用原始 SQL 语句合并这两个条件,意味着您需要使用类似以下的方式将它们连接起来:

SELECT * FROM invoices WHERE #{not_paid} AND #{paid_with_paypal}
Enter fullscreen mode Exit fullscreen mode

幸运的是,ActiveRecord 和 Ecto 都提供了相应的解决方案。

ActiveRecord

irb(main):003:0> Invoice.where.not(paid_at: nil).where(payment_method: "Paypal")
Invoice Load (8.0ms)  SELECT  "invoices".* FROM "invoices" WHERE "invoices"."paid_at" IS NOT NULL AND "invoices"."payment_method" = $1 LIMIT $2  [["payment_method", "Paypal"], ["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Invoice id: 2, user_id: 2, payment_method: "Paypal", paid_at: "2018-02-01 08:00:00", created_at: "2018-01-03 08:00:00", updated_at: "2018-01-03 08:00:00">]>
Enter fullscreen mode Exit fullscreen mode

异形

iex(6)> Invoice |> where([i], not is_nil(i.paid_at)) |> where([i], i.payment_method == "Paypal") |> Repo.all()
[debug] QUERY OK source="invoices" db=30.0ms decode=0.6ms queue=0.2ms
SELECT i0."id", i0."payment_method", i0."paid_at", i0."user_id", i0."inserted_at", i0."updated_at" FROM "invoices" AS i0 WHERE (NOT (i0."paid_at" IS NULL)) AND (i0."payment_method" = 'Paypal') []
[
  %Financex.Accounts.Invoice{
    __meta__: #Ecto.Schema.Metadata<:loaded, "invoices">,
    id: 2,
    inserted_at: ~N[2018-01-03 08:00:00.000000],
    paid_at: #DateTime<2018-02-01 08:00:00.000000Z>,
    payment_method: "Paypal",
    updated_at: ~N[2018-01-03 08:00:00.000000],
    user: #Ecto.Association.NotLoaded<association :user is not loaded>,
    user_id: 2
  }
]
Enter fullscreen mode Exit fullscreen mode

比较

这两个查询都在回答同一个问题:“哪些发票已支付且使用了PayPal?”

正如预期的那样,ActiveRecord 提供了一种更简洁的查询编写方式(以上述示例为例),而 Ecto 则要求开发者花费更多时间来编写查询。和往常一样,蝙蝠女(孤儿,那个拥有卡珊德拉·凯恩身份的哑女)或 ActiveRecord 的代码并不冗长。

不要被上面 Ecto 查询语句的冗长和看似复杂的形式所迷惑。在实际环境中,该查询语句会被重写成更简洁的形式:

Invoice
|> where([i], not is_nil(i.paid_at))
|> where([i], i.payment_method == "Paypal")
|> Repo.all()
Enter fullscreen mode Exit fullscreen mode

从这个角度来看,函数的“纯粹”特性where(它本身不执行数据库操作)与管道运算符的结合,使得 Ecto 中的查询组合非常简洁。

订购

排序是查询的一个重要方面。它使开发人员能够确保给定的查询结果遵循指定的顺序。

ActiveRecord

irb(main):002:0> Invoice.order(created_at: :desc)
Invoice Load (1.5ms)  SELECT  "invoices".* FROM "invoices" ORDER BY "invoices"."created_at" DESC LIMIT $1  [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Invoice id: 4, user_id: 4, payment_method: nil, paid_at: nil, created_at: "2018-01-05 08:00:00", updated_at: "2018-01-05 08:00:00">, #<Invoice id: 3, user_id: 3, payment_method: nil, paid_at: nil, created_at: "2018-01-04 08:00:00", updated_at: "2018-01-04 08:00:00">, #<Invoice id: 2, user_id: 2, payment_method: "Paypal", paid_at: "2018-02-01 08:00:00", created_at: "2018-01-03 08:00:00", updated_at: "2018-01-03 08:00:00">, #<Invoice id: 1, user_id: 1, payment_method: "Credit Card", paid_at: "2018-02-01 08:00:00", created_at: "2018-01-02 08:00:00", updated_at: "2018-01-02 08:00:00">]>
Enter fullscreen mode Exit fullscreen mode

异形

iex(6)> order_by(Invoice, desc: :inserted_at) |> Repo.all()
[debug] QUERY OK source="invoices" db=19.8ms
SELECT i0."id", i0."payment_method", i0."paid_at", i0."user_id", i0."inserted_at", i0."updated_at" FROM "invoices" AS i0 ORDER BY i0."inserted_at" DESC []
[
  %Financex.Accounts.Invoice{
    __meta__: #Ecto.Schema.Metadata<:loaded, "invoices">,
    id: 3,
    inserted_at: ~N[2018-01-04 08:00:00.000000],
    paid_at: nil,
    payment_method: nil,
    updated_at: ~N[2018-01-04 08:00:00.000000],
    user: #Ecto.Association.NotLoaded<association :user is not loaded>,
    user_id: 3
  },
  %Financex.Accounts.Invoice{
    __meta__: #Ecto.Schema.Metadata<:loaded, "invoices">,
    id: 4,
    inserted_at: ~N[2018-01-04 08:00:00.000000],
    paid_at: nil,
    payment_method: nil,
    updated_at: ~N[2018-01-04 08:00:00.000000],
    user: #Ecto.Association.NotLoaded<association :user is not loaded>,
    user_id: 4
  },
  %Financex.Accounts.Invoice{
    __meta__: #Ecto.Schema.Metadata<:loaded, "invoices">,
    id: 2,
    inserted_at: ~N[2018-01-03 08:00:00.000000],
    paid_at: #DateTime<2018-02-01 08:00:00.000000Z>,
    payment_method: "Paypal",
    updated_at: ~N[2018-01-03 08:00:00.000000],
    user: #Ecto.Association.NotLoaded<association :user is not loaded>,
    user_id: 2
  },
  %Financex.Accounts.Invoice{
    __meta__: #Ecto.Schema.Metadata<:loaded, "invoices">,
    id: 1,
    inserted_at: ~N[2018-01-02 08:00:00.000000],
    paid_at: #DateTime<2018-02-01 08:00:00.000000Z>,
    payment_method: "Credit Card",
    updated_at: ~N[2018-01-02 08:00:00.000000],
    user: #Ecto.Association.NotLoaded<association :user is not loaded>,
    user_id: 1
  }
]
Enter fullscreen mode Exit fullscreen mode

比较

在这两种工具中,给查询添加排序都很简单。

虽然 Ecto 示例使用 aInvoice作为第一个参数,但该order_by函数也接受Ecto.Query结构体,这使得该order_by函数可以用于组合中,例如:

Invoice
|> where([i], not is_nil(i.paid_at))
|> where([i], i.payment_method == "Paypal")
|> order_by(desc: :inserted_at)
|> Repo.all()
Enter fullscreen mode Exit fullscreen mode

限制

没有限制的数据库会是什么样子?简直是一场灾难。幸运的是,ActiveRecord 和 Ecto 都有助于限制返回记录的数量。

ActiveRecord

irb(main):004:0> Invoice.limit(2)
Invoice Load (0.2ms)  SELECT  "invoices".* FROM "invoices" LIMIT $1  [["LIMIT", 2]]
=> #<ActiveRecord::Relation [#<Invoice id: 1, user_id: 1, payment_method: "Credit Card", paid_at: "2018-02-01 08:00:00", created_at: "2018-01-02 08:00:00", updated_at: "2018-01-02 08:00:00">, #<Invoice id: 2, user_id: 2, payment_method: "Paypal", paid_at: "2018-02-01 08:00:00", created_at: "2018-01-03 08:00:00", updated_at: "2018-01-03 08:00:00">]>
Enter fullscreen mode Exit fullscreen mode

异形

iex(22)> limit(Invoice, 2) |> Repo.all()
[debug] QUERY OK source="invoices" db=3.6ms
SELECT i0."id", i0."payment_method", i0."paid_at", i0."user_id", i0."inserted_at", i0."updated_at" FROM "invoices" AS i0 LIMIT 2 []
[
  %Financex.Accounts.Invoice{
    __meta__: #Ecto.Schema.Metadata<:loaded, "invoices">,
    id: 1,
    inserted_at: ~N[2018-01-02 08:00:00.000000],
    paid_at: #DateTime<2018-02-01 08:00:00.000000Z>,
    payment_method: "Credit Card",
    updated_at: ~N[2018-01-02 08:00:00.000000],
    user: #Ecto.Association.NotLoaded<association :user is not loaded>,
    user_id: 1
  },
  %Financex.Accounts.Invoice{
    __meta__: #Ecto.Schema.Metadata<:loaded, "invoices">,
    id: 2,
    inserted_at: ~N[2018-01-03 08:00:00.000000],
    paid_at: #DateTime<2018-02-01 08:00:00.000000Z>,
    payment_method: "Paypal",
    updated_at: ~N[2018-01-03 08:00:00.000000],
    user: #Ecto.Association.NotLoaded<association :user is not loaded>,
    user_id: 2
  }
]
Enter fullscreen mode Exit fullscreen mode

比较

ActiveRecord 和 Ecto 都提供了限制查询返回记录数量的方法。

Ecto 的limit工作方式与 类似order_by,适用于查询组合。

协会

ActiveRecord 和 Ecto 在处理关联关系方面采用了不同的方法。

ActiveRecord

在 ActiveRecord 中,您可以直接使用模型中定义的任何关联,而无需进行任何特殊处理,例如:

irb(main):012:0> user = User.find(2)
User Load (0.3ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 2], ["LIMIT", 1]]
=> #<User id: 2, full_name: "Barbara Gordon", email: "barbara@gordon.test", created_at: "2018-01-02 10:02:00", updated_at: "2018-01-02 10:02:00">
irb(main):013:0> user.invoices
Invoice Load (0.4ms)  SELECT  "invoices".* FROM "invoices" WHERE "invoices"."user_id" = $1 LIMIT $2  [["user_id", 2], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Invoice id: 2, user_id: 2, payment_method: "Paypal", paid_at: "2018-02-01 08:00:00", created_at: "2018-01-03 08:00:00", updated_at: "2018-01-03 08:00:00">]>
Enter fullscreen mode Exit fullscreen mode

上面的例子表明,我们可以通过调用 `getInvoices()` 方法获取用户发票列表user.invoices。调用时,ActiveRecord 会自动查询数据库并加载与该用户关联的发票。虽然这种方法简化了代码编写,减少了额外的步骤,但如果需要遍历多个用户并为每个用户获取发票,则可能会出现问题。这个问题被称为“N+1 问题”。

在 ActiveRecord 中,针对“N+1 问题”提出的解决方案是使用以下includes方法:

irb(main):022:0> user = User.includes(:invoices).find(2)
User Load (0.3ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 2], ["LIMIT", 1]]
Invoice Load (0.6ms)  SELECT "invoices".* FROM "invoices" WHERE "invoices"."user_id" = $1  [["user_id", 2]]
=> #<User id: 2, full_name: "Barbara Gordon", email: "barbara@gordon.test", created_at: "2018-01-02 10:02:00", updated_at: "2018-01-02 10:02:00">
irb(main):023:0> user.invoices
=> #<ActiveRecord::Associations::CollectionProxy [#<Invoice id: 2, user_id: 2, payment_method: "Paypal", paid_at: "2018-02-01 08:00:00", created_at: "2018-01-03 08:00:00", updated_at: "2018-01-03 08:00:00">]>
Enter fullscreen mode Exit fullscreen mode

invoices在这种情况下,ActiveRecord在获取用户时会预先加载关联关系(如所示的两个 SQL 查询所示)。

异形

你可能已经注意到了,Ecto 非常不喜欢魔法或隐含意义。它要求开发者明确地表达他们的意图。

user.invoices让我们尝试使用Ecto的相同方法:

iex(7)> user = Repo.get(User, 2)
[debug] QUERY OK source="users" db=18.3ms decode=0.6ms
SELECT u0."id", u0."full_name", u0."email", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) [2]
%Financex.Accounts.User{
  __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
  email: "barbara@gordon.test",
  full_name: "Barbara Gordon",
  id: 2,
  inserted_at: ~N[2018-01-02 10:02:00.000000],
  invoices: #Ecto.Association.NotLoaded<association :invoices is not loaded>,
  updated_at: ~N[2018-01-02 10:02:00.000000]
}
iex(8)> user.invoices
#Ecto.Association.NotLoaded<association :invoices is not loaded>
Enter fullscreen mode Exit fullscreen mode

结果是……Ecto.Association.NotLoaded不太实用。

要访问发票,开发人员需要使用以下函数告知 Ecto preload

iex(12)> user = preload(User, :invoices) |> Repo.get(2)
[debug] QUERY OK source="users" db=11.8ms
SELECT u0."id", u0."full_name", u0."email", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) [2]
[debug] QUERY OK source="invoices" db=4.2ms
SELECT i0."id", i0."payment_method", i0."paid_at", i0."user_id", i0."inserted_at", i0."updated_at", i0."user_id" FROM "invoices" AS i0 WHERE (i0."user_id" = $1) ORDER BY i0."user_id" [2]
%Financex.Accounts.User{
  __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
  email: "barbara@gordon.test",
  full_name: "Barbara Gordon",
  id: 2,
  inserted_at: ~N[2018-01-02 10:02:00.000000],
  invoices: [
    %Financex.Accounts.Invoice{
      __meta__: #Ecto.Schema.Metadata<:loaded, "invoices">,
      id: 2,
      inserted_at: ~N[2018-01-03 08:00:00.000000],
      paid_at: #DateTime<2018-02-01 08:00:00.000000Z>,
      payment_method: "Paypal",
      updated_at: ~N[2018-01-03 08:00:00.000000],
      user: #Ecto.Association.NotLoaded<association :user is not loaded>,
      user_id: 2
    }
  ],
  updated_at: ~N[2018-01-02 10:02:00.000000]
}

iex(15)> user.invoices
[
  %Financex.Accounts.Invoice{
    __meta__: #Ecto.Schema.Metadata<:loaded, "invoices">,
    id: 2,
    inserted_at: ~N[2018-01-03 08:00:00.000000],
    paid_at: #DateTime<2018-02-01 08:00:00.000000Z>,
    payment_method: "Paypal",
    updated_at: ~N[2018-01-03 08:00:00.000000],
    user: #Ecto.Association.NotLoaded<association :user is not loaded>,
    user_id: 2
  }
]
Enter fullscreen mode Exit fullscreen mode

与 ActiveRecord 类似includes,预加载会获取关联的invoices,这将使它们在调用时可用user.invoices

比较

ActiveRecord 和 Ecto 之间的争论最终还是回到了同一个关键点:显式性。两者都允许开发者轻松访问关联关系,但 ActiveRecord 虽然代码更简洁,却可能导致一些意想不到的行为。Ecto 则遵循所见即所得 (WYSIWYG) 的原则,只执行开发者定义的查询语句中显示的内容。

Rails 因其在应用程序各个层面上使用和推广缓存策略而闻名。例如,它采用了一种名为“俄罗斯套娃”的缓存方法,这种方法完全依赖于“N+1 问题”来实现其缓存机制。

验证

ActiveRecord 中的大多数验证规则在 Ecto 中也可用。以下列出了一些常见的验证规则,以及 ActiveRecord 和 Ecto 对它们的定义:

ActiveRecord 异形
validates :title, presence: true validate_required(changeset, [:title])
validates :email, confirmation: true validate_confirmation(changeset, :email)
validates :email, format: {with: /@/} validate_format(changeset, :email, ~r/@/)
validates :start, exclusion: {in: %w(a b)} validate_exclusion(changeset, :start, ~w(a b))
validates :start, inclusion: {in: %w(a b)} validate_inclusion(changeset, :start, ~w(a b))
validates :terms_of_service, acceptance: true validate_acceptance(changeset, :terms_of_service)
validates :password, length: {is: 6} validate_length(changeset, :password, is: 6)
validates :age, numericality: {equal_to: 1} validate_number(changeset, :age, equal_to: 1)

包起来

这就是苹果和橘子之间最本质的比较。

ActiveRecord 专注于简化数据库查询操作。它的大部分特性都集中在模型类本身,无需开发者深入了解数据库及其操作的影响。ActiveRecord 默认会隐式地执行许多操作。虽然这使得入门更容易,但也使得理解其底层机制变得更加困难,而且只有遵循“ActiveRecord 的方式”才能正常工作。

另一方面,Ecto 要求明确定义,这会导致代码更加冗长。但好处是,所有操作都清晰可见,没有任何隐藏信息,而且你可以按照自己的方式进行定义。

两者各有优势,取决于你的角度和喜好。既然比较的是苹果和橘子,那我们就到此为止吧。差点忘了告诉你,蝙蝠女侠(BatGirl)的代号(1989-2001)是……神谕(Oracle)。不过我们还是别细说了。😉

本文由特约作者Elvio Vicosa撰写。Elvio 是《Phoenix for Rails Developers》一书的作者

文章来源:https://dev.to/appsignal/activerecord-vs-ecto-part-two-3001