12/08/2018, 14:29

Rails Database Best Practices (Phần 1)

Thường thì chúng ta bắt đầu code với mục đích "chạy là ổn", để sau 1 thời gian làm việc, khi nhìn lại thì chúng ta nhận ra 1 đống vấn đề càn giải quyết. Tôi cũng đã trải qua 1 khoảng thời gian cần phải tăng tốc các trang web với database rất tồi tệ. Đó cũng là nguyên nhân để tôi có bài viết này. ...

Thường thì chúng ta bắt đầu code với mục đích "chạy là ổn", để sau 1 thời gian làm việc, khi nhìn lại thì chúng ta nhận ra 1 đống vấn đề càn giải quyết. Tôi cũng đã trải qua 1 khoảng thời gian cần phải tăng tốc các trang web với database rất tồi tệ. Đó cũng là nguyên nhân để tôi có bài viết này.

Rule #1: Hãy để database của bạn làm đúng việc của nó

Về cơ bản, database của chúng ta thực sự nhanh và mạnh khi được sử dụng đúng cách. Nó làm tốt hơn Ruby trong hầu hết những việc như filter, sort, ... .Nếu để database làm điều đó, nó sẽ làm theo cách nhanh hơn hẳn so với điều tương tự trong ruby, hoặc bất cứ ngôn ngữ nào khác.

Bạn có thể phải tìm hiểu 1 chút về cách database làm việc, nhưng thật sự mà nói, bạn không cần phải đi quá sâu để gặt hái được thành quả đâu.

Vậy, quy định đầu tiên là: hãy để database làm những việc nó thực sự làm tốt, thay vì làm những việc đó trong Ruby

Rule #2: Viết các scope hiệu quả và chainable

Các scope giúp chúng ta truy cập vào các subset của data với các điều kiện cụ thể. Đáng tiếc là tính hữu dụng của nó có thể bị kiềm chế bởi 1 vài thứ "anti-patterns". Chúng ta hãy thử kiểm tra scope .active trong ví dụ sau:

class Client < ActiveRecord::Base
  has_many :projects
end

class Project < ActiveRecord::Base
  belongs_to :client

  # Please don't do this...
  scope :active, -> {
    includes(:client)
      .where(active: true)
      .select { |project| project.client.active? }
      .sort_by { |project| project.name.downcase }
  }
end

Ở đây có 1 vài thứ chúng ta cần quan tâm:

  • Scope không trả về 1 ActiveRecord Relation, do đó nó không có khả năng kết nối và không thể dùng được với merge()
  • Scope filter từ 1 tập dữ liệu lớn xuống 1 tập dữ liệu nhỏ hơn trong ruby
  • Scope thực hiện sort trong ruby
  • Scope thực hiện sort như 1 giai đoạn của chính nó
  1. Trả về 1 ActiveRecord::Relation

Tại sao trả về 1 Relation lại tốt hơn? Bởi vì 1 Relation có khả năng kết nối. 1 chainale scope dễ dàng sử dụng lại. Chúng ta có thể combine nhiều scope trong 1 câu query khi chúng trả về Relation

  1. Filter data trong database (không phải trong Ruby)

Tại sao filter trong ruby lại là 1 ý tưởng tồi? Bởi vì nó thực sự chậm so với việc filter trong database. Đối với lượng bản ghi nhỏ có thể sẽ không có nhiều sự khác biệt. Nhưng đối với số record lớn thì nó thực sự trở thành vấn đề.

Filter trong Ruby chậm hơn do thời gian được sử dụng để chuyển toàn bộ data sang server. ActiveRecord phải phân tích kết quả của query và tạo các AR model object

Database của chúng ta sử dụng index, Ruby thì không.

  1. Sort trong database (không phải trong Ruby)

Về phần sort, nó sẽ làm nhanh hơn trong database, tuy nhiên trừ khi chúng ta làm việc với lượng record lớn, còn không thì nó sẽ không có nhiều khác biệt. Tuy nhiên có 1 vấn đề lớn hơn là .sort_by sẽ trigger query và do đó, chúng ta sẽ mất Relation.

  1. Tách riêng các scope:

Các scope nên được tách ra để đảm bảo chỉ thực hiện các yêu cầu nhỏ, nhằm dễ dàng cho việc tái sử dụng sau này và tăng tính linh hoạt.

Vậy, chúng ta sẽ viết lại scope trên như sau:

class Client < ActiveRecord::Base
  has_many :projects

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

class Project < ActiveRecord::Model
  belongs_to :client

  scope :active, -> {
    where(active: true)
      .joins(:client)
      .merge(Client.active)
  }

  scope :ordered, -> {
    order('LOWER(name)')
  }
end

(To be continue)

0