Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

ActiveRecord กับ EctoPart Two

นี่เป็นส่วนที่สองของซีรีส์ "ActiveRecord vs. Ecto" ซึ่ง Batman และ Batgirl ต่อสู้กันเพื่อสืบค้นฐานข้อมูล และเราเปรียบเทียบแอปเปิ้ลกับส้ม

หลังจากตรวจสอบสคีมาฐานข้อมูลและการย้ายข้อมูลใน ActiveRecord กับ Ecto ตอนที่หนึ่ง โพสต์นี้จะกล่าวถึงวิธีที่ทั้ง ActiveRecord และ Ecto ช่วยให้นักพัฒนาสามารถสืบค้นฐานข้อมูล และเปรียบเทียบทั้ง ActiveRecord และ Ecto เมื่อต้องรับมือกับข้อกำหนดเดียวกัน ระหว่างทาง เราจะค้นพบตัวตนของ Batgirl ในปี 1989-2011 ด้วย

ข้อมูลเมล็ดพันธุ์

มาเริ่มกันเลย! ตามโครงสร้างฐานข้อมูลที่กำหนดไว้ในโพสต์แรกของชุดนี้ ถือว่า users และ invoices ตารางมีข้อมูลต่อไปนี้เก็บไว้:

ผู้ใช้

id full_name อีเมล created_at* updated_at
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

* 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: "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]
}

การเปรียบเทียบ

ทั้งสองกรณีมีความคล้ายคลึงกันมาก 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: "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]
  }
]

การเปรียบเทียบ

เป็นไปตามรูปแบบเดียวกันกับส่วนก่อนหน้า 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 หมายความว่าคุณอาจจะใช้การต่อกันบางประเภท ลองนึกภาพคุณมีสองเงื่อนไข:

  1. not_paid = 'paid_at IS NOT NULL'
  2. 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: "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"

ใน 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">]>

ในกรณีนี้ 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: "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 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 แต่ขอไม่ไปในที่ 😉