12/08/2018, 14:40

Rails Database Best Practices (Phần 2)

Link phần 1: https://viblo.asia/raincatcher/posts/OREGwQLQklN Phần này, chúng ta sẽ tiếp tục tìm hiểu thêm 1 số phương pháp để tăng hiệu quả khi làm việc với cơ sở dữ liệu Rule #3: Reduce calls to the Database ActiveRecord cung cấp API để dễ dàng làm rất nhiều việc với database của chúng ta, ...

Link phần 1: https://viblo.asia/raincatcher/posts/OREGwQLQklN Phần này, chúng ta sẽ tiếp tục tìm hiểu thêm 1 số phương pháp để tăng hiệu quả khi làm việc với cơ sở dữ liệu

Rule #3: Reduce calls to the Database

ActiveRecord cung cấp API để dễ dàng làm rất nhiều việc với database của chúng ta, nhưng đồng thời nó cũng dễ dàng khiến chúng ta sử dụng những công cụ đó 1 cách không hiệu quả. Các layer abstraction sẽ ẩn đi những gì đang thực sự diễn ra.

Một trong những sự thiếu hiệu quả là sự gia tăng của số lượng truy vấn đến database. Ví dụ với việc sử dụng 1 local database và các dataset nhỏ, khi chúng ta đẩy chúng lên production, số lượng truy vấn có thể tăng lên hàng chục lần hoặc thậm chí là nhiều hơn. Và lúc này request sẽ trở nên cực kì chhhhhậậậậậmmmmmmmmm.

Nếu 1 trang web thường xuyên được truy cập, việc giảm bớt một số lượng nhỏ truy vấn cũng là điều nên bỏ thời gian để thực hiện.

Trong nhiều trường hợp, chúng ta chỉ cần dùng .includes() hoặc joins(). Đôi khi chúng ta sẽ phải dùng .group(), .having() hoặc 1 số method khác. Đặc biệt có 1 số trường hợp chúng ta phải viết trực tiếp câu truy vấn.

Thế còn Caching? Sure, caching là 1 cách khác để tăng tốc độ load trang, nhưng sẽ tốt hơn nếu chúng ta loại bỏ các truy vấn không hiệu quả ngay từ đầu.

Rule #4: Use Indexes

Database chỉ có thể tìm kiếm nhanh chóng cho các cột được gắn index, nếu không nó sẽ quét tuần tự qua tất cả các bản ghi. Việc đánh index sẽ giúp tăng tốc độ truy vấn lên rất nhiều

Việc thêm index cũng rất đơn giản. Trong Rails migration:

class SomeMigration < ActiveRecord::Migration
  def change
    # Specify that an index is desired when initially defining the table.
    create_table :memberships do |t|
      t.timestamps             null: false
      t.string :status,        null: false, default: 'active', index: true
      t.references :account,   null: false, index: true, foreign_key: true
      t.references :club,      null: false, index: true, foreign_key: true
      # ...
    end
    
    # Add an index to an existing table.
    add_index :payments, :billing_period
    
    # An index on multiple columns.
    # This is useful when we always use multiple items in the where clause.
    add_index :accounts, [:provider, :uid]
  end
end

Rule #5: Use Query Objects for complicated Queries

Tôi nhận thấy các scope tốt nhất là đơn giản. Chúng nên được tái sử dụng. Khi cần xử lí 1 yêu cầu phức tạp, ta có thể sử dụng 1 lớp truy vấn để đóng gói các truy vấn. Dưới đây là 1 ví dụ:

# A query that returns all of the adults who have signed up as volunteers this year,
# but have not yet become a pta member.
class VolunteersNotMembersQuery
  def initialize(year:)
    @year = year
  end

  def relation
    volunteer_ids  = GroupMembership.select(:person_id).school_year(@year)
    pta_member_ids = PtaMembership.select(:person_id).school_year(@year)

    Person
      .active
      .adults
      .where(id: volunteer_ids)
      .where.not(id: pta_member_ids)
      .order(:last_name)
  end
end

Trông có vẻ như chúng ta tạo ra rất nhiều truy vấn, nhưng thực tế không phải vậy. Dòng 9-10 định nghĩa relations. Chúng được sử dụng ở dòng 15-16. Đây là kết quả với 1 truy vấn duy nhất:

SELECT people.*
FROM people
WHERE people.status = 0
  AND people.kind != "student"
  AND (people.id IN (SELECT group_memberships.person_id FROM group_memberships WHERE group_memberships.school_year_id = 1))
  AND (people.id NOT IN (SELECT pta_memberships.person_id FROM pta_memberships WHERE pta_memberships.school_year_id = 1))
ORDER BY people.last_name ASC

Rule #6: Avoid ad-hoc queries outside of Scopes and Query Objects

Tôi không nhớ đã nghe nó lần đầu tiên ở đâu, tuy nhiên điều này đã gây khó khăn cho tôi trong 1 thời gian dài: "Hạn chế dùng các building method(e.g: .where, .group, .joins, .not, etc) cho các scope và querry object ".

Điều đó có nghĩa là, đóng gói dữ liệu vào scope và query object, thay vì xây dựng các truy vấn trong services, controllers, tasks, etc.

Tại sao? Bởi vì 1 truy vấn được đặt trong controller (or view, task, etc) là rất khó để test một cách độc lập và không thể tái sử dụng. Đảm bảo nguyên tắc này sẽ giúp cho code của chúng ta dễ hiểu và dễ maintain.

0