Computer >> Máy Tính >  >> Lập trình >> Ruby

ActiveRecord so với EctoPart Two

Đây là phần thứ hai của loạt phim "ActiveRecord vs. Ecto", trong đó Batman và Batgirl chiến đấu để truy vấn cơ sở dữ liệu và chúng tôi so sánh táo và cam.

Sau khi xem xét lược đồ cơ sở dữ liệu và di chuyển trong phần một của ActiveRecord so với Ecto, bài đăng này trình bày cách cả ActiveRecord và Ecto cho phép các nhà phát triển truy vấn cơ sở dữ liệu và cách cả ActiveRecord và Ecto so sánh khi xử lý các yêu cầu giống nhau. Trên đường đi, chúng ta cũng sẽ tìm ra danh tính của Batgirl 1989-2011.

Dữ liệu hạt giống

Bắt đầu nào! Dựa trên cấu trúc cơ sở dữ liệu được xác định trong bài đầu tiên của loạt bài này, hãy giả sử usersinvoices các bảng có dữ liệu sau được lưu trữ trong đó:

người dùng

id full_name email 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

* created_at của ActiveRecord trường được đặt tên là inserted_at ở Ecto theo mặc định.

hóa đơn

id user_id Payment_method pay_at created_at * updated_at
1 1 Thẻ tín dụng 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 của ActiveRecord trường được đặt tên là inserted_at ở Ecto theo mặc định.

Các truy vấn được thực hiện thông qua bài đăng này giả định rằng dữ liệu ở trên được lưu trữ trong cơ sở dữ liệu, vì vậy hãy ghi nhớ thông tin này khi đọc nó.

Tìm mục bằng khóa chính của nó

Hãy bắt đầu với việc lấy một bản ghi từ cơ sở dữ liệu bằng cách sử dụng khóa chính của nó.

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]
}

So sánh

Cả hai trường hợp đều khá giống nhau. ActiveRecord dựa trên find phương thức lớp của User lớp người mẫu. Có nghĩa là mọi lớp con ActiveRecord đều có find của riêng nó phương pháp trong đó.

Ecto sử dụng một cách tiếp cận khác, dựa trên khái niệm Kho lưu trữ làm trung gian giữa lớp ánh xạ và miền. Khi sử dụng Ecto, User mô-đun không có kiến ​​thức về cách tìm ra chính nó. Trách nhiệm đó có trong Repo , có thể ánh xạ nó tới kho dữ liệu bên dưới, trong trường hợp của chúng tôi là Postgres.

Khi so sánh chính truy vấn SQL, chúng ta có thể nhận ra một vài điểm khác biệt:

  • ActiveRecord tải tất cả các trường (users.* ), trong khi Ecto chỉ tải các trường được liệt kê trong schema định nghĩa.
  • ActiveRecord bao gồm LIMIT 1 vào truy vấn, trong khi Ecto thì không.

Tìm nạp tất cả các mục

Hãy tiến thêm một bước nữa và tải tất cả người dùng từ cơ sở dữ liệu.

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]
  }
]

So sánh

Nó tuân theo mô hình giống hệt như phần trước. ActiveRecord sử dụng all phương thức lớp và Ecto dựa vào mẫu kho lưu trữ để tải các bản ghi.

Một lần nữa có một số khác biệt trong các truy vấn SQL:

  • Giống như phần trước, ActiveRecord tải tất cả các trường (users.* ), trong khi Ecto chỉ tải các trường được liệt kê trong schema định nghĩa.
  • ActiveRecord cũng xác định LIMIT 11 , trong khi Ecto chỉ cần tải mọi thứ. Giới hạn này đến từ inspect phương pháp được sử dụng trên bảng điều khiển (https://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation.rb#L599).

Truy vấn với các điều kiện

Rất ít khả năng chúng ta cần tìm nạp tất cả các bản ghi từ một bảng. Nhu cầu phổ biến là sử dụng các điều kiện để lọc ra dữ liệu được trả về.

Hãy sử dụng ví dụ đó để liệt kê tất cả các invoices vẫn phải trả (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
  }
]

So sánh

Trong cả hai ví dụ, where từ khóa được sử dụng, là kết nối đến WHERE của SQL mệnh đề. Mặc dù các truy vấn SQL được tạo khá giống nhau, nhưng cách cả hai công cụ đến đó có một số khác biệt quan trọng.

ActiveRecord chuyển đổi paid_at: nil đối số cho paid_at IS NULL Câu lệnh SQL tự động. Để đạt được cùng một đầu ra bằng cách sử dụng Ecto, các nhà phát triển cần phải rõ ràng hơn về ý định của họ, bằng cách gọi is_nil() .

Một điểm khác biệt khác cần được làm nổi bật là hành vi "thuần túy" của hàm where ở Ecto. Khi gọi where hoạt động một mình, nó không tương tác với cơ sở dữ liệu. Sự trở lại của where hàm là một 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)>

Cơ sở dữ liệu chỉ được chạm vào khi Repo.all() hàm được gọi, truyền Ecto.Query struct như một đối số. Cách tiếp cận này cho phép thành phần truy vấn trong Ecto, là chủ đề của phần tiếp theo.

Thành phần truy vấn

Một trong những khía cạnh mạnh mẽ nhất của truy vấn cơ sở dữ liệu là thành phần. Nó mô tả một truy vấn theo cách chứa nhiều hơn một điều kiện.

Nếu bạn đang xây dựng các truy vấn SQL thô, điều đó có nghĩa là bạn có thể sẽ sử dụng một số kiểu nối. Hãy tưởng tượng bạn có hai điều kiện:

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

Để kết hợp hai điều kiện đó bằng cách sử dụng SQL thô, có nghĩa là bạn sẽ phải nối chúng bằng cách sử dụng một cái gì đó tương tự như:

SELECT * FROM invoices WHERE #{not_paid} AND #{paid_with_paypal}

May mắn thay, cả ActiveRecord và Ecto đều có giải pháp cho điều đó.

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
  }
]

So sánh

Cả hai truy vấn đều trả lời cùng một câu hỏi:"Paypal đã thanh toán và sử dụng hóa đơn nào?".

Như đã được mong đợi, ActiveRecord cung cấp một cách ngắn gọn hơn để soạn truy vấn (ví dụ như vậy), trong khi Ecto yêu cầu các nhà phát triển chi tiêu nhiều hơn một chút để viết truy vấn. Như thường lệ, Batgirl (Orphan, câm với danh tính Cassandra Cain) hoặc Activerecord không dài dòng như vậy.

Đừng để bị lừa bởi độ dài và độ phức tạp rõ ràng của truy vấn Ecto được hiển thị ở trên. Trong môi trường thế giới thực, truy vấn đó sẽ được viết lại để trông giống như sau:

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

Nhìn từ góc độ đó, sự kết hợp của các khía cạnh "thuần túy" của hàm where , không tự thực hiện các hoạt động cơ sở dữ liệu, với toán tử ống dẫn, làm cho thành phần truy vấn trong Ecto thực sự sạch sẽ.

Đặt hàng

Thứ tự là một khía cạnh quan trọng của truy vấn. Nó cho phép các nhà phát triển đảm bảo rằng một kết quả truy vấn nhất định tuân theo một thứ tự được chỉ định.

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
  }
]

So sánh

Việc thêm thứ tự vào một truy vấn được thực hiện ngay trong cả hai công cụ.

Mặc dù ví dụ Ecto sử dụng Invoice là tham số đầu tiên, order_by hàm cũng chấp nhận Ecto.Query cấu trúc, cho phép order_by chức năng được sử dụng trong các sáng tác, như:

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

Giới hạn

Cơ sở dữ liệu không có giới hạn sẽ là gì? Một thảm họa. May mắn thay, cả ActiveRecord và Ecto đều giúp hạn chế số lượng bản ghi được trả về.

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
  }
]

So sánh

Cả ActiveRecord và Ecto đều có cách giới hạn số lượng bản ghi được trả về bởi một truy vấn.

limit của Ecto hoạt động tương tự như order_by , phù hợp với các thành phần truy vấn.

Hiệp hội

ActiveRecord và Ecto có các cách tiếp cận khác nhau khi nói đến cách xử lý các liên kết.

ActiveRecord

Trong ActiveRecord, bạn có thể sử dụng bất kỳ liên kết nào được xác định trong một mô hình mà không cần phải làm bất kỳ điều gì đặc biệt về điều đó, ví dụ:

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

Ví dụ trên cho thấy rằng chúng tôi có thể nhận được danh sách các hóa đơn của người dùng khi gọi user.invoices . Khi làm như vậy, ActiveRecord tự động truy vấn cơ sở dữ liệu và tải các hóa đơn được liên kết với người dùng. Mặc dù cách tiếp cận này làm cho mọi thứ dễ dàng hơn, theo nghĩa là viết ít mã hơn hoặc phải lo lắng về các bước bổ sung, có thể là một vấn đề nếu bạn đang lặp lại một số người dùng và tìm nạp hóa đơn cho từng người dùng. Sự cố này được gọi là "sự cố N + 1".

Trong ActiveRecord, giải pháp khắc phục được đề xuất cho "N + 1 vấn đề" là sử dụng includes phương pháp:

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

Trong trường hợp này, ActiveRecord tải nhanh các invoices liên kết khi tìm nạp người dùng (như được thấy trong hai truy vấn SQL được hiển thị).

Ecto

Như bạn có thể đã nhận thấy, Ecto thực sự không thích ma thuật hay sự ẩn ý. Nó yêu cầu các nhà phát triển phải rõ ràng về ý định của họ.

Hãy thử cùng một phương pháp sử dụng user.invoices với 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>

Kết quả là một Ecto.Association.NotLoaded . Không hữu ích lắm.

Để có quyền truy cập vào các hóa đơn, nhà phát triển cần cho Ecto biết về điều đó, bằng cách sử dụng preload chức năng:

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
  }
]

Tương tự như ActiveRecord includes , tải trước với tìm nạp các invoices được liên kết , điều này sẽ làm cho chúng khả dụng khi gọi user.invoices .

So sánh

Một lần nữa, trận chiến giữa ActiveRecord và Ecto kết thúc với một điểm đã biết:tính rõ ràng. Cả hai công cụ đều cho phép các nhà phát triển dễ dàng truy cập các liên kết, nhưng trong khi ActiveRecord làm cho nó ít dài dòng hơn, kết quả của nó có thể có các hành vi không mong muốn. Ecto tuân theo kiểu tiếp cận WYSIWYG, chỉ thực hiện những gì được nhìn thấy trong truy vấn do nhà phát triển xác định.

Rails nổi tiếng với việc sử dụng và quảng bá các chiến lược bộ nhớ đệm cho tất cả các lớp khác nhau của ứng dụng. Một ví dụ là về việc sử dụng phương pháp lưu vào bộ nhớ đệm "búp bê Nga", phương pháp này hoàn toàn dựa vào "vấn đề N + 1" để cơ chế bộ nhớ đệm thực hiện phép thuật của nó.

Xác thực

Hầu hết các xác nhận hiện có trong ActiveRecord cũng có sẵn trong Ecto. Dưới đây là danh sách các xác thực phổ biến và cách cả ActiveRecord và Ecto xác định chúng:

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)

Kết thúc

Bạn đã có nó:sự so sánh giữa táo và cam cần thiết.

ActiveRecord tập trung vào việc dễ dàng thực hiện các truy vấn cơ sở dữ liệu. Phần lớn các tính năng của nó tập trung vào chính các lớp mô hình, không yêu cầu các nhà phát triển phải hiểu sâu về cơ sở dữ liệu, cũng như tác động của các hoạt động đó. ActiveRecord thực hiện rất nhiều thứ theo mặc định. Mặc dù điều đó giúp bạn bắt đầu dễ dàng hơn, nhưng việc hiểu những gì đang diễn ra ở hậu trường sẽ khó hơn và nó chỉ hoạt động nếu bạn làm theo "cách ActiveRecord".

Mặt khác, Ecto yêu cầu tính rõ ràng dẫn đến mã dài dòng hơn. Như một lợi ích, mọi thứ đều được chú ý, không có gì đằng sau hậu trường và bạn có thể chỉ định theo cách riêng của mình.

Cả hai đều có mặt trái của chúng tùy thuộc vào quan điểm và sở thích của bạn. Vì vậy, sau khi so sánh táo và cam, chúng ta đi đến phần cuối của BAT-tle này. Suýt quên nói với bạn tên mã của BatGirl (1989 - 2001) là .... Oracle. Nhưng chúng ta không đi sâu vào điều đó. 😉