MongoDB / NoQuery: Giữ lịch sử thay đổi tài liệu


134

Một yêu cầu khá phổ biến trong các ứng dụng cơ sở dữ liệu là theo dõi các thay đổi đối với một hoặc nhiều thực thể cụ thể trong cơ sở dữ liệu. Tôi đã nghe cái này được gọi là phiên bản hàng, bảng nhật ký hoặc bảng lịch sử (tôi chắc chắn có những tên khác cho nó). Có một số cách để tiếp cận nó trong RDBMS - bạn có thể viết tất cả các thay đổi từ tất cả các bảng nguồn vào một bảng duy nhất (nhiều hơn một nhật ký) hoặc có một bảng lịch sử riêng cho mỗi bảng nguồn. Bạn cũng có tùy chọn để quản lý việc đăng nhập mã ứng dụng hoặc thông qua kích hoạt cơ sở dữ liệu.

Tôi đang cố gắng suy nghĩ xem giải pháp cho cùng một vấn đề sẽ như thế nào trong cơ sở dữ liệu NoQuery / tài liệu (cụ thể là MongoDB) và cách giải quyết theo cách thống nhất. Nó sẽ đơn giản như việc tạo số phiên bản cho các tài liệu và không bao giờ ghi đè lên chúng? Tạo các bộ sưu tập riêng cho các tài liệu "thực" so với "đã ghi"? Làm thế nào điều này sẽ ảnh hưởng đến truy vấn và hiệu suất?

Dù sao, đây có phải là một kịch bản phổ biến với cơ sở dữ liệu NoQuery không, và nếu vậy, có một giải pháp chung không?


Bạn đang sử dụng trình điều khiển ngôn ngữ nào?
Joshua Partogi

Chưa quyết định - vẫn mày mò và thậm chí còn chưa quyết định lựa chọn lại mục đích được nêu ra (mặc dù MongoDB trông extrememly khả năng). Tôi đã mày mò với NoRM (C #) và tôi thích một số tên liên quan đến dự án đó, vì vậy nó có vẻ rất có thể là sự lựa chọn.
Phil Sandler

2
Tôi biết đây là một câu hỏi cũ nhưng đối với bất kỳ ai đang tìm kiếm phiên bản với MongoDB, câu hỏi SO này có liên quan và trong ý kiến ​​của tôi với câu trả lời tốt hơn.
AWolf

Câu trả lời:


107

Câu hỏi hay, tôi cũng đang xem xét điều này.

Tạo một phiên bản mới trên mỗi thay đổi

Tôi đã xem qua mô-đun Phiên bản của trình điều khiển Mongoid cho Ruby. Tôi đã không sử dụng nó cho mình, nhưng từ những gì tôi có thể tìm thấy , nó thêm một số phiên bản cho mỗi tài liệu. Các phiên bản cũ hơn được nhúng trong chính tài liệu. Hạn chế lớn là toàn bộ tài liệu được sao chép trên mỗi thay đổi , điều này sẽ dẫn đến rất nhiều nội dung trùng lặp được lưu trữ khi bạn xử lý các tài liệu lớn. Cách tiếp cận này là tốt mặc dù khi bạn xử lý các tài liệu có kích thước nhỏ và / hoặc không thường xuyên cập nhật tài liệu.

Chỉ lưu trữ các thay đổi trong phiên bản mới

Một cách tiếp cận khác là chỉ lưu trữ các trường đã thay đổi trong phiên bản mới . Sau đó, bạn có thể 'làm phẳng' lịch sử của mình để xây dựng lại bất kỳ phiên bản nào của tài liệu. Mặc dù điều này khá phức tạp, vì bạn cần theo dõi các thay đổi trong mô hình của mình và lưu trữ các bản cập nhật và xóa theo cách mà ứng dụng của bạn có thể xây dựng lại tài liệu cập nhật. Điều này có thể khó khăn, vì bạn đang xử lý các tài liệu có cấu trúc chứ không phải các bảng SQL phẳng.

Lưu trữ các thay đổi trong tài liệu

Mỗi lĩnh vực cũng có thể có một lịch sử cá nhân. Tái cấu trúc tài liệu cho một phiên bản nhất định theo cách này dễ dàng hơn nhiều. Trong ứng dụng của bạn, bạn không phải theo dõi rõ ràng các thay đổi, mà chỉ cần tạo một phiên bản mới của tài sản khi bạn thay đổi giá trị của nó. Một tài liệu có thể trông giống như thế này:

{
  _id: "4c6b9456f61f000000007ba6"
  title: [
    { version: 1, value: "Hello world" },
    { version: 6, value: "Foo" }
  ],
  body: [
    { version: 1, value: "Is this thing on?" },
    { version: 2, value: "What should I write?" },
    { version: 6, value: "This is the new body" }
  ],
  tags: [
    { version: 1, value: [ "test", "trivial" ] },
    { version: 6, value: [ "foo", "test" ] }
  ],
  comments: [
    {
      author: "joe", // Unversioned field
      body: [
        { version: 3, value: "Something cool" }
      ]
    },
    {
      author: "xxx",
      body: [
        { version: 4, value: "Spam" },
        { version: 5, deleted: true }
      ]
    },
    {
      author: "jim",
      body: [
        { version: 7, value: "Not bad" },
        { version: 8, value: "Not bad at all" }
      ]
    }
  ]
}

Việc đánh dấu một phần tài liệu là đã bị xóa trong một phiên bản vẫn còn hơi khó xử. Bạn có thể giới thiệu một statetrường cho các phần có thể bị xóa / khôi phục khỏi ứng dụng của bạn:

{
  author: "xxx",
  body: [
    { version: 4, value: "Spam" }
  ],
  state: [
    { version: 4, deleted: false },
    { version: 5, deleted: true }
  ]
}

Với mỗi cách tiếp cận này, bạn có thể lưu trữ một phiên bản cập nhật và làm phẳng trong một bộ sưu tập và dữ liệu lịch sử trong một bộ sưu tập riêng biệt. Điều này sẽ cải thiện thời gian truy vấn nếu bạn chỉ quan tâm đến phiên bản mới nhất của tài liệu. Nhưng khi bạn cần cả phiên bản mới nhất và dữ liệu lịch sử, bạn sẽ cần thực hiện hai truy vấn, thay vì một truy vấn. Vì vậy, việc lựa chọn sử dụng một bộ sưu tập duy nhất so với hai bộ sưu tập riêng biệt sẽ phụ thuộc vào tần suất ứng dụng của bạn cần các phiên bản lịch sử .

Hầu hết câu trả lời này chỉ là một suy nghĩ vẩn vơ trong suy nghĩ của tôi, tôi chưa thực sự thử bất kỳ câu hỏi nào trong số này. Nhìn lại nó, tùy chọn đầu tiên có lẽ là giải pháp dễ nhất và tốt nhất, trừ khi chi phí chung của dữ liệu trùng lặp rất có ý nghĩa đối với ứng dụng của bạn. Tùy chọn thứ hai khá phức tạp và có lẽ không đáng để bỏ công sức. Tùy chọn thứ ba về cơ bản là tối ưu hóa tùy chọn hai và sẽ dễ thực hiện hơn, nhưng có lẽ không đáng để nỗ lực thực hiện trừ khi bạn thực sự không thể đi với tùy chọn thứ nhất.

Mong được phản hồi về vấn đề này và những giải pháp của người khác cho vấn đề này :)


Điều gì về việc lưu trữ deltas ở đâu đó, để bạn phải làm phẳng để có được một tài liệu lịch sử và luôn có sẵn hiện tại?
jpmc26

@ jpmc26 Điều đó tương tự như cách tiếp cận thứ hai, nhưng thay vì lưu các đồng bằng để đến các phiên bản mới nhất, bạn đang lưu các đồng bằng để chuyển sang các phiên bản lịch sử. Cách tiếp cận nào để sử dụng tùy thuộc vào tần suất bạn sẽ cần các phiên bản lịch sử.
Niels van der Nghỉ

Bạn có thể thêm một đoạn về việc sử dụng tài liệu làm chế độ xem vào trạng thái hiện tại của sự vật và có tài liệu thứ hai dưới dạng thay đổi sẽ theo dõi từng thay đổi bao gồm dấu thời gian (giá trị ban đầu cần xuất hiện trong nhật ký này) - sau đó bạn có thể 'phát lại 'đến bất kỳ thời điểm cụ thể nào và ví dụ như tương quan những gì đang diễn ra khi thuật toán của bạn chạm vào nó hoặc xem cách một mục được hiển thị khi người dùng nhấp vào nó.
Manuel Arwed Schmidt

Điều này sẽ ảnh hưởng đến hiệu suất nếu các trường được lập chỉ mục dưới dạng mảng?
DmitriD

@ Tất cả - Bạn có thể vui lòng chia sẻ một số mã để đạt được điều này?
Pra_A

8

Chúng tôi đã thực hiện một phần điều này trên trang web của mình và chúng tôi sử dụng 'Lưu trữ bản sửa đổi trong một tài liệu riêng biệt "(và cơ sở dữ liệu riêng biệt). Chúng tôi đã viết một chức năng tùy chỉnh để trả về các khác biệt và chúng tôi lưu trữ điều đó. Không quá khó và có thể cho phép phục hồi tự động.


2
Bạn có thể vui lòng chia sẻ một số mã xung quanh giống nhau? Cách tiếp cận này có vẻ đầy hứa hẹn
Pra_A

1
@smilyface - Tích hợp Spring Boot Javers là tốt nhất để đạt được điều này
Pra_A

@PAA - Tôi đã hỏi một câu hỏi (khái niệm gần như giống nhau). stackoverflow.com/questions/56683389/ cường Bạn có bất kỳ đầu vào cho điều đó?
smilyface

6

Tại sao không phải là một biến thể trên Cửa hàng thay đổi trong tài liệu ?

Thay vì lưu trữ các phiên bản theo từng cặp khóa, các cặp khóa hiện tại trong tài liệu luôn thể hiện trạng thái gần đây nhất và 'nhật ký' các thay đổi được lưu trữ trong một mảng lịch sử. Chỉ những khóa đã thay đổi kể từ khi tạo sẽ có một mục trong nhật ký.

{
  _id: "4c6b9456f61f000000007ba6"
  title: "Bar",
  body: "Is this thing on?",
  tags: [ "test", "trivial" ],
  comments: [
    { key: 1, author: "joe", body: "Something cool" },
    { key: 2, author: "xxx", body: "Spam", deleted: true },
    { key: 3, author: "jim", body: "Not bad at all" }
  ],
  history: [
    { 
      who: "joe",
      when: 20160101,
      what: { title: "Foo", body: "What should I write?" }
    },
    { 
      who: "jim",
      when: 20160105,
      what: { tags: ["test", "test2"], comments: { key: 3, body: "Not baaad at all" }
    }
  ]
}

2

Người ta có thể có một cơ sở dữ liệu NoQuery hiện tại và cơ sở dữ liệu NoQuery lịch sử. Sẽ có một ETL hàng đêm chạy hàng ngày. ETL này sẽ ghi lại mọi giá trị bằng dấu thời gian, vì vậy thay vì các giá trị, nó sẽ luôn là các bộ dữ liệu (các trường được phiên bản). Nó sẽ chỉ ghi lại một giá trị mới nếu có thay đổi được thực hiện trên giá trị hiện tại, tiết kiệm không gian trong quy trình. Ví dụ, tệp json cơ sở dữ liệu NoQuery lịch sử này có thể trông giống như vậy:

{
  _id: "4c6b9456f61f000000007ba6"
  title: [
    { date: 20160101, value: "Hello world" },
    { date: 20160202, value: "Foo" }
  ],
  body: [
    { date: 20160101, value: "Is this thing on?" },
    { date: 20160102, value: "What should I write?" },
    { date: 20160202, value: "This is the new body" }
  ],
  tags: [
    { date: 20160101, value: [ "test", "trivial" ] },
    { date: 20160102, value: [ "foo", "test" ] }
  ],
  comments: [
    {
      author: "joe", // Unversioned field
      body: [
        { date: 20160301, value: "Something cool" }
      ]
    },
    {
      author: "xxx",
      body: [
        { date: 20160101, value: "Spam" },
        { date: 20160102, deleted: true }
      ]
    },
    {
      author: "jim",
      body: [
        { date: 20160101, value: "Not bad" },
        { date: 20160102, value: "Not bad at all" }
      ]
    }
  ]
}

0

Đối với người dùng của Python (trăn 3+, và lên tất nhiên), có HistoricalCollection đó là một phần mở rộng của đối tượng Collection pymongo của.

Ví dụ từ các tài liệu:

from historical_collection.historical import HistoricalCollection
from pymongo import MongoClient
class Users(HistoricalCollection):
    PK_FIELDS = ['username', ]  # <<= This is the only requirement

# ...

users = Users(database=db)

users.patch_one({"username": "darth_later", "email": "darthlater@example.com"})
users.patch_one({"username": "darth_later", "email": "darthlater@example.com", "laser_sword_color": "red"})

list(users.revisions({"username": "darth_later"}))

# [{'_id': ObjectId('5d98c3385d8edadaf0bb845b'),
#   'username': 'darth_later',
#   'email': 'darthlater@example.com',
#   '_revision_metadata': None},
#  {'_id': ObjectId('5d98c3385d8edadaf0bb845b'),
#   'username': 'darth_later',
#   'email': 'darthlater@example.com',
#   '_revision_metadata': None,
#   'laser_sword_color': 'red'}]

Tiết lộ đầy đủ, tôi là tác giả gói. :)

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.