Computer >> Máy Tính >  >> Lập trình >> Ruby

Cách chế ngự bảng người dùng ngày càng tăng của bạn

Khách hàng đến đó bằng cách nào?

Trước khi đi sâu vào chi tiết, hãy cố gắng hiểu cách một ứng dụng có thể kết thúc ở trạng thái này. Chúng tôi bắt đầu với một users đơn giản bàn. Sau một vài tuần, chúng tôi cần xác định được thời gian đăng nhập cuối cùng để chúng tôi thêm users.last_sign_in_at . Sau đó, chúng tôi cần biết tên của người dùng. Chúng tôi thêm first_namelast_name . Xử lý Twitter? Một cột khác. Hồ sơ GitHub? Số điện thoại? Sau một vài tháng, chiếc bàn trở nên đáng kinh ngạc.

Chuyện này bị sao vậy?

Một bảng lớn chỉ ra một số vấn đề:

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

Ứng dụng được tìm nạp User trên mọi yêu cầu cho mục đích xác thực và ủy quyền nhưng thường chỉ được sử dụng một số cột. Khắc phục sự cố sẽ cải thiện cả thiết kế và hiệu suất.

Trích xuất bảng

Chúng tôi có thể giải quyết vấn đề bằng cách trích xuất các cột hiếm khi được sử dụng sang một bảng (hoặc các bảng) mới . Ví dụ:chúng tôi có thể trích xuất thông tin hồ sơ (first_name , v.v.) vào profiles với các bước sau:

  1. Tạo profiles với các cột trùng lặp các cột liên quan đến cấu hình trong users .
  2. Thêm profile_id tới users . Đặt nó thành NULL bây giờ.
  3. Đối với mỗi hàng trong users , chèn một hàng vào profiles trùng lặp các cột liên quan đến hồ sơ.
  4. Điểm profile_id của hàng tương ứng trong users vào hàng được chèn trong 3.
  5. Không không make users.profile_id non- NULL . Ứng dụng chưa biết về sự tồn tại của nó nên nó sẽ bị hỏng.

Chúng tôi cần thay thế các tham chiếu đến users.first_name với profiles.first_name và như thế. Nếu chúng tôi chỉ trích xuất một vài cột với một số ít tham chiếu thì tôi khuyên chúng tôi nên làm điều này theo cách thủ công. Nhưng ngay sau khi chúng ta bắt gặp bản thân nghĩ rằng "Ồ, không. Đây là công việc tồi tệ nhất từ ​​trước đến nay!" chúng ta nên tìm kiếm một giải pháp thay thế.

Đừng bỏ bê vấn đề. Một phần của mã mà mọi người tránh sẽ xấu đi hơn nữa và thậm chí còn bị thiếu chú ý nhiều hơn . Cách dễ nhất để phá vỡ vòng luẩn quẩn là bắt đầu từ quy mô nhỏ.

Hãy đọc tiếp, nếu bạn tò mò về cách khách hàng của tôi giải quyết vấn đề.

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

Cách tiếp cận gia tăng nhất là cố định một tham chiếu đến cột cũ tại một thời điểm. Hãy tập trung vào việc di chuyển first_name từ users tới profiles .

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

rails generate model Profile first_name:string

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

class ExtractUsersFirstNameToProfiles < ActiveRecord::Migration
  # Redefine the models to break dependency on production code. We need
  # vanilla models without callbacks, etc. Also, removing a model in the future
  # might break the 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

Bởi vì nó buộc mỗi người dùng phải có chính xác một hồ sơ, một tham chiếu từ users tới profiles tốt hơn so với tham chiếu ngược lại.

Với cấu trúc cơ sở dữ liệu tại chỗ, chúng ta có thể ủy quyền first_name từ User tới Profile . Khách hàng của tôi có một số yêu cầu:

  1. Người truy cập phải sử dụng Profile được liên kết . Họ cũng phải ghi lại nơi mà trình truy cập không dùng nữa được gọi.
  2. Đang lưu User sẽ tự động lưu Profile để tránh vi phạm mã khi sử dụng các trình truy cập không dùng nữa.
  3. User#first_name_changed?ActiveModel::Dirty khác các phương pháp sẽ vẫn hoạt động.

Điều này có nghĩa là User sẽ trông như thế này:

class User < ActiveRecord::Base
  # We need autosave as the client code might be unaware of
  # Profile#first_name and still reference 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)
 
    # Call super so that User#first_name_changed? and similar still work as
    # expected.
    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)
A reference to an obsolete attribute #{name} at:
#{filtered_backtrace.join("\n")}
END
  end
end

Sau những 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 có thêm các tham chiếu đến Profile (nếu hiệu suất trở thành vấn đề, chỉ cần sử dụng một công cụ như AppSignal). Mã ghi lại tất cả các tham chiếu đến các thuộc tính kế thừa, ngay cả những thuộc tính không thể áp dụng được (ví dụ:user[attr] = ... hoặc user.send("#{attr}=", ...) ) để chúng tôi có thể xác định vị trí của tất cả chúng ngay cả khi grep không hữu ích.

Với cơ sở hạ tầng này, chúng tôi có thể cam kết sửa một tham chiếu đến users.first_name theo lịch trình thường xuyên, ví dụ:vào mỗi buổi sáng (để bắt đầu một ngày với một chiến thắng nhanh chóng) hoặc vào khoảng buổi trưa (để làm việc gì đó dễ dàng hơn sau một buổi sáng tập trung). Cam kết này là cần thiết vì mục tiêu của chúng tôi là giảm bớt rào cản tinh thần để khắc phục sự cố . Để nguyên mã ở trên mà không thực hiện hành động sẽ làm ứng dụng kém đi hơn nữa.

Sau khi xóa tất cả các tham chiếu không dùng nữa (và xác nhận bằng grep và nhật ký) cuối cùng chúng ta có thể loại bỏ users.first_name :

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

Chúng ta cũng nên loại bỏ mã được thêm vào User vì nó không còn cần thiết nữa.

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 lưu ý một số hạn chế của nó:

  • Nó không xử lý các truy vấn hàng loạt như User.update_all .
  • Nó không xử lý các truy vấn SQL thô.
  • Nó có thể phá vỡ các bản vá lỗi khỉ (hãy nhớ rằng các phần phụ thuộc cũng có thể giới thiệu chúng).
  • UserProfile có thể không đồng bộ, nếu profiles.first_name được cập nhật nhưng users.first_name không.

Bạn có thể vượt qua một số trong số chúng. Ví dụ:bạn có thể giữ cho các mô hình được đồng bộ hóa với một đối tượng dịch vụ hoặc một lệnh gọi lại trên Profile . Hoặc nếu bạn sử dụng PostgreSQL, bạn có thể cân nhắc sử dụng chế độ xem cụ thể hóa trong thời gian tạm thời.

Vậy là xong!

Bài học quan trọng nhất của bài viết là đừng tránh mã có mùi mà thay vào đó hãy giải quyết nó . Nếu nhiệm vụ quá sức thì hãy làm việc lặp đi lặp lại theo một lịch trình bình thường. Bài báo đã trình bày a phương pháp để xem xét khi trích xuất một bảng là khó khăn. Nếu bạn không thể áp dụng nó thì hãy tìm thứ khác. Nếu bạn không biết làm thế nào thì chỉ cần gửi cho tôi một dòng. Tôi sẽ cố gắng giúp đỡ. Đừng để các bit của bạn bị thối rữa.