Câu hỏi "Tôi nên sử dụng ORM nào" thực sự nhắm vào phần nổi của tảng băng khổng lồ khi nói đến chiến lược truy cập dữ liệu tổng thể và tối ưu hóa hiệu suất trong một ứng dụng quy mô lớn.
Thiết kế và bảo trì cơ sở dữ liệu
Điều này là, bởi một biên độ rộng, là yếu tố quyết định quan trọng nhất của thông lượng của một ứng dụng hoặc trang web dựa trên dữ liệu và thường bị các lập trình viên bỏ qua hoàn toàn.
Nếu bạn không sử dụng các kỹ thuật chuẩn hóa phù hợp, trang web của bạn sẽ bị tiêu diệt. Nếu bạn không có khóa chính, hầu hết mọi truy vấn sẽ chậm. Nếu bạn sử dụng các kiểu chống nổi tiếng như sử dụng bảng cho Cặp giá trị khóa (Thực thể-giá trị thuộc tính AKA) mà không có lý do chính đáng, bạn sẽ làm nổ số lần đọc và ghi vật lý.
Nếu bạn không tận dụng các tính năng mà cơ sở dữ liệu cung cấp cho bạn, chẳng hạn như nén trang, FILESTREAM
lưu trữ (đối với dữ liệu nhị phân), SPARSE
cột, hierarchyid
cho phân cấp, v.v. (tất cả các ví dụ về SQL Server), thì bạn sẽ không thấy bất cứ nơi nào gần hiệu suất mà bạn có thể được nhìn thấy.
Bạn nên bắt đầu lo lắng về chiến lược truy cập dữ liệu của mình sau khi bạn đã thiết kế cơ sở dữ liệu của mình và tự thuyết phục bản thân rằng nó tốt nhất có thể, ít nhất là trong thời điểm hiện tại.
Háo hức so với tải nhanh
Hầu hết các ORM đã sử dụng một kỹ thuật gọi là lười tải cho các mối quan hệ, có nghĩa là theo mặc định, nó sẽ tải một thực thể (hàng bảng) tại một thời điểm và thực hiện một chuyến đi khứ hồi tới cơ sở dữ liệu mỗi khi nó cần tải một hoặc nhiều liên quan (nước ngoài phím) hàng.
Đây không phải là điều tốt hay xấu, nó phụ thuộc vào những gì thực sự sẽ được thực hiện với dữ liệu và mức độ bạn biết trước. Đôi khi lười tải là hoàn toàn đúng đắn. NHibernate, ví dụ, có thể quyết định không truy vấn bất cứ điều gì cả và chỉ cần tạo proxy cho một ID cụ thể. Nếu tất cả những gì bạn cần là chính ID, tại sao nó phải yêu cầu nhiều hơn? Mặt khác, nếu bạn đang cố in một cây của mỗi phần tử trong hệ thống phân cấp 3 cấp, thì việc tải nhanh trở thành thao tác O (N²), điều này cực kỳ tệ cho hiệu suất.
Một lợi ích thú vị khi sử dụng "SQL thuần" (nghĩa là các truy vấn / thủ tục lưu trữ ADO.NET thô) là về cơ bản, nó buộc bạn phải suy nghĩ chính xác dữ liệu nào là cần thiết để hiển thị bất kỳ màn hình hoặc trang cụ thể nào. ORMs và các tính năng lười biếng nạp không ngăn cản bạn làm điều này, nhưng họ làm cho bạn cơ hội để được ... tốt, lười biếng , và vô tình phát nổ số truy vấn bạn thực hiện. Vì vậy, bạn cần hiểu các tính năng tải háo hức ORM của mình và luôn cảnh giác về số lượng truy vấn bạn gửi đến máy chủ cho bất kỳ yêu cầu trang nào.
Bộ nhớ đệm
Tất cả các ORM chính đều duy trì bộ đệm cấp một, "bộ đệm nhận dạng" AKA, có nghĩa là nếu bạn yêu cầu cùng một thực thể hai lần bằng ID của nó, thì nó không yêu cầu chuyến đi khứ hồi thứ hai và cả (nếu bạn thiết kế cơ sở dữ liệu của mình một cách chính xác ) cung cấp cho bạn khả năng sử dụng đồng thời lạc quan.
Bộ đệm L1 khá mờ trong L2S và EF, bạn phải tin rằng nó hoạt động. NHibernate rõ ràng hơn về nó ( Get
/ Load
vs. Query
/ QueryOver
). Tuy nhiên, miễn là bạn cố gắng truy vấn bằng ID càng nhiều càng tốt, bạn sẽ ổn ở đây. Rất nhiều người quên đi bộ đệm L1 và liên tục tìm kiếm cùng một thực thể nhiều lần bởi một thứ khác không phải ID của nó (tức là trường tra cứu). Nếu bạn cần làm điều này thì bạn nên lưu ID hoặc thậm chí toàn bộ thực thể để tra cứu trong tương lai.
Ngoài ra còn có bộ đệm cấp 2 ("bộ đệm truy vấn"). NHibernate có tích hợp sẵn này. Linq to SQL và Entity Framework đã biên dịch các truy vấn , có thể giúp giảm tải máy chủ ứng dụng khá nhiều bằng cách tự biên dịch biểu thức truy vấn, nhưng nó không lưu trữ dữ liệu. Microsoft dường như coi đây là mối quan tâm của ứng dụng hơn là mối quan tâm truy cập dữ liệu và đây là điểm yếu lớn của cả L2S và EF. Không cần phải nói đó cũng là một điểm yếu của SQL "thô". Để có được hiệu suất thực sự tốt với cơ bản bất kỳ ORM nào khác ngoài NHibernate, bạn cần phải thực hiện mặt tiền bộ nhớ đệm của riêng mình.
Ngoài ra còn có một bộ nhớ cache L2 "mở rộng" cho EF4 đó là ổn , nhưng không thực sự là một sự thay thế bán buôn cho một bộ nhớ cache ứng dụng cấp.
Số lượng truy vấn
Cơ sở dữ liệu quan hệ được dựa trên bộ dữ liệu. Chúng thực sự tốt trong việc tạo ra một lượng lớn dữ liệu trong một khoảng thời gian ngắn, nhưng chúng gần như không tốt về độ trễ truy vấn vì có một lượng chi phí nhất định liên quan đến mỗi lệnh. Một ứng dụng được thiết kế tốt sẽ phát huy những điểm mạnh của DBMS này và cố gắng giảm thiểu số lượng truy vấn và tối đa hóa lượng dữ liệu trong mỗi truy vấn.
Bây giờ tôi không nói là truy vấn toàn bộ cơ sở dữ liệu khi bạn chỉ cần một hàng. Những gì tôi đang nói là, nếu bạn cần Customer
, Address
, Phone
, CreditCard
, và Order
hàng tất cả cùng một lúc nhằm phục vụ một trang duy nhất, sau đó bạn nên hỏi cho họ tất cả cùng một lúc, không thực hiện mỗi truy vấn riêng biệt. Đôi khi điều đó còn tệ hơn thế, bạn sẽ thấy mã truy vấn cùng một Customer
bản ghi 5 lần liên tiếp, đầu tiên để lấy Id
, sau đó Name
, sau đó EmailAddress
, sau đó ... nó không hiệu quả một cách lố bịch.
Ngay cả khi bạn cần thực thi một số truy vấn tất cả hoạt động trên các tập dữ liệu hoàn toàn khác nhau, thì việc gửi tất cả đến cơ sở dữ liệu dưới dạng một "tập lệnh" sẽ vẫn hiệu quả hơn và trả về nhiều tập kết quả. Đó là chi phí bạn quan tâm chứ không phải tổng lượng dữ liệu.
Điều này nghe có vẻ như lẽ thường nhưng thực sự rất dễ để mất dấu vết của tất cả các truy vấn đang được thực hiện trong các phần khác nhau của ứng dụng; Nhà cung cấp thành viên của bạn truy vấn các bảng người dùng / vai trò, hành động Tiêu đề của bạn truy vấn giỏ hàng, hành động Menu của bạn truy vấn bảng sơ đồ trang web, hành động Sidebar của bạn truy vấn danh sách sản phẩm nổi bật và sau đó có thể trang của bạn được chia thành một vài khu vực tự trị riêng biệt. truy vấn riêng các bảng Lịch sử đơn hàng, Đã xem gần đây, Danh mục và Khoảng không quảng cáo và trước khi bạn biết, bạn đang thực hiện 20 truy vấn trước khi bạn thậm chí có thể bắt đầu phân phát trang. Nó chỉ phá hủy hoàn toàn hiệu suất.
Một số khung - và tôi nghĩ chủ yếu là NHibernate ở đây - cực kỳ thông minh về điều này và cho phép bạn sử dụng một thứ gọi là tương lai , xử lý toàn bộ các truy vấn và cố gắng thực hiện tất cả chúng cùng một lúc, vào phút cuối có thể. AFAIK, bạn tự mình làm nếu bạn muốn làm điều này với bất kỳ công nghệ nào của Microsoft; bạn phải xây dựng nó thành logic ứng dụng của bạn.
Lập chỉ mục, dự đoán và dự đoán
Ít nhất 50% các nhà phát triển tôi nói chuyện và thậm chí một số DBA dường như gặp rắc rối với khái niệm bao gồm các chỉ số. Họ nghĩ, "tốt, Customer.Name
cột được lập chỉ mục, vì vậy mọi tra cứu tôi làm trên tên phải nhanh." Ngoại trừ nó không hoạt động theo cách đó trừ khi Name
chỉ mục bao gồm cột cụ thể mà bạn đang tìm kiếm. Trong SQL Server, điều đó được thực hiện với INCLUDE
trong CREATE INDEX
câu lệnh.
Nếu bạn vô tình sử dụng SELECT *
ở mọi nơi - và đó là nhiều hơn hoặc ít hơn mọi ORM sẽ làm trừ khi bạn chỉ định rõ ràng bằng cách sử dụng phép chiếu - thì DBMS rất có thể chọn bỏ qua hoàn toàn các chỉ mục của bạn vì chúng chứa các cột không được bảo hiểm. Một phép chiếu có nghĩa là, ví dụ, thay vì làm điều này:
from c in db.Customers where c.Name == "John Doe" select c
Bạn làm điều này thay vào đó:
from c in db.Customers where c.Name == "John Doe"
select new { c.Id, c.Name }
Và ý chí này, đối với hầu hết ORMs hiện đại, hướng dẫn nó để chỉ đi và truy vấn Id
và Name
các cột được có lẽ bao phủ bởi các chỉ số (nhưng không phải là Email
, LastActivityDate
hoặc bất cứ điều gì khác cột mà bạn tình cờ dính vào trong đó).
Nó cũng rất dễ dàng để thổi bay hoàn toàn mọi lợi ích lập chỉ mục bằng cách sử dụng các vị từ không phù hợp. Ví dụ:
from c in db.Customers where c.Name.Contains("Doe")
... Trông gần giống với truy vấn trước đây của chúng tôi nhưng thực tế sẽ dẫn đến quét toàn bộ bảng hoặc chỉ mục vì nó dịch sang LIKE '%Doe%'
. Tương tự, một truy vấn khác có vẻ đơn giản đáng ngờ là:
from c in db.Customers where (maxDate == null) || (c.BirthDate >= maxDate)
Giả sử bạn có một chỉ mục trên BirthDate
, vị từ này có cơ hội tốt để khiến nó hoàn toàn vô dụng. Lập trình viên giả định của chúng tôi ở đây rõ ràng đã cố gắng tạo ra một loại truy vấn động ("chỉ lọc ngày sinh nếu tham số đó được chỉ định"), nhưng đây không phải là cách đúng để làm điều đó. Viết như thế này thay thế:
from c in db.Customers where c.BirthDate >= (maxDate ?? DateTime.MinValue)
... bây giờ công cụ DB biết cách tham số hóa điều này và thực hiện tìm kiếm chỉ mục. Một thay đổi nhỏ, dường như không đáng kể đối với biểu thức truy vấn có thể ảnh hưởng mạnh đến hiệu suất.
Thật không may, LINQ nói chung làm cho tất cả quá dễ dàng để viết các truy vấn xấu như thế này bởi vì đôi khi các nhà cung cấp có thể đoán những gì bạn đang cố gắng làm và tối ưu hóa truy vấn, và đôi khi chúng không. Vì vậy, bạn kết thúc với kết quả không nhất quán một cách bực bội , điều hiển nhiên rõ ràng (với một DBA có kinh nghiệm, dù sao) nếu bạn chỉ viết SQL cũ đơn giản.
Về cơ bản, tất cả đều xuất phát từ thực tế là bạn thực sự phải theo dõi chặt chẽ cả SQL được tạo và các kế hoạch thực hiện mà chúng dẫn đến, và nếu bạn không nhận được kết quả như mong đợi, đừng ngại bỏ qua Lớp ORM thỉnh thoảng và mã hóa SQL. Điều này áp dụng cho bất kỳ ORM nào , không chỉ riêng EF.
Giao dịch và khóa
Bạn có cần hiển thị dữ liệu hiện tại lên đến mili giây không? Có lẽ - nó phụ thuộc - nhưng có lẽ là không. Đáng buồn thay, Entity Framework không cung cấp cho bạnnolock
, bạn chỉ có thể sử dụng READ UNCOMMITTED
ở cấp độ giao dịch (không phải cấp độ bảng). Trong thực tế, không có ORM nào đặc biệt đáng tin cậy về điều này; nếu bạn muốn đọc bẩn, bạn phải thả xuống cấp độ SQL và viết các truy vấn đặc biệt hoặc các thủ tục được lưu trữ. Vì vậy, những gì nó sôi lên, một lần nữa, là nó dễ dàng như thế nào để bạn làm điều đó trong khuôn khổ.
Entity Framework đã đi một chặng đường dài về vấn đề này - phiên bản 1 của EF (trong .NET 3.5) thật tuyệt vời, khiến cho việc vượt qua sự trừu tượng của các "thực thể" trở nên vô cùng khó khăn, nhưng bây giờ bạn đã có ExecuteStoreQuery và Dịch , vì vậy nó thực sự không tệ lắm. Kết bạn với những anh chàng này vì bạn sẽ sử dụng họ rất nhiều.
Ngoài ra còn có vấn đề về khóa ghi và khóa chết và thực tiễn chung là giữ khóa trong cơ sở dữ liệu trong ít thời gian nhất có thể. Về vấn đề này, hầu hết các ORM (bao gồm Khung thực thể) thực sự có xu hướng tốt hơn SQL thô vì chúng gói gọn đơn vị của mẫu Công việc , trong EF là SaveChanges . Nói cách khác, bạn có thể "chèn" hoặc "cập nhật" hoặc "xóa" các thực thể vào nội dung trái tim của mình, bất cứ khi nào bạn muốn, đảm bảo rằng kiến thức sẽ không bị thay đổi thực sự vào cơ sở dữ liệu cho đến khi bạn cam kết đơn vị công việc.
Lưu ý rằng UOW không giống với giao dịch dài hạn. UOW vẫn sử dụng các tính năng tương tranh lạc quan của ORM và theo dõi tất cả các thay đổi trong bộ nhớ . Không một câu lệnh DML nào được phát ra cho đến khi cam kết cuối cùng. Điều này giữ cho thời gian giao dịch càng thấp càng tốt. Nếu bạn xây dựng ứng dụng của mình bằng SQL thô, sẽ rất khó để đạt được hành vi hoãn lại này.
Điều này có ý nghĩa gì đối với EF cụ thể: Làm cho các đơn vị công việc của bạn càng thô càng tốt và không cam kết chúng cho đến khi bạn thực sự cần. Làm điều này và bạn sẽ kết thúc với sự tranh chấp khóa thấp hơn nhiều so với việc bạn sử dụng các lệnh ADO.NET riêng lẻ vào các thời điểm ngẫu nhiên.
EF hoàn toàn tốt cho các ứng dụng có lưu lượng truy cập cao / hiệu suất cao, giống như mọi khung công tác khác đều tốt cho các ứng dụng có lưu lượng cao / hiệu suất cao. Điều quan trọng là làm thế nào bạn sử dụng nó. Dưới đây là so sánh nhanh về các khung phổ biến nhất và các tính năng mà chúng cung cấp về hiệu suất (chú thích: N = Không được hỗ trợ, P = Một phần, Y = có / được hỗ trợ):
Như bạn có thể thấy, EF4 (phiên bản hiện tại) không quá tệ, nhưng có lẽ nó không phải là tốt nhất nếu hiệu suất là mối quan tâm chính của bạn. NHibernate đã trưởng thành hơn rất nhiều trong lĩnh vực này và thậm chí Linq to SQL cung cấp một số tính năng nâng cao hiệu suất mà EF vẫn không có. ADO.NET thô thường sẽ nhanh hơn cho các kịch bản truy cập dữ liệu rất cụ thể , nhưng, khi bạn đặt tất cả các phần lại với nhau, nó thực sự không mang lại nhiều lợi ích quan trọng mà bạn nhận được từ các khung khác nhau.
Và, chỉ để đảm bảo hoàn toàn chắc chắn rằng tôi nghe giống như một bản ghi bị hỏng, không có vấn đề nào trong một chút nếu bạn không thiết kế cơ sở dữ liệu, ứng dụng và chiến lược truy cập dữ liệu của mình đúng cách. Tất cả các mục trong biểu đồ trên là để cải thiện hiệu suất vượt quá mức cơ bản; hầu hết thời gian, đường cơ sở là những gì cần cải thiện nhất.