Five Active Record Features You Should Be Using
Trong một ứng dụng ruby on rails, Active Record có nhiệm vụ trao đổi vớ tầng persitences. Vì vậy nếu chúng ta sử dụng avtice record 1 cách hợp lý và hiệu quả thì sẽ giảm được khá là nhiều code. Trong Ruby on Rails 4.0, có một số sự thay đổi về active cord, Giờ chúng ta đi tìm hiểu xem đó là những ...
Trong một ứng dụng ruby on rails, Active Record có nhiệm vụ trao đổi vớ tầng persitences. Vì vậy nếu chúng ta sử dụng avtice record 1 cách hợp lý và hiệu quả thì sẽ giảm được khá là nhiều code. Trong Ruby on Rails 4.0, có một số sự thay đổi về active cord, Giờ chúng ta đi tìm hiểu xem đó là những thay đổi gì nhé. Giả sử chúng ta có 1 ứng dụng “booksandreviews.com” với các model như sau.
class Book < ActiveRecord::Base belongs_to :author has_many :reviews end class Author < ActiveRecord::Base has_many :books end class Review < ActiveRecord::Base belongs_to :book end
1. Nested Queries
Trong khi thực hiện query, query càng ít càng tốt. Vì active record có nhiệm vụ thực hiện các câu query 1 cách thủ công vì vậy quan trọng là mình phải sử dụng thật nhiều sự trợ giúp nếu cần. Đối với những câu query đơn giản thì chẳng mấy khi chúng ta gặp vấn đề nhưng trong những câu query phức tạp thì chúng ta cần phải tối ưu hơn kết quả trả về.
Vào một ngày nào đó, Tim dựa và các doanh số bán hàng gần đây đã đưa ra giả thuyết rằng có một bug tồn tại trong hệ thống. Theo một số liệu gần đây về daonh số bán hàng hoạt động không được tốt và anh ấy muốn cấu trả lời cho vấn đề này. Tim muốn phân tích các hoạt động của hệ thống. Anh ấy muốn xem kết quả sách được public ngày hôm nay và một số lượng sách được published vào năm 2015
book_ids = Book.where(publish_year: '2015').map(&:id) # => SELECT "books".* FROM "books" WHERE (publish_year = '2015') reviews = Review.where(publish_date: '2015-11-15', book_ids: book_ids).to_a # => SELECT "reviews".* FROM "reviews" WHERE "reviews"."publish_date" = '2015-11-15' AND "reviews"."book_ids" IN (1, 2, 3)
Câu lệnh trên sẽ load ra được các cuốn sach mong muốn, Bước đầu tiên nó lấy ra các ids của sách và truyền các ids đó và câu query Review. Với việc làm trên nó không chỉ phải mất 2 câu query mà nó còn lãng phí tài nguyên bộ nhớ bằng cách tạo ra một dãy đối tượng sách và sau đó lai tạo thêm một dãy các book_ids. Với một lượng sách đủ lớn thì nó có thể gây ra một vấn đề nghiêm trọng.
Câu lệnh where của Active Record trả về một instance của ActiveRecord::Relation. Các quan hệ này có thể được chuyển sang các phương thức khác nhau để hỗi trợ xây dựng câu truy vấn. Với 2 câu query trên chúng ta có thể viết lại như sau
books = Book.where(publish_year: '2015') # => ActiveRecord::Relation reviews = Review.where(publish_date: '2015-11-15', book: books).to_a # SELECT "reviews".* FROM "reviews" WHERE "reviews"."publish_date" = '2015-11-15' AND "reviews"."book_id" IN (SELECT "books"."id" FROM "books" WHERE "books"."publish_year" = '2015')
Chúng ta vẫn phải thực hiện 2 câu truy vấn nhưng ở 2 câu sau bằng cách áp dụng nested query vì vậy tiết kiệm bộ nhớ hơn rất nhiều. Chúng ta đã tiết kiệm được nguyên 1 mảng book_ids không dùng tới.
2. DRY Scopes
Tim yêu cầu thêm nhiều thông tin. Bây giờ anh ấy muốn biết danh sách sách được published năm 2015 và có ít nhất được một người đồng ý. Vì Reviews là chủ quan, họ cần đồng ý để duy trì chất lượng của “booksandreviews.com”.
Thật là may mắn, chúng ta có một scope được viết trong Review class như sau
class Review < ActiveRecord::Base belongs_to :book scope :approved, ->{ where(approved: true) } end
Tuy nhiên, anh ấy cần trả về là sách chứ không phải Reviews. Chúng ta có câu lệnh sau
books = Book.where(publish_year: '2015') .includes(:reviews) .references(:reviews) .where('reviews.approved = ?', true ) .to_a # => SELECT #long books and reviews column select# FROM "books" LEFT OUTER JOIN "reviews" ON "reviews"."book_id" = "books"."id" WHERE "books"."publish_year" = '2015' AND (reviews.approved = 't')
Book được trả về khi scope của approved bị lặp khá là nhiều. Điều này có nghĩa rằng Scope của review thay đổi, đoạn code này không có lợi từ những thay đổi kia. Các methods .includes và .references được sử dụng để duy trì việc trả về Book ( trong trường hợp nhiều reviews thuộc về Book )
Nguyên tắc Don't Repeat Yourself (DRY) được tạo ra là do nguyên tắc này. Khi đoạng code bị lặp đi lặp lại nhiều thì thật là không hay chút nào. Có một tin vui đó là Actice Record có một method để giải quyết vấn đề này đó là .merge
Với .merge một scope được sử dụng lại trong một đoạn truy vấn khác
books = Book.where(publish_year: '2015') .includes(:reviews) .references(:reviews) .merge(Review.approved) .to_a # => SELECT #long books and reviews column select# FROM "books" LEFT OUTER JOIN "reviews" ON "reviews"."book_id" = "books"."id" WHERE "books"."publish_year" = '2015' AND (reviews.approved = 't')
3. where.not
Tiếp theo Tim muốn lấy tất cả các sách không được published năm 2012
Đây là một đoạn code thường được dùng để thực hiện lệnh truy vấn trên
books = Book.where('publish_year != 2012').to_a # => SELECT "books".* FROM "books" WHERE (publish_year != '2012')
Giống trước đây, đoạn code trên có thể hoạt động tốt hơn. Có một số đoạn raw sql rằng các developer trong giai đoạn phát triển không đủ hiểu để quản lý và phát triển. Cho dù bất cứ lý do gì thì tốt nhất là tin tưởng vào trừu tượng thay vì các câu sql thông thường. Để giải quyết vấn đề này .not được sử dụng trong Active Record 4.0
books = Book.where.not(publish_year: 2012).to_a # => SELECT "books".* FROM "books" WHERE (publish_year != '2012')
4. first and take
Có một sự thay đổi đáng chú ý ở Rails 4 so với Rails 3 đó là sự suất hiện của hàm .first
Trong Ruby on Rails 4.0+, hàm .first trả về dòng đầu tiên sau khi table được sắp xếp bởi ids
Author.where(first_name: 'Bill').first # => SELECT "authors".* FROM "authors" WHERE "authors"."first_name" = "Bill" ORDER BY "authors"."id" ASC LIMIT 1
Đoạn code trên sẽ làm việc tốt đối với những table có cột id. Tuy nhiên, nếu một bảng không có cột id đoạn code trên gây ra một vấn đề.
Bất chấp mỗ Author có một id, những cách joins phức tạp có thể gây ra vấn đề sắp xếp.
Để giảm bớt vấn đề trên, hàm take được sử dụng để thay thế first:
Author.where(first_name: 'Bill').take # => SELECT "authors".* FROM "authors" WHERE "authors"."first_name" = "Bill" LIMIT 1
Tham khảo
http://jakeyesbeck.com/2015/11/15/five-active-record-features-you-should-be-using/