Đâ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ử users
và invoices
các bảng có dữ liệu sau được lưu trữ trong đó:
người dùng
id | full_name | created_at * | updated_at | |
---|---|---|---|---|
1 | Bette Kane | [email protected] | 2018-01-01 10:01:00 | 2018-01-01 10:01:00 |
2 | Barbara Gordon | [email protected] | 2018-01-02 10:02:00 | 2018-01-02 10:02:00 |
3 | Cassandra Cain | [email protected] | 2018-01-03 10:03:00 | 2018-01-03 10:03:00 |
4 | Stephanie Brown | [email protected] | 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: "[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]
}
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ê trongschema
đị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: "[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]
}
]
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ê trongschema
đị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:
-
not_paid = 'paid_at IS NOT NULL'
-
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: "[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">]>
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: "[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">]>
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: "[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>
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: "[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
}
]
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 đó. 😉