Sử dụng Eloquent Model của Laravel một cách hiệu quả

Vâng, lại một cái cuối tháng nữa đến rồi đi, lại một bài Viblo nữa cần rặn. Nói chung là cờ bí thì dí tốt, đề tài bí thì cứ dí vào cái gì tương đối quen thuộc với bản thân mà táng. Lấy số lượng người làm PHP trong công ty, nhân với số tháng cần viết report, nhân tiếp với tỉ lệ phần trăm số lượng dự án PHP dùng laravel, nhẩm nhanh trong đầu cũng ra kết quả là "nhiều lắm đếm không nổi". Thế nên thật sự cũng khó có thể biết chính xác được là liệu đã có ai viết về đề tài này chưa (mà cũng chả có tâm đến mức đó), xác suất cao là có người viết rồi. Nhưng thôi, bằng đấy bài viết về cùng một chủ đề, thì chuyện trùng chắc cũng ko phải là vấn đề. Nhìn chung, đây là một bài viết khá hay, về một vấn đề tương đối đơn giản nhưng cũng khá dễ mắc phải nếu như bạn không hiểu rõ lắm về cách hoạt động của Eloquent Model. Bài viết có thể sẽ tương đối có ích cho những ai mới làm việc cùng laravel.

Đầu tiên là source, đây là bài viết gốc Using Laravel's Eloquent Efficiently , nói chung mình sẽ dịch theo ý là chính, nên về mặt câu chữ sẽ có đôi chỗ không thật sát lắm so với source.

Nhìn chung, mình đã từng gặp khá nhiều trường hợp các bạn sử dụng quan hệ của các Eloquent Model trong laravel, và lớp query builder nền tảng của chúng một cách không thực sự hiệu quả. Rất nhiều trong số các trường hợp đó đã xảy ra vì trong laravel, có nhiều cách viết code mang lại cho ta kết quả dữ liệu trả về nhìn khá là giống nhau, phần lớn trong số đó khiến ta có cảm giác như chúng là những cách viết hoàn toàn tương đồng. Nếu như bạn đã có ít nhiều kinh nghiệm làm việc với laravel, hay đã được tiếp cận với framework theo một cách khá toàn diện, hiểu được những gì diễn ra đằng sau mỗi chức năng build-in của framework, thì những vấn đề này cũng không quá phức tạp để nắm bắt. Nhưng nếu bạn chỉ học theo kiểu mì ăn liền, ra được kết quả là được, thì có nhiều trường hợp bạn sẽ khá lúng túng trong việc chọn được cách làm thích hợp nhất.

Phần lớn trong những kịch bản kiểu như vậy, có thể được quy gọn lại thành, chọn kiểu nào trong 2 cách gọi $blog->posts$blog->pots() (Ở đây ta đang có 2 model, Blog và Post, quan hệ một-nhiều, đoạn này lẽ ra cũng chả cần phải thuyết minh)

Trong cách viết thứ nhất, ta đã gọi đến database, lấy tất cả những bản ghi posts có quan hệ tới bản ghi blog, sau đó ta tạo ra một Collection của các model Post, và kết quả trả về chính là Collection này. Trong khi đó , cách viết thứ 2 $blog->posts() sẽ trả về kết quả cho ta là một đối tượng thuộc class Builder, và lúc này này chưa có query nào tới database được thực hiện. Hiểu được sự khác biệt đó rồi, tiếp theo bạn hãy tự hỏi mình, Trong tình huống này, ta cần có thông tin gì trong kết quả trả về? Rõ ràng, nếu như bạn chỉ cần lấy những thông tin cơ bản như tên tuổi, địa chỉ email ... là những attribute được lưu trực tiếp trong database, có thể query lấy ra trực tiếp, thì tội gì phải trả về hẳn Collection ? Ngược lại, nếu như dữ liệu bạn cần không được lưu trực tiếp mà cần qua các bước biến đổi ( ví dụ như ngày tháng, thường là không mấy khi ta lấy nguyên xi như trong database ra), lúc đó , hãy tận dụng mutator hay attribute casting của laravel, và việc trả về Collection của các Model lại là lựa chọn đúng đắn. Nguyên tắc cuối cùng mà bạn cần nhớ, đó là việc thực thi thao tác với dữ liệu trong tầng database thường là nhanh hơn rất nhiều so với tầng Application, thế nên hãy tạo query dữ liệu một cách chi tiết nhất có thể. Ví dụ như, nếu như sau khi lấy về các posts của một blog, nếu bạn tiếp tục cần thêm điều kiện lọc dữ liệu nữa, thì thay vì dùng $blog->posts để rồi sau đó thực hiện hàm where() của Collection, hãy dùng $blog->posts() và tiếp tục thêm điều kiện where() vào trong chuỗi query.

Nguyên tắc chung mà mình muốn đề cập tới trong bài viết này cũng chỉ có thế, dễ và cũng chả có gì mấy phải không? Giờ hãy lấy một vài ví dụ minh họa cho sinh động nào.

Không sử dụng Collection để đếm số lượng bản ghi của đối tượng có quan hệ

Như đã nói ở trên, thao tác trong database nhanh hơn trên tầng application rất nhiều, thế nên khi bạn đơn thuần chỉ muốn đếm số lượng bản ghi, đừng dùng cách viết trả về Collection. Cụ thể trong kịch bản của ta, hãy viết $blog->posts()->count() . Một thí nghiệm benchmark đơn giản, với bộ dữ liệu có khoảng 5000 bản ghi, đã cho kết quả là cách viết trên nhanh hơn so với cách viết trả về Collection khoảng 20 lần đấy.

Đừng lấy về cả Collection nếu như bạn chỉ cần lấy một phần tử của nó

Trường hợp này xảy ra khá nhiều phải không. Bạn cần lấy ra, trong số những bài post của blog đó, chỉ bài viết đầu tiên, hoặc bài viết cuối cùng, hoặc một bài viết có id cụ thể nào đó. Trong trường hợp đó, đừng quen tay mà viết $blog->posts->first() mặc dù bạn có cho rằng nó có vẻ thuận mắt hơn đi chăng nữa. Hãy dùng $blog->posts()->first(). Dùng cái gì thì lấy cái đó thôi.

Đừng dùng "where "trên Collection, hãy dùng nó trên Builder.

Cái này đã nói ở trên rồi, duwois này cụ thể hơn tí thôi. Ví dụ như khi ta viết $blog->posts->where('author', 1) thì trước hết, nó đã lấy ra tất cả các bản ghi posts có quan hệ với bản ghi blog, sau đó , dựng các model Post, đưa vào Collection sau đó duyệt từng phần tử của Collection đó theo điều kiện where() của ta, kết quả trả về lại là một Collection khác nữa. Nói thôi đã thấy dông dài rồi, chưa kể đến chuyện, tất cả những tìm kiếm của ta đều thực hiện trên tầng application, có nghĩa là việc đánh index trong database, trong trường hợp này là hoàn toàn vô nghĩa.

Thay vào đó, khi ta viết $blog->posts()->where('author',1) , tất cả việc tìm kiếm đều được thực hiện trong tầng database, và chỉ trả về tầng application những bản ghi ta cần tìm kiếm, mọi chuyện nhanh hơn rất nhiều.

Đùng dùng "pluck" trên Collection, hãy dùng trên Builder.

Tương tự như trên, việc sử dụng hàm pluck là một kịch bản khá phổ biến trong các dự án laravel. Ví dụ như khi ta cần lấy về một mảng $id => $name của các bài Post trong Blog thôi chẳng hạn. Đến đây chắc các bạn quen lắm rồi phải không, cách viết $blog->posts->pluck('name', 'id') sẽ lấy ra tất cả các posts, tạo thành Model, đưa vào Collection, rồi duyệt từng phần tử của Collection đó. Trong khi đấy, cách viết $blog->posts()->pluck('name', 'id') sẽ thay đổi từ câu query truyền xuống database, và sử dụng SELECT name, id thay vì SELECT *, làm tăng thời gian xử lí hơn rất nhhiều lần.

Hãy dùng Collection, khi bạn cần biến đổi dữ liệu

Thật ra bài viết gốc cũng ko có đoạn này, nhưng để tránh hiểu lầm, rằng lúc nào dùng Collection cũng là không tốt, mạn phép viết thêm mấy dòng này. Chẳng hạn như, bạn cần lấy ra dữ liệu ngày tháng của bài post, và muốn hiển thị cho đẹp. Vì trong database, thường là việc xử lí thay đổi dữ liệu theo tình huống sẽ rất khó khăn, chẳng hạn như hiển thị theo múi giờ ứng với từng user khác nhau, có thể làm được trong database, nhưng mà làm cũng không nhanh gọn gì cho cam. Đằng nào thì ta cũng lôi lên trên tầng application để xử lí, và đằng nào thì laravel cũng đã hỗ trợ cho ta những công cụ hết sức hữu ích như mutator trong Eloquent model rồi. Vậy thì lúc này, để code gọn đẹp, hãy viết code thực hiện biến đổi dữ liệu trong Post model, dùng $blog->posts->date, và ta đã có luôn dữ liệu ngày tháng đẹp đẽ sẵn sàng cho ta sử dụng.