6996 thắc mắc nho nhỏ trong Ruby và RoR (Phần 1: ActiveRecord)
Chào các bạn, chắc hắn khi mới tiếp xúc với ruby và đặc biệt là khi sử dụng framwork Ruby on Rails, chắc hẳn sẽ thấy nó rất tiện lợi với nhiều hàm hỗ trợ, ngoài ra còn có nhiều gem giúp cho một task nào đó được thực hiện nhanh chóng, tuy nhiên, khi càng đi sâu vào thì mới thấy Rails nó rắc rối vãi ...
Chào các bạn, chắc hắn khi mới tiếp xúc với ruby và đặc biệt là khi sử dụng framwork Ruby on Rails, chắc hẳn sẽ thấy nó rất tiện lợi với nhiều hàm hỗ trợ, ngoài ra còn có nhiều gem giúp cho một task nào đó được thực hiện nhanh chóng, tuy nhiên, khi càng đi sâu vào thì mới thấy Rails nó rắc rối vãi saucepan. Vì vậy ở bài này mình sẽ giải thích một số khái niệm mà người mới lập trình với ruby và Rails hay nhầm lẫn. “Các ngươi muốn của cải của ta ư? Nếu muốn các ngươi hãy ra biển mà tìm. Ta để hết ngoài biển đấy! ” Và ở đây mọi thứ đều có nguồn ở Internet :v, ở phần này mình sẽ nói về các hàm trong Active Record trước.
Thằng này thì ai cũng biết, ai cũng dùng nhưng không phải ai cũng hiểu. Trước khi nói những vấn đê tiếp theo, thì mình sẽ nói sơ qua về thằng này trước khi nói các vấn đề tiếp theo. Trong phần này chúng ta cùng tìm hiểu nhiều điều thú vị và hữu ích hơn về Active Record, tìm hiểu ActiveRecord thực sự trả về cái gì và tìm hiểu làm cách nào để làm cho câu truy vấn của bạn hiệu quả hơn
Relations và Lazy Evaluation
Để bắt đầu chúng ta cùng xem qua các hàm tìm kiếm quen thuộc trong model mà chúng ta hay sử dụng:
User.find(1) => #<Use id: 1, name: "lam nhay khong em"....> User.where(id: [1,2]) => [#<User id: 1, name: "toi ben di anh"....>, #<User id: 1, name: "hiep hai a nhe"....>] User.find(3,4) => [#<User id: 3, name: "met qua e oi"....>, #<User id: 1, name: "yeu sl"....>]
Nhìn vào ví dụ trên ai cũng thấy hàm find thứ nhất sẽ trả về cho chúng ta một đối tượng của class User trong model, và ở hàm where và hàm find thứ hai đều trả về giống nhau, có vẻ như chúng đều trả về một mảng các object của class User nhỉ, nhưng thực sự chúng có phải là mảng hay không, chúng ta cùng xem thử nhé.
User.where(id: [1,2]).class => User::ActiveRecord_Relation User.find(3,4).class => Array
Ồ, vi diệu nhỉ. Lúc mới học, mình cũng bị ngộ nhận về vấn đề này, nhìn vào thì cứ tưởng là mảng cơ và lướt qua nó, tuy nhiên khi tìm hiểu sâu hơn thì mình mới xuất hiện nhiều điều thú vị (và đây cũng là lý do mình thấy Rails nó rắc rối vãi ra). Ok, bây giờ chúng ta cùng để ý thằng Relation nhé, khi tìm hiểu về relation và Active Record trong Rails, mình có thấy một câu như thế này Active Record queries return relations to be lazy , theo chị google trên sờ lết "Truy vấn Active Record trả về mối quan hệ lười biếng", chuối bm ra. Thế nên mình xin phép để nguyên văn câu tiếng anh. Uhm, nhưng tại sao lại là lazy. Chúng ta cùng trả lời câu hỏi khi nào thì chúng ta thực sự cần thực thi câu lệnh truy vấn ? Điều gì xảy ra khi bạn viết các câu lệnh truy vấn quá phức tạp ? Relation sẽ giúp bạn giải quyết vấn đề đó. Relations chỉ thực hiện khi chúng ta thực sự cần xem gì đó ở bên trong chúng, ví dụ khi chúng ta có trái ba na na mà chưa muốn ăn thì k việc gì phải bóc vỏ nó ra cả, relations cũng tương tự như vậy, không thực thi câu lệnh truy vấn nếu như chúng ta chưa sử dụng kết quả của câu truy vấn. Để hiểu rõ chúng ta cùng xem ví dụ sau nhé.
class ThangDepTraiController < ApplicationController def index @em_con_nho_hay_em_da_quen = User.limit(6) @em_quen_roi = User.limit(9) # viet the nay bi loi convention ma thoi cung ke binding.pry end end
<h1><%= @em_con_nho_hay_em_da_quen.first.inspect %><h1> <% binding.pry %>
Ở vi dụ trên, ở controller mình có 2 biến instance là @em_con_nho_hay_em_da_quen và @em_quen_roi, đều dùng để lấy ra các user ở trong db, tuy nhiên khi ở view, mình chỉ sử dụng biến @em_con_nho_hay_em_da_quen. Vậy thực sự điều mình muốn nói qua ví dụ này là gì ? Hãy chú ý đến hai vị trí mà mình đặt debug, đều nằm ở cuối tất cả. Khi debug, mình chú ý đến câu lệnh sql mà nó thực thi, khi debug ở controller, ta thấy ở màn hình console log không có câu lệnh sql nào được thực thi cả. Và khi debug đến breakpoint ở view, mình cũng chỉ thấy có một câu lệnh sql được thực thi, trong khi ở controller mình gọi tới 2 lần hàm limit, what the hợi is going on ?
Khi chúng ta sử dụng câu lệnh ***@em_con_nho_hay_em_da_quen = User.limit(6)***, thì thứ trả về view thực sự là một instance của lớp Relation (mình tạm gọi là relation) chứ không phải là một mớ dữ liệu đã được query như chúng ta tưởng, tưởng tượng rằng controller ném về cho view một relation, khi ở view, nếu muốn hiển thị dữ liệu ra cho người dùng, thì relation sẽ thực hiện việc truy vấn csdl để lấy dữ liệu ra, còn biến instance @em_quen_roi không dùng đến thì sẽ k thực hiện truy vấn, giảm đi sự thừa thãi không đáng có. Cơ chế hoạt động như vậy được gọi là Lazy Evaluation. Khái niệm ngược với Lazy evaluation là Eager evaluation, còn hay được gọi là Strict evaluation, ở đây mình chỉ giải thích cách hoạt động của relation trong Rails, còn muốn tìm hiểu Lazy Evaluation là gì thì search Google giúp mình nhé. Vậy túm váy ở đây, khi sử dụng các hàm của lớp Realtion trong Rails, sẽ giúp cho chúng ta tăng hiệu suất bằng cách tránh các câu query không cần thiết, và đừng lầm tưởng như mình rằng cau truy vấn dữ liệu nào cũng trả về cho chúng ta một mảng các object model nhé. Muốn biết hàm nao trong ActiveRecord trả về cho chúng ta một relation thì vui lòng hỏi bác GU giúp mình.
Trước khi vào phần này mình có một lưu ý, các hàm của class ActiveRecord::FinderMethods không trả về object của class ActiveRecord::FinderMethods, có thể ví dụ các hàm như find, find_by, take, first, last đều trae về các object model, nghĩa là chỉ cần gọi đến hàm đó, thì câu lệnh sql sẽ được thực thi ngay lập tức, k theo cơ chế Lazy Evaluation. Có thể nói khi mới học Rails, mình bắt đầu mù mắt với các hàm tìm kiếm của ActiveRecord. Ta thấy rằng các hàm đó đều dùng để tìm kiếm bản ghi trong cơ sở dữ liệu. Vậy chúng khác nhau như thế nào, chúng ta cùng xem:
- find: ắt hẳn khi search google thì ai cũng biết nó thực hiện tìm kiếm theo id, nếu tìm kiếm thất bại thì nó sẽ trả về một Exception là ActiveRecord::RecordNotFound, vậy tìm đúng thì sao, bạn đoán là nó sẽ trả về một object model như find_by hay là một mảng các object model. Ban đầu mình chỉ lướt qua thằng này, tuy nhiên sau khi tìm hiểu kỹ thì thấy nó khá là thú vị. Với kiểu đối số truyền vào như thế nào thì nó sẽ cho kết quả như thế ấy, có 2 kiểu đối số chúng ta có thể truyền vào ở đây là số interger/chuỗi có dạng number(vd: "69") hoặc là mảng các số integer/mảng chuỗi có dạng number(vd: "69","96"). Với trường hợp đối số truyền vào là 1 số thì sẽ trả về 1 object, còn đối số truyền vào là môt mảng số thì kết quả trả về là một mảng các object. Và câu truy vấn ở 2 trường hợp này cũng sẽ khác nhau.
//Find the client with primary key (id) 69 client = Client.find(69) // Câu truy vấn SQL khi thực hiện lệnh trên --> SELECT * FROM clients WHERE (clients.id = 10) LIMIT 1 //> #<Client id: 69, first_name: "Vac cay qua nui" // Find the clients with primary keys 69 and 96. client = Client.find([69, 96]) // Or even Client.find(69, 96) // Câu truy vấn khi thực hiện lệnh SELECT * FROM clients WHERE (clients.id IN (69,96)) // => [#<Client id: 69, first_name: "Vac cay qua nui">, #<Client id: 96, first_name: "La han day xe bo">]
- find_by: thực hiện tìm tiếm với điều kiện nào đó, nếu tìm thấy, trả về 1 object, nếu không tìm thấy thì trả về nil.
- find_by!: tương tự với find_by, tuy nhiên nếu không tìm thấy thì sẽ trả về Exception ActiveRecord::RecordNotFound
- where: cũng thực hiện với tìm kiếm với điều kiện nào đó, nếu tìm thấy sẽ trả về một relation model, nếu không tìm thấy sẽ trả về realtion rỗng.
Ở đây mình cũng thấy rối rối, tự dưng có find_by mà không có find! @@, đây cũng có thể là bẫy để người phỏng vấn đưa ta vào tròng.
Theo kiến thức anh văn 15 năm ở trường thì mình có thể dịch được empty nghĩa là trống và blank nghĩa là rỗng, hoặc ngược lại đều như nhau. Vậy chúng có gì khác nhau mà trong Rails phải định nghĩa hai hàm làm gì cho mất công vại. Ban đầu khi liếc mắt đôi ghèn qua, chúng ta sẽ nghĩ nó là hàm kiểm tra mảng rỗng dùng cho mảng thôi mà. Nhưng dùng kính lúp vạch lá tìm bug kĩ hơn một chút, thật ra khi sử dụng hai hàm này kết hợp với hàm where trong Active Record so vơi dùng cho các mảng thì khác nhau một trời một vực. Để hiểu hai hàm trên thực thi như thế nào, chúng ta cùng xem ví dụ sau:
User.where(admin: true).empty? (0.6ms) SELECT COUNT(*) FROM `users` WHERE `users`.`admin` = 1 => false User.where(admin: true).blank? User Load (0.9ms) SELECT `users`.* FROM `users` WHERE `users`.`admin` = 1 => false
Trong ví dụ trên, hàm empty? thực hiện việc đếm số bảng ghi thỏa mản điều kiện ở mệnh đề where, trong khi đó, hàm blank? thực hiện lấy tất cả các bảng ghi sau đó chỉ đơn thuần là kiểm tra mảng các bảng ghi sau khi thực hiện câu lệnh where có rỗng hay không mà thôi. Điều này có nghĩa là hàm empty? nhanh hơn hàm blank? trong trường hợp trên. Tuy nhiên, với trường hợp các bảng ghi đã được loaded thì sao ? Cùng xem ví dụ dưới đây:
admins = User.where(admin: true).load User Load (1.0ms) SELECT `users`.* FROM `users` WHERE `users`.`admin` = 1 admin.blank? => false admin.empty? => false
Như vậy ở trường hợp này, cả hai hàm chỉ đơn thuần là kiểm tra mảng có rỗng hay không mà thôi, vì vậy ở đây, hiệu suất của cả 2 thằng là như nhau. Như vậy tổng kết lại, chúng ta có thể thấy tùy vào từng trường hợp để chúng ta đưa ra khi nào thì sử dụng câu lệnh nào. Ấy là với lập trình, còn lựa chọn bạn tình thì tùy duyên thôi, nhiều đứa còn éo có mà lựa nữa kia (như mình chẳng hạn