นี่เป็นส่วนที่สองของซีรีส์ "ActiveRecord vs. Ecto" ซึ่ง Batman และ Batgirl ต่อสู้กันเพื่อสืบค้นฐานข้อมูล และเราเปรียบเทียบแอปเปิ้ลกับส้ม
หลังจากตรวจสอบสคีมาฐานข้อมูลและการย้ายข้อมูลใน ActiveRecord กับ Ecto ตอนที่หนึ่ง โพสต์นี้จะกล่าวถึงวิธีที่ทั้ง ActiveRecord และ Ecto ช่วยให้นักพัฒนาสามารถสืบค้นฐานข้อมูล และเปรียบเทียบทั้ง ActiveRecord และ Ecto เมื่อต้องรับมือกับข้อกำหนดเดียวกัน ระหว่างทาง เราจะค้นพบตัวตนของ Batgirl ในปี 1989-2011 ด้วย
ข้อมูลเมล็ดพันธุ์
มาเริ่มกันเลย! ตามโครงสร้างฐานข้อมูลที่กำหนดไว้ในโพสต์แรกของชุดนี้ ถือว่า users
และ invoices
ตารางมีข้อมูลต่อไปนี้เก็บไว้:
ผู้ใช้
id | full_name | อีเมล | created_at* | updated_at |
---|---|---|---|---|
1 | เบ็ตต์ เคน | [email protected] | 2018-01-01 10:01:00 | 2018-01-01 10:01:00 |
2 | บาร์บาร่า กอร์ดอน | [email protected] | 2018-01-02 10:02:00 | 2018-01-02 10:02:00 |
3 | แคสแซนดรา เคน | [email protected] | 2018-01-03 10:03:00 | 2018-01-03 10:03:00 |
4 | สเตฟานี่ บราวน์ | [email protected] | 2018-01-04 10:04:00 | 2018-01-04 10:04:00 |
* created_at
ของ ActiveRecord ฟิลด์ชื่อ inserted_at
ใน Ecto โดยค่าเริ่มต้น
ใบแจ้งหนี้
id | user_id | วิธีการชำระเงิน | paid_at | created_at* | updated_at |
---|---|---|---|---|---|
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 |
* created_at
ของ ActiveRecord ฟิลด์ชื่อ inserted_at
ใน Ecto โดยค่าเริ่มต้น
แบบสอบถามที่ดำเนินการผ่านโพสต์นี้ถือว่าข้อมูลข้างต้นถูกจัดเก็บไว้ในฐานข้อมูล ดังนั้นโปรดคำนึงถึงข้อมูลนี้ขณะอ่าน
ค้นหารายการโดยใช้คีย์หลัก
เริ่มต้นด้วยการรับบันทึกจากฐานข้อมูลโดยใช้คีย์หลัก
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: "[email protected]", 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: "[email protected]",
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
โมดูล ซึ่งสามารถจับคู่กับ datastore ด้านล่าง ซึ่งในกรณีของเราคือ 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: "[email protected]", created_at: "2018-01-01 10:01:00", updated_at: "2018-01-01 10:01:00">, #<User id: 2, full_name: "Barbara Gordon", email: "[email protected]", created_at: "2018-01-02 10:02:00", updated_at: "2018-01-02 10:02:00">, #<User id: 3, full_name: "Cassandra Cain", email: "[email protected]", created_at: "2018-01-03 10:03:00", updated_at: "2018-01-03 10:03:00">, #<User id: 4, full_name: "Stephanie Brown", email: "[email protected]", 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: "[email protected]",
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: "[email protected]",
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: "[email protected]",
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: "[email protected]",
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
class method และ 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">]>
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
}
]
การเปรียบเทียบ
ในทั้งสองตัวอย่าง where
ใช้คีย์เวิร์ดซึ่งเป็นการเชื่อมต่อกับ SQL WHERE
ข้อ แม้ว่าแบบสอบถาม SQL ที่สร้างขึ้นจะค่อนข้างคล้ายกัน แต่วิธีที่เครื่องมือทั้งสองได้รับมีความแตกต่างที่สำคัญบางประการ
ActiveRecord แปลง paid_at: nil
อาร์กิวเมนต์ paid_at IS NULL
คำสั่ง SQL โดยอัตโนมัติ เพื่อให้ได้ผลลัพธ์เดียวกันโดยใช้ Ecto นักพัฒนาจะต้องมีความชัดเจนมากขึ้นเกี่ยวกับความตั้งใจของพวกเขา โดยการเรียก is_nil()
.
ความแตกต่างอีกประการที่ต้องเน้นคือพฤติกรรม "บริสุทธิ์" ของฟังก์ชัน where
ในเอ็กโต เมื่อโทรไปที่ 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"'
ในการรวมสองเงื่อนไขเหล่านี้โดยใช้ raw 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">]>
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
}
]
การเปรียบเทียบ
คำถามทั้งสองตอบคำถามเดียวกัน:"ใบแจ้งหนี้ใดที่ชำระและใช้ Paypal แล้ว"
ตามที่คาดไว้ ActiveRecord เสนอวิธีการเขียนแบบสอบถามที่กระชับยิ่งขึ้น (สำหรับตัวอย่างนั้น) ในขณะที่ Ecto ต้องการให้นักพัฒนาใช้เวลามากขึ้นในการเขียนแบบสอบถาม ตามปกติแล้ว Batgirl (เด็กกำพร้า ปิดเสียงบุคคลที่มีตัวตนของ Cassandra Cain) หรือ 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">]>
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
}
]
การเปรียบเทียบ
การเพิ่มลำดับให้กับข้อความค้นหานั้นตรงไปตรงมาในเครื่องมือทั้งสอง
แม้ว่าตัวอย่าง Ecto จะใช้ Invoice
เป็นพารามิเตอร์แรก order_by
ฟังก์ชั่นยังยอมรับ Ecto.Query
structs ซึ่งเปิดใช้งาน 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">]>
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
}
]
การเปรียบเทียบ
ทั้ง ActiveRecord และ Ecto มีวิธีจำกัดจำนวนระเบียนที่ส่งคืนโดยคิวรี
limit
ของ Ecto ทำงานคล้ายกับ 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: "[email protected]", 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"
ใน 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: "[email protected]", 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 จะโหลด invoices
เชื่อมโยงเมื่อดึงข้อมูลผู้ใช้ (ดังที่เห็นในแบบสอบถาม SQL สองรายการที่แสดง)
Ecto
อย่างที่คุณอาจสังเกตเห็นแล้วว่า 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: "[email protected]",
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: "[email protected]",
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 | 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) |
สรุป
มีแล้ว:การเปรียบเทียบระหว่างแอปเปิ้ลกับส้มที่สำคัญ
ActiveRecord มุ่งเน้นไปที่ความง่ายในการสืบค้นฐานข้อมูล ฟีเจอร์ส่วนใหญ่เน้นไปที่คลาสโมเดลเอง ไม่ต้องการให้นักพัฒนามีความเข้าใจอย่างลึกซึ้งเกี่ยวกับฐานข้อมูล หรือผลกระทบของการดำเนินการดังกล่าว ActiveRecord ทำหลายอย่างโดยปริยายโดยปริยาย แม้ว่าวิธีนี้จะทำให้เริ่มต้นได้ง่ายขึ้น แต่ก็ทำให้เข้าใจสิ่งที่เกิดขึ้นเบื้องหลังได้ยากขึ้น และจะได้ผลก็ต่อเมื่อคุณปฏิบัติตาม "วิธี ActiveRecord"
ในทางกลับกัน Ecto ต้องการความชัดเจนซึ่งส่งผลให้โค้ดมีรายละเอียดมากขึ้น ข้อดีคือ ทุกอย่างอยู่ในความสนใจ ไม่มีอะไรอยู่เบื้องหลัง และคุณสามารถระบุวิธีการของคุณเองได้
ทั้งสองมีข้อดีขึ้นอยู่กับมุมมองและความชอบของคุณ เมื่อเปรียบเทียบแอปเปิ้ลกับส้มแล้ว เราก็มาถึงจุดสิ้นสุดของ BAT-tle นี้ เกือบลืมบอกชื่อรหัสของ BatGirl (1989 - 2001) คือ .... Oracle แต่ขอไม่ไปในที่ 😉