Vài lưu ý khi sử dụng rails active record
Active record là thư viện tuyệt vời của rails để thao tác với database. Tuy nhiên trong quá trình sử dụng, không ít tình huống mà chính chúng ta cũng chả hiểu tại sao nó có thể như thế được. Nhưng rõ ràng chỉ có bạn code sai chứ code nó không có chạy sai. Sau đây là vài trường hợp như thế mà mình ...
Active record là thư viện tuyệt vời của rails để thao tác với database. Tuy nhiên trong quá trình sử dụng, không ít tình huống mà chính chúng ta cũng chả hiểu tại sao nó có thể như thế được. Nhưng rõ ràng chỉ có bạn code sai chứ code nó không có chạy sai. Sau đây là vài trường hợp như thế mà mình đã gặp phải. Có thể bạn đã biết thừa nhưng xem lại cũng không thừa.
Có 1 đoạn scope đơn giản như sau:
class User < ApplicationRecord scope :test_nil, -> do nil end end
Theo bạn User.test_nil sẽ trả về gì? nil? Tất nhiên là không. Scope nếu trả về nil thì sẽ trả về toàn bộ các bản ghi. Tương tự với User.all. Nhưng mà tự nhiên bạn trả về nil làm gì? Các hàm như find_by, find_by_#{attribute} sẽ trả vè nil nếu không tìm thấy. Hầu hết trường hợp thì mọi thứ chạy ổn nhưng tự nhiên không tìm thấy thì hẹo. Bài học rút ra: Đừng dùng find_by trong scope.
Thỉnh thoảng mình thấy mấy bạn newbie rails code thế này.
User.where("age = ? and rank = ?", age, rank)
Mình thực sự nản mấy vụ thế này, Đúng là nó vẫn chạy ok trong nhưng lúc bình thường, nhưng nếu rank nil thì sao? Chả nhẽ không code an toàn hơn chút được à? Nếu bạn không biết thì phép so sánh với null đúng trong sql sẽ là
select * from users where age = 30 and rank is null;
Chỉ cần đơn giản sửa thành.
User.where(age: age, rank: rank)
Đó vậy là active record sẽ tự động sửa câu truy vấn thành is null nếu cần. Nếu thực sự cần sử dụng sql thuần. Nhớ để ý đến loại DBMS bạn dùng và cân nhắc về sanitize nếu cần. Cái này nếu có thời gian mình sẽ nói thêm sau.
Đôi khi bạn sẽ gặp vụ này trong việc phân trang. Đầu tiên thì bạn code 1 đoạn đơn giản lấy data ra, phân trang (bằng kaminari cũng vẫn bị thôi), show ra. Nhưng bạn phát hiện code dính n+1 query vì nó cần 1 số thông tin khác khi show. Bạn fix nó, đơn giản thôi, dùng includes. Và đây là lúc vấn đề lòi ra. Ví dụ nhé, cho 2 model như dưới đây.
class User < ApplicationRecord has_many :posts end class Post < ApplicationRecord belongs_to :user end
Rồi query
User.joins(:posts).includes(:posts).limit(5).offset(5)
Theo suy nghĩ đơn thuần bạn sẽ nghĩ nó lấy cho bạn 5 User và mang theo đống post kia thôi. Nhưng không, joins sẽ tạo ra 1 bảng mới theo kiểu.
Users | Posts |
---|---|
User1 | Post1 |
User1 | Post2 |
User1 | Post3 |
User2 | Post4 |
User2 | Post5 |
User3 | Post6 |
Vậy là limit sẽ chỉ lấy ra 2 User. Và vì gọi bắt đầu từ User nên nó chỉ trả về 2 user mà bạn không thể hiểu tại sao nó bị vậy. Có thể bạn thấy nó ít khi bị nhưng mà một khi bị thì chả biết tại sao luôn, ngồi ngắm đống data mãi mới ra. Thêm nữa nếu số lượng row ít thì nó ko bị đâu, vì khi đó active record sẽ tách ra thành 2 câu sql, pluck id ra rồi select bằng phép in với câu query thứ 2 nên sẽ không bị sao. Mọi truyện chỉ lòi ra khi lên product hoặc đâu đó tạo ra nhiều dữ liệu.