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

ActiveRecord cho cơ sở dữ liệu không có id duy nhất

Đôi khi những tình huống độc đáo và những thứ ngoài tầm kiểm soát của chúng ta dẫn đến những yêu cầu cực kỳ không chính thống. Gần đây, tôi đã có một trải nghiệm mà tôi cần sử dụng ActiveRecord mà không cần dựa vào ID cơ sở dữ liệu cho bất kỳ bản ghi nào. Nếu bất cứ ai đang xem xét làm điều tương tự, tôi thực sự khuyên bạn nên tìm một cách khác! Nhưng, hãy chuyển sang phần còn lại của câu chuyện.

Các quyết định đã được thực hiện. Cần hợp nhất các cơ sở dữ liệu nhỏ hơn (sao chép về cấu trúc nhưng không có dữ liệu). Tôi tham gia dự án ngay khi nhóm đang hoàn thiện một tập lệnh sao chép và dán các bản ghi cơ sở dữ liệu từ cơ sở dữ liệu này sang cơ sở dữ liệu khác. Nó đã sao chép chính xác mọi thứ, bao gồm cả ID.

Cơ sở dữ liệu A

id quả user_id
... ... ...
123 màu cam 456
... ... ...

Cơ sở dữ liệu B

id quả user_id
... ... ...
123 chuối 74
... ... ...

Cơ sở dữ liệu A sau khi hợp nhất

id quả user_id
... ... ...
123 màu cam 456
123 chuối 74
... ... ...

Điều này phá vỡ lý do cơ bản để có ID:nhận dạng duy nhất. Tôi không biết chi tiết cụ thể, nhưng tôi cảm thấy như tất cả các loại vấn đề sẽ xuất hiện khi các ID trùng lặp được đưa vào hệ thống. Tôi đã cố nói điều gì đó, nhưng tôi là người mới tham gia dự án, và những người khác dường như chắc chắn rằng đây là con đường tốt nhất về phía trước. Trong vài ngày tới, chúng tôi sẽ triển khai mã và bắt đầu xử lý dữ liệu có ID trùng lặp. Câu hỏi không còn là "chúng ta có nên làm điều này không?"; thay vào đó, các câu hỏi là, "làm thế nào để chúng tôi làm điều này?" và "việc này sẽ mất bao lâu nữa?"

Làm việc với các ID trùng lặp

Vì vậy, làm thế nào để bạn xử lý dữ liệu có ID trùng lặp? Giải pháp là tạo một ID tổng hợp của một số trường. Hầu hết các lần tìm nạp DB của chúng tôi trông giống như sau:

# This doesn't work, there may be 2 users with id: 123
FavoriteFruit.find(123)

# Multiple IDs scope the query to the correct record
FavoriteFruit.find_by(id: 123, user_id: 456)

Tất cả các lệnh gọi ActiveRecord đều được cập nhật theo cách này và khi tôi xem qua mã, nó có vẻ hợp lý. Cho đến khi chúng tôi triển khai nó.

Tất cả địa ngục đều tan vỡ

Ngay sau khi chúng tôi triển khai mã, điện thoại bắt đầu đổ chuông. Khách hàng đã nhìn thấy những con số không cộng lại. Họ không thể cập nhật hồ sơ của chính họ. Tất cả các loại tính năng đã bị phá vỡ.

Chúng ta nên làm gì? Chúng tôi không chỉ triển khai mã; chúng tôi cũng đã chuyển dữ liệu từ cơ sở dữ liệu này sang cơ sở dữ liệu khác (và dữ liệu mới đã được tạo / cập nhật sau khi chúng tôi triển khai). Đó không phải là một tình huống quay lui đơn giản. Chúng tôi cần sửa chữa mọi thứ nhanh chóng.

Rails đang làm gì?

Bước đầu tiên trong quá trình gỡ lỗi là xem hành vi hiện tại là gì và cách tạo lại lỗi. Tôi đã sao chép dữ liệu sản xuất và khởi động bảng điều khiển Rails. Tùy thuộc vào thiết lập của bạn, bạn có thể không tự động thấy các truy vấn SQL Rails chạy khi bạn thực thi một truy vấn ActiveRecord. Dưới đây là cách đảm bảo các câu lệnh SQL hiển thị trên bảng điều khiển của bạn:

ActiveRecord::Base.logger = Logger.new(STDOUT)

Sau đó, tôi đã thử một số truy vấn Rails phổ biến:

$ FavoriteFruit.find_by(id: 123, user_id: 456)

FavoriteFruit Load (0.6ms)
SELECT  "favorite_fruits".*
FROM "favorite_fruits"
WHERE "favorite_fruits"."id" = $1
AND "favorite_fruits"."user_id" = $2
[["id", "123"], ["user_id", "456"]]

find_by có vẻ hoạt động tốt, nhưng sau đó tôi thấy một số mã như thế này:

fruit = FavoriteFruit.find_by(id: 123, user_id: 456)
...
...
fruit.reload

reload đó khiến tôi tò mò, vì vậy tôi cũng đã thử nghiệm điều đó:

$ fruit.reload

FavoriteFruit Load (0.3ms)
SELECT  "favorite_fruits".*
FROM "favorite_fruits"
WHERE "favorite_fruits"."id" = $1
LIMIT $2
[["id", 123], ["LIMIT", 1]]

Ồ ồ. Vì vậy, mặc dù ban đầu chúng tôi đã tìm nạp đúng bản ghi bằng find_by , bất cứ khi nào chúng tôi gọi reload , nó sẽ lấy ID của bản ghi và thực hiện một truy vấn đơn giản tìm theo id, tất nhiên, thường sẽ cung cấp dữ liệu không chính xác do các ID trùng lặp của chúng tôi.

Tại sao nó lại làm như vậy? Tôi đã kiểm tra mã nguồn Rails để tìm manh mối. Đây là một khía cạnh tuyệt vời của mã hóa với Ruby on Rails, mã nguồn là Ruby thuần túy và có sẵn miễn phí để truy cập. Tôi chỉ cần truy cập vào "tải lại ActiveRecord" và nhanh chóng tìm thấy điều này:

# File activerecord/lib/active_record/persistence.rb, line 602
def reload(options = nil)
  self.class.connection.clear_query_cache

  fresh_object =
    if options && options[:lock]
      self.class.unscoped { self.class.lock(options[:lock]).find(id) }
    else
      self.class.unscoped { self.class.find(id) }
    end

  @attributes = fresh_object.instance_variable_get("@attributes")
  @new_record = false
  self
end

Điều này cho thấy rằng reload ít nhiều là một trình bao bọc cho self.class.find(id) . Chỉ truy vấn bằng một ID đã được tạo cứng vào phương thức này. Để chúng tôi làm việc với các ID trùng lặp, chúng tôi cần ghi đè các phương thức Rails cốt lõi (không bao giờ được khuyến nghị) hoặc ngừng sử dụng reload hoàn toàn.

Giải pháp của chúng tôi

Vì vậy, chúng tôi quyết định thực hiện mỗi lần reload trong mã và thay đổi nó thành find_by để lấy cơ sở dữ liệu tìm nạp qua nhiều khóa.

Tuy nhiên, đó chỉ là một số lỗi được giải quyết. Sau khi tìm hiểu kỹ hơn, tôi quyết định kiểm tra update của chúng tôi cuộc gọi:

$ fruit = FavoriteFruit.find_by(id: 123, user_id: 456)
$ fruit.update(last_eaten: Time.now)

FavoriteFruit Update (43.3ms)
UPDATE "favorite_fruits"
SET "last_eaten" = $1
WHERE "favorite_fruits"."id" = $2
[["updated_at", "2020-04-16 06:24:57.989195"], ["id", 123]]

Ồ ồ. Bạn có thể thấy điều đó ngay cả khi find_by xác định phạm vi bản ghi theo các trường cụ thể, khi chúng tôi gọi update trên bản ghi Rails, nó đã tạo một WHERE id = x đơn giản truy vấn, cũng bị gián đoạn với các ID trùng lặp. Chúng tôi đã giải quyết vấn đề này như thế nào?

Chúng tôi đã thực hiện một phương pháp cập nhật tùy chỉnh, update_unique , trông giống như sau:

class FavoriteFruit
  def update_unique(attributes)
    run_callbacks :save do
      self.class
        .where(id: id, user_id: user_id)
        .update_all(attributes)
    end
    self.class.find_by(id: id, user_id: user_id)
  end
end

Điều này cho phép chúng tôi cập nhật các bản ghi có phạm vi nhiều hơn ID:

$ fruit.update_unique(last_eaten: Time.now)

FavoriteFruit Update All (3.2ms)
UPDATE "favorite_fruits"
SET "last_eaten" = '2020-04-16 06:24:57.989195'
WHERE "favorite_fruits"."id" = $1
AND "favorite_fruits"."user_id" = $2
[["id", "123"], ["user_id", "456"]]

Mã này đảm bảo một phạm vi hẹp để cập nhật bản ghi, nhưng bằng cách gọi update_all của lớp , chúng tôi đã mất các lệnh gọi lại thường đi kèm với việc cập nhật bản ghi. Do đó, chúng tôi phải chạy thủ công các lệnh gọi lại và thực hiện một lệnh gọi cơ sở dữ liệu khác để truy xuất bản ghi đã cập nhật kể từ update_all không trả về bản ghi đã cập nhật. Sản phẩm cuối cùng không quá lộn xộn, nhưng nó chắc chắn khó đọc hơn fruit.update .

Giải pháp Thực tế

Do chi phí thấp, quản lý và hạn chế về thời gian, giải pháp của chúng tôi là vá Rails thành việc sử dụng nhiều khóa cho tất cả các lệnh gọi cơ sở dữ liệu. Điều này có hiệu quả, theo nghĩa là khách hàng vẫn sẽ mua và sử dụng sản phẩm, nhưng đó là một ý tưởng tồi vì một số lý do:

  • Bất kỳ sự phát triển nào trong tương lai có thể vô tình tạo lại lỗi bằng cách sử dụng các phương pháp Rails phổ biến. Các nhà phát triển mới sẽ cần được đào tạo nghiêm ngặt để giữ cho mã không có lỗi ẩn, chẳng hạn như sử dụng reload phương pháp.
  • Mã phức tạp hơn, kém rõ ràng hơn và ít bảo trì hơn. Đây là nợ kỹ thuật làm chậm tốc độ phát triển ngày càng nhiều khi dự án tiếp tục.
  • Quá trình thử nghiệm bị chậm lại rất nhiều. Bạn không chỉ cần kiểm tra xem một hàm hoạt động mà còn cả nó hoạt động khi các đối tượng khác nhau có ID trùng lặp. Cần nhiều thời gian hơn để viết các bài kiểm tra và sau đó mỗi khi bộ kiểm thử được chạy, cần nhiều thời gian hơn để chạy qua tất cả các bài kiểm tra bổ sung. Việc kiểm tra cũng có thể dễ dàng bỏ sót lỗi nếu mỗi nhà phát triển trong dự án không kiểm tra cẩn thận tất cả các tình huống có thể xảy ra.

Giải pháp thực sự cho vấn đề này là không bao giờ có các ID trùng lặp ngay từ đầu. Nếu dữ liệu cần được chuyển từ cơ sở dữ liệu này sang cơ sở dữ liệu khác, thì tập lệnh làm việc đó sẽ thu thập và chèn dữ liệu mà không có ID, cho phép cơ sở dữ liệu nhận sử dụng bộ đếm tăng tự động được chuẩn hóa của nó để cung cấp cho mỗi bản ghi ID duy nhất của riêng nó.

Một giải pháp khác là sử dụng UUID cho tất cả các bản ghi. Loại ID này là một chuỗi dài các ký tự được tạo ngẫu nhiên (thay vì đếm từng bước như với ID số nguyên). Sau đó, việc di chuyển dữ liệu sang các cơ sở dữ liệu khác sẽ không có xung đột hoặc sự cố.

Điểm mấu chốt là Rails được xây dựng với sự hiểu biết rằng ID là duy nhất cho mỗi bản ghi và một cách nhanh chóng và dễ dàng để thao tác dữ liệu cụ thể trong cơ sở dữ liệu. Rails là một khung công tác được cố định và cái hay của điều này là mọi thứ chạy trơn tru như thế nào, miễn là bạn tuân theo cách thức hoạt động của Rails. Điều này không chỉ áp dụng cho Rails mà còn cho nhiều khía cạnh khác của lập trình. Khi mọi thứ trở nên phức tạp, chúng ta nên biết cách xác định vấn đề; tuy nhiên, nếu chúng ta viết mã rõ ràng, có thể bảo trì và thông thường, chúng ta có thể tránh được nhiều phức tạp này ngay từ đầu.