5 phương thức của ActiveRecord mà bạn nên dùng

Đây là bài viết chia sẻ về các hàm trong ActiveRecord, chúng rất hữu ích trong một số trường hợp và sẽ giúp các bạn giảm thiểu tối đa code trong việc viết sql và các hàm do mình tự nghĩ ra. Giả sử chúng ra có 3 model là Books, AuthorsReviews được cài đặt 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. Pluck

Được giới thiệu và đưa vào sử dụng ở phiên bản Ruby on rails 4.0, hàm pluck giúp phân bổ bộ nhớ ở mức tối thiểu khi trả về kế quả từ ActiveRecord. Một trường hợp mà rất tuyệt vời để ta sử dụng pluck đó là khi có một bảng cơ sở dữ liệu có thể dùng ActiveRecord để thao tác và nó có rất nhiều cột. Trong trường hợp này nếu ta trả về một tập đầy đủ của đối tượng có thể gây lãng phí bộ nhớ, chúng ta không nên làm như vậy. Ví dụ trong trường hợp này, chúng ta muốn lấy danh sách book_id ta có thể làm như sau:

Book.where.pluck(:id)
# SELECT "books"."id" FROM "books"
=> [1, 3, 45, ...]

Một yếu tố quan trọng để bạn nhận ra phương thức pluck đó là các giá trị trả về của nó. Không giống như người anh select, pluck không trả về các thể hiện của ActiveRecord. Nhiều tên cột có thể được trả về bằng pluck và có thể nằm trong các Array lồng nhau. Array trả về sẽ sắp xếp các cột theo thứ tự được yêu cầu:

Book.where.pluck(:id, :title)
# SELECT "books"."id", "books"."title" FROM "books"
=> [[1, 'A Title'], [3, 'Another One']]

Có thể không phải lúc nào pluck đượn chọn là sự lựa chọn tốt nhất trong việc lấy nhiều cột. Nếu ta muốn lấy quá nhiều cột bằng phương thức pluck có thể tạo ra một hậu quả không thể lường trước được và hiệu năng khi sử dụng sẽ không tốt.

2. Transaction

Một khía cạnh quan trọng của cơ sở dữ liệu quan hệ là các hành vi mang tính chất phần tử. Khi tạo ra các đối tượng ActiveRecord, chúng ta có thể duy trì việc này thông quan transaction. Ví dụ: nếu một ngoại lệ xảy ra trong quá trình chúng ta tạo hoặc cập nhật tất cả các Reviews của Books, một transaction sẽ giúp chúng ta loại bỏ các ngoại lệ khi chạy.

book = Book.find(1)
book.reviews.each do |review|
  review.meaningful_update!
end

Nếu hàm meaningful_update! bắn ra một ngoại lệ, tất cả các bài Reviews nằm ở trước các bài Reviews lỗi sẽ được cập nhật một cách bình thường nhưng các bài Reviews ở sau nó sẽ không được cập nhật. Với một transaction, nó sẽ xử lí cho chúng ta vấn đề này một cách đơn giản.

ActiveRecord::Base.transaction do
  book = Book.find(1)
  book.reviews.each do |review|
    review.meaningful_update!
  end
end

Sau khi thêm transaction vào, các ActiveRecord lỗi trong quá trình cập nhật sẽ bị bỏ qua và các ActiveRecord khác sẽ được cập nhật bình thường.

3. after_commit

callbacks trong Ruby on Rails là một sự lựa chọn để giải quyết nhiều vấn đề. Các callbacks của ActiveRecord được tạo vào những thời điểm khác nhau tùy thuộc vào hoạt động mà model thể hiện của chúng đang trải qua. Một trường hợp sử dụng khá phổ biến của callbacks đó là xoay quanh các việc cần làm sau việc model tiếp tục tồn tại. Model có thể được lưu trữ, tạo hoặc cập nhật. Ví dụ để thêm 1 cuốn sách mới vào hàng đợi sau khi việc đánh giá nó được lưu:

class Book < ActiveRecord::Base
  after_save :enqueue_for_review

  def enqueue_for_review
    ReviewQueue.add(book_id: self.id)
    Logger.info("Added #{self.id} to ReviewQueue")
  end
end

Chúng ta giả sử rằng ReviewQueue hỗ trợ key/value nhằm mục đích đặt các sách mới vào hàng đợi để các nhà phê bình xem xét. Khi mọi thứ rõ ràng, callback sẽ hoạt động như mong muốn:

Book.create!(
  title: 'A New Book',
  author_id: 3,
  content: 'Blah blah...'
)
#=> Added 4 to ReviewQueue
#=> <Book id: 4, title: 'A New Book' ..>

ReviewQueue.size
#=> 1

Tuy nhiên nếu đoạn code này được gói trong một transactiontransaction lỗi. Book trong trường hợp trên sẽ không tồn tại nhưng nó vẫn tồn tại trong ReviewQueue được chạy bởi redis hay cái gì đó tương tự.

ActiveRecord::Base.transaction do
  Book.create!(
    title: 'A New Book',
    author_id: 3,
    content: 'Blah blah...'
  )

  raise StandardError, 'Something Happened'
end

#=> Added 4 to ReviewQueue
#=> Error 'Something Happened'

ReviewQueue.size
#=> 1

Đoạn mã này đã tạo ra một phần tử trên ReviewQueue cho một Book không tồn tại; tuy nhiên nếu áp dụng after_commit callback, vấn đề này sẽ được giải quyết. Ta thay after_save bằng after_commit:

class Book < ActiveRecord::Base
  after_commit :enqueue_for_review

  # ...
end

Nó cùng là đoạn mã trả về nhưng mong muốn sẽ điều nhiều hơn nữa:

ActiveRecord::Base.transaction do
  Book.create!(
    title: 'A New Book',
    author_id: 3,
    content: 'Blah blah...'
  )

  raise StandardError, 'Something Happened'
end

#=> Error 'Something Happened'

ReviewQueue.size
#=> 0

Tuyệt vời, callback after_commit được chạy ngay sau khi bản ghi được tồn tại trên cơ sở dữ liệu, nó chính xác là những gì bạn muốn.

4. touch

Để cập nhật một cột dấu thời gian duy nhất trên 1 ActiveRecord, touch hỗ trợ tuyệt vời trong việc này. Phương pháp này có thể chấp nhận tên một cột mà bạn muốn cập nhật nằm ngoài đối tượng, ví dụ như updated_at. Nếu bảng Reviews có cột last_viewed_at thuộc lại datetime, touch có thể cập nhật dễ dàng:

class ReviewsController < ApplicationController
  def show
    @review = Review.find(params[:id])
    @review.touch(:last_viewed_at)
  end
end

Nó dẫn đến truy vấn:

UPDATE "reviews"
SET "updated_at" = '2016-03-28 00:43:43.616367',
"last_viewed_at" = '2016-03-28 00:43:43.616367'
WHERE "reviews"."id" = 1

Không giống như việc thiết lập các thuộc tính khác, touch sẽ gọi truy vấn và cập nhật ngay lập tức.

5. changes

Hãy nhìn vào đoạn mã sau:

review = Review.find(1)
review.book_id = 5
review.changes
# => {"book_id"=>[4, 5]}

Nhìn vào kết quả trả về bạn có thể đoán được rằng, changes sẽ lưu những gì thay đổi sau khi bản khi được cập nhật. Một phương thức cũng thường được sử dụng để trả về kết quả trước khi bản ghi thay đổi, đó là _was:

review = Review.find(1)
review.status = 'Approved'
review.status_was
# => 'Pending'

Thật tuyệt vời, đây sẽ là một gợi ý nho nhỏ cho các bạn khi mà Mysql5.7 của chúng đã hỗ trợ kiểu json trong database, ta có thể dử dụng changes_was để lưu logs kiểu json, vì bản thân json cũng không khác Hashchanges trả về là mấy.