12/08/2018, 17:13

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/

0