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_name
và last_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 đề:
-
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. - 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.
- Ứ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:
- 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 trongusers
. - Thêm
profile_id
tớiusers
. Đặt nó thànhNULL
bây giờ. - Đối với mỗi hàng trong
users
, chèn một hàng vàoprofiles
trùng lặp các cột liên quan đến hồ sơ. - Điểm
profile_id
của hàng tương ứng trongusers
vào hàng được chèn trong 3. - 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:
- 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. - Đang lưu
User
sẽ tự động lưuProfile
để tránh vi phạm mã khi sử dụng các trình truy cập không dùng nữa. -
User#first_name_changed?
và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).
-
User
vàProfile
có thể không đồng bộ, nếuprofiles.first_name
được cập nhật nhưngusers.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.