ActiveRecord 与 Ecto 对比(第二部分)
这是“ActiveRecord vs. Ecto”系列的第二部分,蝙蝠侠和蝙蝠女侠将就数据库查询展开争论,我们将苹果和橘子进行比较。
在《ActiveRecord 与 Ecto 对比(第一部分)》中探讨了数据库模式和迁移之后,本文将介绍 ActiveRecord 和 Ecto 如何帮助开发者查询数据库,以及在处理相同需求时两者的异同。此外,我们还将揭秘蝙蝠女侠在 1989 年至 2011 年间的真实身份。
种子数据
让我们开始吧!基于本系列第一篇文章中定义的数据库结构,假设users表invoices中存储了以下数据:
用户
| 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">
异形
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]
}
比较
这两种情况非常相似。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">]>
异形
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]
}
]
比较
它遵循与上一节完全相同的模式。ActiveRecord 使用all类方法,而 Ecto 则依赖于存储库模式来加载记录。
SQL 查询语句中又存在一些差异:
- 与上一节相同,ActiveRecord 加载所有字段(
users.*),而 Ecto 只加载定义中列出的字段schema。 - ActiveRecord 还定义了一个
LIMIT 11,而 Ecto 则直接加载所有内容。此限制源于inspect控制台中使用的方法(https://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation.rb#L599)。
使用条件进行查询
我们不太可能需要从表中获取所有记录。常见的需求是使用条件来筛选返回的数据。
让我们用这个例子列出所有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">]>
异形
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
}
]
比较
这两个例子中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)>
Repo.all()只有在调用函数并将结构体作为参数传递时,才会访问数据库Ecto.Query。这种方法允许在 Ecto 中进行查询组合,这将在下一节中讨论。
查询组成
数据库查询最强大的特性之一就是组合性。它允许查询包含多个条件。
如果你要编写原始 SQL 查询,那么你很可能会用到某种类型的字符串连接。假设你有两个条件:
not_paid = 'paid_at IS NOT NULL'paid_with_paypal = 'payment_method = "Paypal"'
要使用原始 SQL 语句合并这两个条件,意味着您需要使用类似以下的方式将它们连接起来:
SELECT * FROM invoices WHERE #{not_paid} AND #{paid_with_paypal}
幸运的是,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">]>
异形
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
}
]
比较
这两个查询都在回答同一个问题:“哪些发票已支付且使用了PayPal?”
正如预期的那样,ActiveRecord 提供了一种更简洁的查询编写方式(以上述示例为例),而 Ecto 则要求开发者花费更多时间来编写查询。和往常一样,蝙蝠女(孤儿,那个拥有卡珊德拉·凯恩身份的哑女)或 ActiveRecord 的代码并不冗长。
不要被上面 Ecto 查询语句的冗长和看似复杂的形式所迷惑。在实际环境中,该查询语句会被重写成更简洁的形式:
Invoice
|> where([i], not is_nil(i.paid_at))
|> where([i], i.payment_method == "Paypal")
|> Repo.all()
从这个角度来看,函数的“纯粹”特性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">]>
异形
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
}
]
比较
在这两种工具中,给查询添加排序都很简单。
虽然 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()
限制
没有限制的数据库会是什么样子?简直是一场灾难。幸运的是,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">]>
异形
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
}
]
比较
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">]>
上面的例子表明,我们可以通过调用 `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">]>
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>
结果是……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
}
]
与 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