Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Làm chủ bảng Users tăng trưởng nhanh: Chiến lược thiết kế sạch, có thể mở rộng

Khách hàng rơi vào tình trạng này như thế nào?

Trước khi đi sâu vào chi tiết, hãy thử hiểu một ứng dụng có thể rơi vào trạng thái này như thế nào. Chúng ta bắt đầu với một bảng users đơn giản. Sau vài tuần, chúng ta cần xác định thời gian đăng nhập cuối cùng nên thêm users.last_sign_in_at. Sau đó cần biết tên người dùng. Thêm first_name và last_name. Nickname Twitter? Thêm cột nữa. Hồ sơ GitHub? Số điện thoại? Sau vài tháng, bảng trở nên khó quản trị.

Vấn đề là gì?

Một bảng lớn như vậy cho thấy một số vấn đề:

  1. User có nhiều trách nhiệm không liên quan. Điều này làm cho nó khó hiểu, thay đổi và kiểm thử hơn.
  2. Trao đổi dữ liệu giữa ứng dụng và cơ sở dữ liệu yêu cầu băng thông thêm.
  3. Ứng dụng cần nhiều bộ nhớ hơn để lưu trữ model cồng kềnh.

Ứng dụng lấy User ở mọi request để xác thực và phân quyền nhưng thường chỉ sử dụng một vài cột. Việc sửa vấn đề này sẽ cải thiện cả thiết kế và hiệu suất.

Tách bảng

Chúng ta có thể giải quyết bằng cách tách các cột ít dùng sang bảng mới (hoặc các bảng mới). Ví dụ, tách thông tin hồ sơ (first_name, v.v.) vào profiles theo các bước:

  1. Tạo profiles với các cột trùng lặp các cột liên quan đến hồ sơ trong users.
  2. Thêm profile_id vào users. Đặt thành NULL tạm thời.
  3. Với mỗi dòng trong users, chèn một dòng vào profiles sao chép các cột liên quan đến hồ sơ.
  4. Trỏ profile_id của dòng tương ứng trong users đến dòng vừa chèn ở bước 3.
  5. Không làm cho users.profile_id thành non-NULL. Ứng dụng chưa biết sự tồn tại của nó nên sẽ bị lỗi.

Chúng ta cần thay thế các tham chiếu đến users.first_name bằng profiles.first_name, v.v. Nếu chỉ tách vài cột với ít tham chiếu, khuyến nghị làm thủ công. Nhưng ngay khi nghĩ "Trời ơi, đây là công việc tồi tệ nhất!" thì nên tìm giải pháp khác.

Đừng bỏ qua vấn đề. Một phần code mà mọi người tránh né sẽ suy giảm thêm và chịu sự bỏ bê lớn hơn. Cách dễ nhất để phá vỡ vòng luẩn quẩn là bắt đầu từ nhỏ.

Đọc tiếp nếu bạn tò mò khách hàng của tôi giải quyết như thế nào.

Sửa code từng dòng một

Cách tiếp cận tăng tiến nhất là sửa một tham chiếu đến cột cũ tại một thời điểm. Hãy tập trung di chuyển first_name từ users sang profiles.

Đầu tiên, tạo Profile với:

rails generate model Profile first_name:string

Sau đó thêm tham chiếu từ users đến profiles và sao chép users.first_name sang profiles:

class ExtractUsersFirstNameToProfiles < ActiveRecord::Migration # Định nghĩa lại model để phá phụ thuộc vào code production. Cần # các model thuần túy không có callback, v.v. Việc xóa model sau này # có thể làm hỏng migration. class User < ActiveRecord::Base; end class Profile < ActiveRecord::Base; end def up add_reference :users, :profile, index: true, unique: true, foreign_key: true User.find_each do |user| profile = Profile.create!(first_name: user.first_name) user.update!(profile_id: profile.id) end change_column_null :users, :profile_id, false end def down remove_reference :users, :profile end end

Vì nó buộc mỗi user phải có đúng một profile, tham chiếu từ users đến profiles ưu tiên hơn tham chiếu ngược lại.

Với cấu trúc DB sẵn sàng, chúng ta có thể ủy thác first_name từ User sang Profile. Khách hàng có một số yêu cầu:

  1. Các accessor nên sử dụng Profile liên kết. Chúng cũng nên ghi log nơi accessor cũ được gọi từ.
  2. Lưu User nên tự động lưu Profile để tránh làm hỏng code dùng accessor cũ.
  3. User#first_name_changed? và các phương thức ActiveModel::Dirty khác vẫn nên hoạt động.

Điều này có nghĩa User nên trông như sau:

class User < ActiveRecord::Base # Cần autosave vì code client có thể không biết # Profile#first_name và vẫn tham chiếu User#first_name. belongs_to :profile, autosave: true def first_name log_backtrace(:first_name) profile.first_name end def first_name=(new_first_name) log_backtrace(:first_name) # Gọi super để User#first_name_changed? và các phương thức tương tự # vẫn hoạt động như mong đợi. super profile.first_name = new_first_name end private def log_backtrace(name) filtered_backtrace = caller.select do |item| item.start_with?(Rails.root.to_s) end Rails.logger.warn(<<-END) Tham chiếu đến thuộc tính lỗi thời #{name} tại: #{filtered_backtrace.join("\n")} END end end

Sau các thay đổi này, ứng dụng hoạt động như cũ nhưng có thể chậm hơn một chút do tham chiếu thêm đến Profile (nếu hiệu suất thành vấn đề, dùng công cụ như AppSignal). Code ghi log tất cả tham chiếu đến thuộc tính cũ, kể cả những cái không grep được (ví dụ user[attr] = ... hoặc user.send("#{attr}=", ...)) nên chúng ta có thể định vị tất cả ngay cả khi grep không giúp được.

Với cơ sở hạ tầng này, chúng ta có thể cam kết sửa một tham chiếu đến users.first_name theo lịch trình định kỳ, ví dụ mỗi sáng (để bắt đầu ngày với một quick win) hoặc trưa (để làm việc nhẹ sau buổi sáng tập trung). Sự cam kết này thiết yếu vì mục tiêu là giảm rào cản tâm lý khi sửa vấn đề. Để code trên mà không hành động sẽ làm nghèo nàn ứng dụng thêm.

Sau khi xóa hết tham chiếu cũ (và xác nhận bằng grep và log) chúng ta cuối cùng có thể xóa users.first_name:

class RemoveUsersFirstName < ActiveRecord::Migration def change remove_column :users, :first_name, :string end end

Chúng ta cũng nên xóa code thêm vào User vì nó không còn cần thiết.

Hạn chế

Phương pháp này có thể áp dụng cho trường hợp của bạn nhưng hãy nhớ một số hạn chế:

  • Không xử lý các truy vấn hàng loạt như User.update_all.
  • Không xử lý truy vấn SQL thô.
  • Có thể làm hỏng monkey-patches (nhớ rằng dependency có thể giới thiệu chúng).
  • User và Profile có thể out-of-sync, nếu profiles.first_name được cập nhật nhưng users.first_name không.

Bạn có thể khắc phục một số hạn chế. Ví dụ, giữ model in-sync bằng service object hoặc callback trên Profile. Hoặc nếu dùng PostgreSQL, có thể cân nhắc materialized view trong giai đoạn trung gian.

Kết thúc!

Bài học quan trọng nhất: đừng tránh code có mùi hôi mà hãy đối mặt trực tiếp. Nếu nhiệm vụ quá lớn, hãy làm lặp lại theo lịch trình định kỳ. Bài viết trình bày một phương pháp cân nhắc khi tách bảng khó khăn. Nếu không áp dụng được thì tìm cách khác. Nếu không biết làm sao thì liên hệ tôi. Sẽ cố gắng giúp đỡ. Đừng để code của bạn thối rữa.