這是 "ActiveRecord vs. Ecto" series 的第二部分 在其中 Batman 和 Batgirl 為查詢數據庫而戰 我們比較蘋果和橙子
在第一部分查看 ActiveRecord vs. Ecto 的 database schemas 和 migrations 之後 這篇文章涵蓋 ActiveRecord 和 Ecto 如何使 developers 能夠查詢數據庫 以及兩者在處理相同要求時如何比較 一路上 我們也將發現 Batgirl 1989-2011 的身份
Seed data
讓我們開始! 基於第一篇文章中定義的 database structure 假設 users 和 invoices 表有以下數據存儲:
users
| id | full_name | created_at* | updated_at | |
|---|---|---|---|---|
| 1 | Bette Kane | bette@kane.test | 2018-01-01 10:01:00 | 2018-01-01 10:01:00 |
| 2 | Barbara Gordon | barbara@gordon.test | 2018-01-02 10:02:00 | 2018-01-02 10:02:00 |
| 3 | Cassandra Cain | cassandra@cain.test | 2018-01-03 10:03:00 | 2018-01-03 10:03:00 |
| 4 | Stephanie Brown | stephanie@brown.test | 2018-01-04 10:04:00 | 2018-01-04 10:04:00 |
* ActiveRecord's created_at field 在 Ecto 中默認命名為 inserted_at
invoices
| id | user_id | payment_method | paid_at | created_at* | updated_at |
|---|---|---|---|---|---|
| 1 | 1 | Credit Card | 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's created_at field 在 Ecto 中默認命名為 inserted_at
本文中執行的 queries 假設上述數據存儲在數據庫中 所以閱讀時請記住這些信息
Find item using its primary key
讓我們從使用 primary key 從數據庫獲取記錄開始
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">
Ecto
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]
}
Comparison
兩種情況都相當相似 ActiveRecord 依賴 User model class 的 find class method 這意味著每個 ActiveRecord 子類都有自己的 find 方法
Ecto 使用不同的方法 依賴 Repository concept 作為 mapping layer 和 domain 之間的 mediator 使用 Ecto 時 User module 沒有關於如何找到自己的知識 這種責任存在於 Repo module 中 能將其映射到底層 datastore 在我們的例子中是 Postgres
比較 SQL query 本身 我們可以發現幾個差異:
- ActiveRecord 載入所有 fields (
users.*) 而 Ecto 只載入schema定義中列出的 fields - ActiveRecord 在查詢中包含
LIMIT 1而 Ecto 不包含
Fetching all items
讓我們更進一步 從數據庫載入所有 users
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">]>
Ecto
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]
}
]
Comparison
它遵循與前一節完全相同的模式 ActiveRecord 使用 all class method 和 Ecto 依賴 repository pattern 載入記錄
SQL queries 中再次有一些差異:
- 同前一節 ActiveRecord 載入所有 fields (
users.*) 而 Ecto 只載入schema定義中列出的 fields - ActiveRecord 也定義
LIMIT 11而 Ecto 只是載入所有東西 這個 limit 來自 console 上使用的inspect方法 (https://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation.rb#L599)
Querying with conditions
我們不太可能需要從表中獲取所有記錄 一個常見需求是使用 conditions 過濾返回的數據
讓我們用那個例子列出所有仍需付款的 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">]>
Ecto
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
}
]
Comparison
在兩個例子中 都使用 where 關鍵字 這是連接到 SQL WHERE clause 雖然生成的 SQL queries 相當相似 但兩個工具如何到達那裡有一些重要差異
ActiveRecord 自動將 paid_at: nil 參數轉換為 paid_at IS NULL SQL statement 為了使用 Ecto 獲得相同輸出 developers 需要更明確他們的意圖 通過調用 is_nil()
另一個要強調的差異是 Ecto 中 where function 的 "pure" 行為 當單獨調用 where function 時 它不與數據庫交互 where function 的返回是一個 Ecto.Query struct:
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() function 時才被觸及 將 Ecto.Query struct 作為參數傳遞 這種方法允許 Ecto 中的 query composition 這是下一節的主題
Query composition
數據庫查詢最強大的方面之一是 composition 它描述一個包含多於單一條件的查詢
如果你在構建 raw SQL queries 你可能會使用某種 concatenation 想像你有兩個條件:
not_paid = 'paid_at IS NOT NULL'paid_with_paypal = 'payment_method = "Paypal"'
為了使用 raw SQL 結合這兩個條件 意味著你必須用類似的東西 concatenate 它們:
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">]>
Ecto
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
}
]
Comparison
兩個查詢回答同一個問題: "哪些 invoices 被付款並使用 Paypal?"
如預期 ActiveRecord 提供更簡潔的方式組合查詢 (對於那個例子) 而 Ecto 要求 developers 在寫查詢上花費更多 不要被上面顯示的 Ecto query 的 verbosity 和 apparent complexity 欺騙 在真實世界環境中 那個查詢將被重寫為更像:
Invoice |> where([i], not is_nil(i.paid_at)) |> where([i], i.payment_method == "Paypal") |> Repo.all()
從那個角度看 where function 的 "pure" 方面 它本身不執行數據庫操作 結合 pipe operator 使 Ecto 中的 query composition 非常 clean
Ordering
Ordering 是查詢的重要方面 它使 developers 能夠確保給定查詢結果遵循指定順序
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">]>
Ecto
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
}
]
Comparison
添加 order 到查詢在兩個工具中都很直觀
雖然 Ecto 例子使用 Invoice 作為第一個參數 order_by function 也接受 Ecto.Query structs 這使 order_by function 可以用於 compositions 像:
Invoice |> where([i], not is_nil(i.paid_at)) |> where([i], i.payment_method == "Paypal") |> order_by(desc: :inserted_at) |> Repo.all()
Limiting
沒有 limit 的數據庫會是什麼? 一場災難 幸運的是 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">]>
Ecto
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
}
]
Comparison
ActiveRecord 和 Ecto 都有方法限制查詢返回的記錄數量
Ecto 的 limit 工作方式類似 order_by 適合 query compositions
Associations
ActiveRecord 和 Ecto 在 associations 處理上有不同方法
ActiveRecord
在 ActiveRecord 中 你可以使用模型中定義的任何 association 而不必做任何特別的事 例如:
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">]>
上面的例子顯示我們可以在調用 user.invoices 時獲取用戶發票列表 這樣做時 ActiveRecord 自動查詢數據庫並載入與用戶關聯的發票 雖然這種方法使事情更容易 意味著寫更少代碼或不必擔心額外步驟 但如果你在迭代多個用戶並為每個用戶獲取發票 可能會是個問題 這個問題稱為 "N + 1 problem"
在 ActiveRecord 中 "N + 1 problem" 的建議修復是使用 includes method:
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">]>
在這種情況下 ActiveRecord eager-loads invoices association 當獲取用戶時 (如顯示的兩個 SQL queries)
Ecto
正如你可能已經注意到的 Ecto 真的不喜歡 magic 或 implicitness 它要求 developers 明確他們的意圖
讓我們嘗試同樣使用 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 不太有用
為了訪問發票 developer 需要讓 Ecto 知道 使用 preload function:
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 preload 將獲取關聯的 invoices 這將使它們在調用 user.invoices 時可用
Comparison
再一次 ActiveRecord 和 Ecto 之間的戰鬥以已知點結束: explicitness 兩個工具都使 developers 能輕易訪問 associations 但 ActiveRecord 使它較不冗長 結果可能有 unexpected behaviours Ecto 遵循 WYSIWYG 類型的方法 只做開發者定義的查詢中看到的
Rails 以使用和推廣 caching strategies 到應用程序所有不同層而聞名 一個例子是使用 "Russian doll" caching approach 它完全依賴 "N + 1 problem" 為其 caching mechanism 執行魔法
Validations
ActiveRecord 中大多數 validations 在 Ecto 中也可用 這裡是常見 validations 列表 和兩者如何定義它們:
| ActiveRecord | Ecto |
|---|---|
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) |
Wrap up
就是這樣: 基本的蘋果對橙子比較
ActiveRecord 專注於執行數據庫查詢的易用性 其絕大多數功能集中在 model classes 本身 不要求 developers 對數據庫有深入理解 也不要求理解這些操作的影響 ActiveRecord 默認做很多事 implicitly 雖然這使它更容易開始 但使它更難理解背後發生了什麼 而且只有你遵循 "ActiveRecord way" 時才有效
Ecto 另一方面 需要 explicitness 導致更冗長的代碼 作為好處 一切都在聚光燈下 沒有背後的東西 你可以指定你自己的方式
兩者都有優點 取決於你的視角和偏好 所以比較了蘋果和橙子 我們來到這場 BAT-tle 的結束 差點忘了告訴你 BatGirl 的 codename (1989 - 2001) 是 .... Oracle 但我們別深究了 😉