Top 7 sai lầm thường gặp về LINQ dev .NET hay mắc

Nếu bạn đã từng thấy ứng dụng .NET chạy chậm dù code trông rất gọn, khả năng cao thủ phạm nằm ở LINQ. Bên mình đã review không ít đoạn code như vậy. Bảy sai lầm dưới đây lặp lại nhiều nhất, và mỗi cái đều có cách sửa khá đơn giản.

Đây là danh sách rút ra từ việc xem code review trong nhiều dự án, chứ không phải số liệu khảo sát chính thức. Bạn nên đối chiếu với dự án của mình và đo thực tế trước khi tối ưu.

Trả lời nhanh: 7 sai lầm LINQ cần tránh

Nói ngắn gọn, 7 sai lầm gồm: chạy lại truy vấn nhiều lần, nhầm lẫn lazy evaluation, N+1 query với Entity Framework, xử lý sai FirstOrDefault, lạm dụng LINQ trong vòng lặp nóng, dùng Count() thay vì Any(), và viết truy vấn khó đọc. Bài này chọn chúng theo hai tiêu chí: tần suất gặp trong code review và mức ảnh hưởng tới hiệu năng hoặc độ đúng của chương trình.

Đối tượng phù hợp là lập trình viên .NET mới đến trung cấp, đang dùng LINQ mỗi ngày nhưng chưa rõ vì sao một số đoạn code lại chạy chậm hoặc cho kết quả lạ.

Vì sao chọn đúng 7 mục này

Chúng tôi không cố liệt kê mọi thứ có thể sai. Thay vào đó, chúng tôi chỉ giữ những lỗi lặp đi lặp lại. Lý do đơn giản là một người mới học thường gặp đúng những lỗi này trong tháng đầu làm dự án thật. Nếu bạn nắm được bảy điểm này, phần lớn vấn đề LINQ hằng ngày sẽ được che chắn.

Người làm backend và người tối ưu truy vấn Entity Framework nên ưu tiên đọc kỹ các mục 3, 1 và 6. Còn lại thì bất kỳ ai viết LINQ đều có thể gặp.

Danh sách này không thay thế việc đo đạc. Một đoạn code trông “xấu” chưa chắc đã là nút thắt. Hãy dùng profiler hoặc logging trước khi sửa.

Chạy một truy vấn nhiều lần mà không hay biết

LINQ không tự lưu kết quả. Khi bạn định nghĩa một truy vấn như var đơn = db.Đơn.Where(x => x.Hoàn thành), thì câu lệnh chưa chạy. Nó chỉ chạy khi bạn duyệt nó. Nếu bạn duyệt đơn ba lần trong một hàm, database sẽ bị hỏi ba lần.

Cách sửa khá đơn giản: khi kết quả cần dùng lại, hãy gọi ToList() hoặc ToArray() một lần. Nhưng đừng materialize mọi thứ theo phản xạ, vì danh sách lớn sẽ ngốn bộ nhớ. Chỉ làm vậy khi bạn thật sự cần tái sử dụng.

Nhầm lẫn giữa lúc định nghĩa và lúc thực thi

Đây là lỗi khiến nhiều người đau đầu. Bạn định nghĩa truy vấn từ một danh sách, sau đó sửa danh sách đó, rồi mới duyệt truy vấn. Kết quả lúc này phản ánh dữ liệu tại thời điểm duyệt, không phải thời điểm bạn viết dòng định nghĩa.

Muốn chốt dữ liệu tại một thời điểm, hãy materialize ngay lúc đó. Ngược lại, nếu muốn kết quả luôn mới nhất, hãy để truy vấn lazy và kiểm soát rõ thời điểm enumerate.

N+1 query với Entity Framework

Đây là lỗi tốn tài nguyên nhất trong các mục. Hãy tưởng tượng bạn lấy danh sách 100 khách hàng, rồi trong vòng lặp lại truy vấn đơn hàng của từng người. Kết quả là 1 câu SQL cho khách hàng và 100 câu cho đơn hàng, tổng cộng 101 lệnh.

Có hai cách sửa phổ biến. Thứ nhất là dùng Include để tải quan hệ trong một lần. Thứ hai là dùng Select để chiếu trước các trường cần dùng, tránh kéo dư dữ liệu. Sau khi sửa, bạn nên kiểm tra số câu SQL thực tế bằng logging hoặc công cụ profiling, đừng tin cảm tính.

Xử lý FirstOrDefault không đúng cách

FirstOrDefault trả về giá trị mặc định của kiểu nếu không tìm thấy. Với int, giá trị đó là 0. Nếu 0 cũng là một giá trị hợp lệ trong nghiệp vụ, bạn sẽ không phân biệt được “có dữ liệu bằng 0” và “không có dữ liệu”.

Hãy dùng kiểu nullable, ví dụ int?, hoặc kiểm tra kết quả một cách tường minh. Đừng để giá trị mặc định âm thầm đi vào logic nghiệp vụ.

Lạm dụng LINQ trong vòng lặp nóng

LINQ tạo ra các đối tượng tạm và delegate. Trong một đường chạy được gọi hàng triệu lần, chi phí đó có thể đáng kể. Khi đó, một vòng for truyền thống có thể nhanh hơn rõ rệt.

Tuy vậy, với phần lớn nghiệp vụ thông thường, sự khác biệt rất nhỏ và code LINQ dễ đọc hơn. Chúng tôi khuyên chỉ chuyển sang vòng for khi profiler chỉ ra đúng đoạn đó là nút thắt.

Dùng Count() khi chỉ cần biết có dữ liệu hay không

Đây là thói quen rất hay gặp: viết danh sách.Count() > 0 để kiểm tra có phần tử không. Với nhiều nguồn, Count() phải duyệt toàn bộ chuỗi để đếm. Trong khi Any() dừng ngay khi tìm thấy phần tử đầu tiên.

Vì vậy, khi chỉ cần biết có hay không, hãy dùng Any(). Chi phí thực tế vẫn phụ thuộc loại nguồn dữ liệu, nên hãy kiểm tra với dữ liệu thật của bạn.

Viết truy vấn khó đọc, khó bảo trì

Chuỗi toán tử dài và lồng nhau rất khó debug. Khi một người khác phải đọc lại đoạn đó, họ dễ bỏ sót điều kiện quan trọng. Trong làm việc nhóm, điều này làm chậm code review và tăng rủi ro lỗi.

Cách cải thiện là tách biến trung gian có tên rõ ràng, đưa điều kiện phức tạp vào hàm riêng, và đừng ngại viết ngắn hơn một chút nếu nó dễ hiểu hơn. Code dễ đọc thường là code dễ sửa lỗi.

So sánh nhanh các sai lầm theo mức ảnh hưởng

Sai lầm Ảnh hưởng chính Mức ưu tiên sửa
Thực thi nhiều lần Hiệu năng Cao
N+1 với EF Hiệu năng và tải database Cao
FirstOrDefault sai Logic nghiệp vụ Cao
Count() thay Any() Hiệu năng Trung bình
Lạm dụng trong vòng lặp nóng Hiệu năng Thấp đến trung bình, cần đo
Truy vấn khó đọc Bảo trì Trung bình

Mức ưu tiên trên mang tính tham khảo. Hãy kiểm chứng với dữ liệu và tải thực tế của dự án trước khi quyết định.

Câu hỏi thường gặp về sai lầm LINQ

LINQ có chậm hơn vòng for không?

Không phải lúc nào cũng vậy. Với nhiều nghiệp vụ, khác biệt không đáng kể so với thời gian chờ database. Chỉ khi profiler chỉ ra đoạn đó là nút thắt thì mới cân nhắc chuyển sang vòng for.

Khi nào nên dùng ToList() trong LINQ?

Khi bạn cần duyệt kết quả nhiều lần, hoặc muốn chốt dữ liệu tại một thời điểm. Nếu chỉ duyệt một lần, không cần materialize.

Làm sao phát hiện N+1 query trong Entity Framework?

Bật logging để xem danh sách câu SQL sinh ra. Nếu thấy một câu chính theo sau bởi hàng loạt câu giống nhau với tham số khác, rất có thể bạn đang gặp N+1.

Nên dùng Any() hay Count() khi chỉ cần kiểm tra có dữ liệu không?

Dùng Any(). Nó dừng sớm và thường tốn ít tài nguyên hơn Count().

Nếu chỉ nên làm một việc sau bài này, hãy kiểm tra lại các truy vấn trong vòng lặp và tìm N+1 bằng logging. Đó thường là chỗ mang lại cải thiện rõ rệt nhất.

Tham khảo thêm: 7 kinh nghiệm thực tế về LINQ trong C# và .NET và so sánh LINQ method syntax và query syntax. Bạn cũng có thể xem 10 sai lầm thường gặp về Entity Framework để hiểu thêm bối cảnh N+1.

Về cách tổ chức code và đặt tên biến để dễ đọc hơn, bạn có thể tham khảo thêm tại patternsforphp.com.