Năm tính năng của Active Record bạn nên sử dụng
Bài đăng này đã không được cập nhật trong 8 năm
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