+1

Năm tính năng của Active Record bạn nên sử dụng

http://jakeyesbeck.com/2015/11/15/five-active-record-features-you-should-be-using/

Trong một ứng dụng Ruby in Rails, nhiệm vụ của Active Record là giao tiếp, trao đổi với persistence layer. Sử dụng Active Record một cách hiệu quả sẽ giúp bạn cải thiện một cách tốt nhất mã code trong ứng dụng của bạn.

Trong Rails 4.0, chúng ta có một số thay đổi trong Active Record. Hiểu được những thay đổi đó, và làm thế nào để sử dụng chúng một cách tốt nhất là những điều quan trọng mà bất cứ một Rails developer nào cũng cần biết.

Để hỗ trọ cho chủ để này, chúng ta có thể giả định một ứng dụng Rails "booksandreviews.com" với 3 models 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

Những người thông minh tại "booksandreviews.com" cần biết trạng thái data của họ để tạo ra một thước đo(review trung bình của cuốn sách đó) và thực hiện việc bán hàng. Để có thể tính được lượng review của một cuốn sách nào đó thì cần phải có data mới thực hiện được và Active Record sẽ được sử dụng để fetch data đó.

1. Nested Queries

Khi thực thi các truy vấn đến cơ sở dữ liệu thì càng ít truy vấn càng tốt. Kể từ khi Active Record đảm nhận nhiệm vụ trung chuyển các queries tới persistence layer, một điều quan trọng là chắc chắn rằng nó có tất cả mọi sự trợ giúp mà nó cần. Đối với những queries đơn giản thì nó không phải là vấn đề gì cả, nhưng đối với những yêu cầu cao hơn, phức tạp hơn có thể làm ứng dụng trở nên chậm chạp, thậm chí mất cả phút để truy vấn được queries đó => vì vậy chúng ta cần tối ưu hóa queries đó.

Một ngày, Tim từ phòng bán hàng đi kèm với một sự tức giận quan văn phòng và chỉ ra rằng chúng ta phải có một lỗi trong hệ thống. Những cuốn sách rao bán gần đây trên "booksandreviews.com" không được tốt và anh ấy muốn câu trả lời. Và Tim muốn phân tích những gì đang xảy ra. Anh ấy muốn tất cả review được published trong ngày hôm nay, cho tất cả những cuốn sách được published năm 2015.

Nếu không có quá nhiều suy nghĩ thì cách tiếp cận này có vẻ hợp lý:

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)

Thứ nhất, nó sẽ lấy ra những cuốn sách mong muốn, lấy ra id của những cuốn sách đó và pass kết quả đó vào Review query. Không những nó generate ra 2 queries, nó còn lãng phí một số bộ nhớ bởi việc tạo ra một mảng của Book objects để map nó và sau đó là một mảng book_ids. Với một số lượng đủ lớn của books, nó có thể là một trong những nguyên nhân có nghiêm trọng.

Method where của Active Record sẽ trả về một instance của ActiveRecord::Relation. Relations object đó có thể được pass vào những method khác để phục vụ cho việc xây dựng các truy vấn. Đối với truy vấn như trên, chúng ta có thể tiết kiệm được map method và không cần tạo array:

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')

Nó vẫn thực thi 2 lệnh SELECT, nhưng nó cho database chăm sóc việc cấp phát bộ nhớ và tối ưu hóa. Mảng book_ids đã được thay thể bởi books relation và pass nó vào Review query.

Note: Nó có thể được tối giản bằng 1 query với lệnh .join, nhưng bây giờ chúng ta có thể giải định rằng một truy vấn lồng nhau là mong muốn

2. DRY Scopes

Vẫn chưa hài lòng với kết quả, Tim có nhu cầu muốn biết thêm nhiều thông tin hơn. Bây giờ anh ấy muốn biết danh sách tất cả cuốn sách được published trong năm 2015 mà có ít nhất 1 Review được approved. Kể từ khi Reviews là những chủ đề, họ cần được approved(phê duyệt) nhằm duy trì chất lượng mà "booksandreviews.com" được biết đến.

May mắn thay chúng ta có một scope được viết trong Review để thực hiện điều này.

class Review < ActiveRecord::Base
  belongs_to :book

  scope :approved, ->{ where(approved: true) }
end

Tuy nhiên, cái chúng ta muốn trả về là Books chứ không phải là Reviews. Bằng việc thay thế định nghĩa scope, một join query có thể sử dụng cho yêu cầu này:

books = Book.where(publish_year: '2015')
            .joins(:reviews)
            .where('reviews.approved = ?', true )
            .to_a
# => SELECT "books".* FROM "books" INNER JOIN "reviews" ON "reviews"."book_id" = "books"."id" WHERE "books"."publish_year" = "2015" AND "reviews"."approved" = "t"

Books được trả về với chi phí nhân đôi approved scope. Nó có nghĩa là scope trong Review thay đổi, và code đó không có ích gì từ việc thay đổi đó.

Định nghĩ Don't Repeat Yourself(DRY) đã được tạo ra chính vì lý do đó. Khi mà các mã code giống hệt nhau không được chia sẻ và thay vào đó là tiếp tục nhắc lại, thay ddổi cho một phiên bản có thể có những hậu quả nghiêm trọng khác.

Một thông tin tốt là Active Record có cung cấp loại thuốc cho căn bệnh này đó là: .merge.

Với .merge thì scope đã tồn tại có thể được sử dụng trong Active Record query khác.

books = Book.where(publish_year: '2015')
            .joins(:reviews)
            .merge(Review.approved)
            .to_a
# => SELECT "books".* FROM "books" INNER JOIN "reviews" ON "reviews"."book_id" = "books"."id" WHERE "books"."publish_year" = "2015" AND "reviews"."approved" = "t"

Tốt, bây giờ kết quả trả về giống như vậy và code đã DRY.

3. Where.not

Yêu cầu của Tim không dừng lại, anh ấy muốn add thêm một yêu cầu mới "tất cả sản phẩm không được hiện thị trên bảng điều khiển". Bây giờ, anh ấy muốn biết tất cả những cuốn sách không được published năm 2012.

Thậm chí có thể không cần hỏi tại sao yêu cầu ngớ ngẩn đó là cần thiết, chúng ta có thể viết chúng như sau:

books = Book.where('publish_year != 2012').to_a
# => SELECT "books".* FROM "books" WHERE (publish_year != '2012')

Giống như những cái trên, code trên hoạt động, nhưng mà không tốt. Ở đây chúng ta đã sử dụng một số mã raw SQL. Điều đó có nghĩa là có thể một số developer tiếp theo có thể sẽ không hiểu tốt để tiếp thục thao tác với nó.

Để giúp giải quyết tình trạng khó xử này, .not được xây dựng trong Active Record 4.0.

books = Book.where.not(publish_year: 2012).to_a
# => SELECT "books".* FROM "books" WHERE (publish_year != '2012')

Kết quả thì như nhau, nhưng mà trông nó đẹp hơn rất nhiều. Không còn có mã SQL trong code, thay vào đó code trông khả quan hơn.

4. first and take

"booksandreviews.com" được xây dựng từ năm 2012, rất có thể nó được chuyển đổi từ Rails 3.0 sang 4.0. Một trong những thay đổi từ Active Record 3 sang 4 là hành vi .first.

Trong Rails 4.0+, method .first trả về hàng đầu tiên của bảng sau khi nó được order theo id.

Author.where(first_name: 'Bill').first
# => SELECT  "authors".* FROM "authors" WHERE "authors"."first_name" = "Bill" ORDER BY "authors"."id" ASC LIMIT 1

Nó hoạt động đúng cho tất cả các bảng có cột id. Tuy nhiên nếu bảng đó không cần trường id, thì method đó có thể gây ra một vấn đề.

Mặc dù mỗi Author đều có id riêng, nhưng những lệnh join phức tạp có thể gây ra một vấn đề với những lệnh ORDER BY được ẩn bên trong query.

Để giảm bớt các vấn đề đó, take method có thể phù hợp hơn so với first method.

Author.where(first_name: 'Bill').take
# => SELECT  "authors".* FROM "authors" WHERE "authors"."first_name" = "Bill" LIMIT 1

Hoạt động này rõ ràng hơn rất nhiều, cũng trả về một thông tin nhưng mà không đi kèm với order mặc định.

5. .unscoped

Trong suốt quá trình xây dựng "booksandreviews.com", đã có vô số các module được xây dựng và các gems được thêm vào. Giữa sự hỗn loạn này, một người nào đó cần phải gõ vào lệnh gem install hairball và khủng khiếp thay, nó đã thay đổi class Author. Điều này làm cho một người mới như Mike's không hiểu rằng dẫn đến một khiếu nại từ một tác giả mới Mike's rằng: "Authors are missing data".

Mike biết rằng mỗi tác giả đều có trường first_name nhưng vì một số lý do nào đó mà nó đã không được trả về:

authors = Author.where(last_name: 'Smith').take(5)
authors.map(&:first_name)
# => [nil, nil, nil, nil, nil]

Điều mà Mike không biết đó là gem hairball đã add thêm vào default scope cho tất cả các đối tượng Active Record mà nó bắt đầu bằng chữ cái "A". Điều này mất của Mike 1 ngày để tìm ra được nguyên nhân.

Điều mà Mike cần làm là .unscoped method. Method này sẽ remove tất cả các scope trong Active Record.

authors = Author.unscoped.where(last_name: 'Smith').take(5)
authors.map(&:first_name)
# => ['Frank', 'Frank', 'Jim', 'Frank', 'Frank']

Với .unscoped method, tất cả các default scopes có hại đều bị removed.

Queries on Queries

Với 5 kĩ thuật(có thể là nhiều hơn),Active Record query sẽ trở lên DRY hơn và khả quan hơn. Bạn có thể xem danh sách đầy đủ những gì Active Record cung cấp tại Rails Giudes.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí