Tổng quan
Một trợ giúp nhỏ cho các nhà phát triển RDBMS lần đầu tiên làm việc với MongoDB đã phát triển theo yêu cầu thực tế của dự án, giải thích lịch sử và tiêu chuẩn thiết kế để trở thành lớp tiện lợi MongoDB Driver-first trong 1.0.0.
Câu chuyện nguồn gốc
Tôi đã phát triển chủ yếu xung quanh RDBMS trong khoảng 4 năm và cũng đã quen thuộc với môi trường AWS RDS. Khi bắt đầu một dự án cá nhân mới, tôi đã chọn MongoDB lần đầu tiên và nhận ra rằng nó không chỉ là một cơ sở dữ liệu có schema linh hoạt mà còn là một công cụ có thể thay đổi đáng kể năng suất phát triển của tôi.
Đặc biệt, MongoDB Atlas gần như là một thế giới mới đối với tôi. Tôi có thể sử dụng tìm kiếm chuyên nghiệp và tìm kiếm vector trong một nền tảng dữ liệu mà không cần phải gắn thêm hệ thống riêng biệt, và các công việc vận hành như sao lưu, giám sát, mở rộng cũng tiện lợi hơn nhiều so với tiêu chuẩn mà tôi đã quen thuộc với AWS RDS. Nếu cần quy mô và chức năng chuyên nghiệp hơn, tôi có thể tách tìm kiếm hoặc cơ sở dữ liệu vector thành dịch vụ riêng, nhưng trong giai đoạn nhanh chóng tạo và vận hành một dự án, khả năng giải quyết mọi thứ chỉ với Atlas là rất hấp dẫn.
Vấn đề là việc viết truy vấn động đối với tôi, người lần đầu tiên làm việc với MongoDB, rất chậm. Ban đầu, chỉ cần hình thức findBy... của repository là đủ, nhưng khi điều kiện tìm kiếm trở nên phức tạp, tôi phải tự lắp ráp Query, Criteria và các biểu thức toán tử. Trong trạng thái chưa quen thuộc, việc lắp ráp điều kiện lặp đi lặp lại dễ dẫn đến lỗi chính tả, và việc viết mã cùng một hình thức cũng làm giảm tốc độ phát triển rất nhiều.
Bắt đầu từ DSL đơn giản để tra cứu
Vì vậy, điều đầu tiên tôi tạo ra là một lớp nhỏ để đơn giản hóa việc thực hiện R trong CRUD. Đây là điểm khởi đầu của lớp hiện đang được gọi là ReactiveMongoDsl. Lúc đó, không có các chức năng như aggregation, pipeline, lookup, và tôi chỉ cần lắp ráp nhanh hơn một chút các Query, Criteria, paging của Spring Data ReactiveMongoTemplate. Tôi nghĩ rằng việc sử dụng ReactiveMongoTemplate cơ bản hoặc MongoDB Driver trực tiếp là đủ cho các công việc còn lại.
Tuy nhiên, suy nghĩ của một nhà phát triển làm việc cùng tôi thì khác. Anh ấy cũng là người lần đầu tiên làm việc với MongoDB và mỗi khi cần thực hiện các tác vụ như upsert hay bulk, anh ấy đều hỏi tôi xem DSL mà tôi đã tạo có chức năng đó hay không. Tôi có thể đã nói anh ấy sử dụng API cơ bản, nhưng tôi nghĩ rằng anh ấy cũng sẽ gặp phải những khó khăn mà tôi đã trải qua khi lần đầu học MongoDB. Vì vậy, tôi bắt đầu thêm từng chức năng vào ReactiveMongoDsl mỗi khi cần.
Ban đầu, nó khoảng 600 dòng, và khi đạt 1.000 dòng, tôi vẫn nghĩ rằng không cần thiết phải chia thành nhiều lớp. Khi đạt khoảng 2.000 dòng, tôi đã suy nghĩ một chút, nhưng cảm giác rằng cấu trúc để phân tách lại lớn hơn và hơn hết là tôi thực sự thấy phiền phức với công việc đó. Khi vượt qua 5.000 dòng, tôi không thể trì hoãn thêm nữa và đã chuyển một số chức năng có thể tách ra ra ngoài, nhưng đã quá muộn để chia nhỏ các luồng cốt lõi một cách chi tiết. Lý do mà lớp cốt lõi lớn không phải vì tôi đã thiết kế một DSL khổng lồ từ đầu, mà là do lịch sử tích lũy các chức năng cần thiết trong dự án thực tế tại cùng một điểm truy cập.
Sau khi thêm vào các chức năng như tìm kiếm, tổng hợp, lookup, cập nhật nguyên tử, bulk, lịch sử, Tìm kiếm Atlas và Tìm kiếm Vector, tôi nhận ra rằng hầu hết các chức năng mà tôi thường cần khi sử dụng MongoDB trong dự án Java đã có mặt. Thay vì chỉ giữ nó như một trợ giúp nội bộ trong một dự án, tôi đã tách nó ra để có thể sử dụng theo cách tương tự trong nhiều dự án khác, và đó là quá trình ra đời của reactive-mongo-dsl.
Từ trợ giúp Spring đến thư viện Driver-first
Điểm khởi đầu là một trợ giúp để sử dụng ReactiveMongoTemplate dễ dàng hơn, nhưng khi các chức năng tăng lên và bắt đầu tái sử dụng trong nhiều dự án, tôi đã quyết định rằng mô hình thực thi của một framework cụ thể không nên trở thành ranh giới của thư viện. Cốt lõi của 1.0.0 đặt MongoExecutionContext làm hợp đồng thực thi và sử dụng trực tiếp MongoDB Reactive Streams Driver.
Sự thay đổi này không phải để loại bỏ Spring Data MongoDB. Trong ứng dụng Spring, có thể kết nối naming collection của ReactiveMongoTemplate, MongoConverter, chuyển đổi tùy chỉnh, và kiểm toán phản ứng với adapter MongoExecutionContext. Cốt lõi được giữ độc lập với framework, và chỉ các ứng dụng cần thiết mới tiếp tục sử dụng cấu trúc cài đặt Spring hiện có.
Đồng thời, hướng đi không tái triển khai các chức năng mà MongoDB Driver đã cung cấp tốt cũng đã trở nên rõ ràng hơn. Tổng hợp thông thường có thể nhận Bson stage nguyên vẹn, và cho Tìm kiếm/Tìm kiếm Vector có các escape hatch như SearchOperator, VectorSearchQuery, driverOptions(...), stage(Bson). Đây là lựa chọn để có thể sử dụng ngay cả khi Driver cung cấp các chức năng mới mà không cần chờ đợi bản phát hành tiếp theo của DSL.
Triết lý phát triển
Không che giấu MongoDB.
Tôi không muốn tạo ra một ORM để chuyển đổi các chức năng của MongoDB thành các khái niệm cơ sở dữ liệu khác. Tôi kết nối các truy vấn, tổng hợp, giao dịch, Tìm kiếm Atlas và Tìm kiếm Vector của MongoDB thành các luồng ngắn hơn và dễ phát hiện hơn.
Chỉ thêm các chức năng thực sự cần thiết.
Đây không phải là một thư viện được thiết kế trước danh sách chức năng rồi mới triển khai. Do cần truy vấn trong dự án vận hành, chúng tôi đã thêm chức năng truy vấn, vì cần upsert và bulk nên đã thêm các chức năng đó, và để sử dụng chức năng tìm kiếm, chúng tôi đã thêm Atlas Search và Vector Search.
Ưu tiên năng suất hơn là sự hoàn hảo về cấu trúc.
Chúng tôi không cho rằng một lớp chính lớn là cấu trúc lý tưởng. Vấn đề quan trọng hơn vào thời điểm bắt đầu là đội ngũ có thể xử lý các tác vụ MongoDB không quen thuộc một cách nhanh chóng theo cùng một cách, và hiện tại chúng tôi vẫn ưu tiên quy trình sử dụng thực tế hơn là sự trừu tượng.
Chúng tôi không tái tạo lại những gì Driver đã làm.
Nếu MongoDB Java Driver có builder kiểu, chúng tôi sẽ sử dụng kiểu đó càng nhiều càng tốt. DSL cung cấp các API tiện lợi cho các tổ hợp lặp lại mà nó có thể giảm giá trị, nhưng không sao chép lại API Driver chỉ bằng cách đổi tên.
Cần có khả năng hạ xuống API cơ bản.
Chúng tôi không nghĩ rằng DSL có thể thay thế mọi tình huống. Chúng tôi để lại các lối thoát như Bson, filter/sort/operator/options của Driver, publisher customizer, và nếu cần, có thể sử dụng trực tiếp MongoDB Driver.
Bốn quy trình được đề cập trong tài liệu hiện tại.
Truy vấn Mongo thông thường.
Đây là quy trình mà thư viện này bắt đầu. Các điều kiện và thực thi được cấu hình theo thứ tự execute* → fields(...) → end() → find/findAll/count/delete/exists/atomicUpdate.
Tổng hợp gốc Driver.
Để kiểm soát trực tiếp từ stage đầu tiên của pipeline hoặc sử dụng chức năng tổng hợp mới mà Driver cung cấp, hãy sử dụng quy trình aggregation().stage(Bson). DSL có thể truyền các stage mà Driver đã cung cấp như $score, $scoreFusion mà không cần tái hiện lại.
Tìm kiếm Atlas
Đây là quy trình được thêm vào khi áp dụng Atlas Search vào dự án mà không cần giới thiệu dịch vụ tìm kiếm riêng biệt trước đó. Sau search(index), cấu hình text, compound, score, highlight và sequence token.
Tìm kiếm vector
Đây là quy trình được thêm vào để kết nối tìm kiếm embedding bên trong MongoDB. Cấu hình query vector hoặc embedding tự động, ANN/ENN, pre/post filter và tùy chọn embedding nested/array.
Mô hình thực thi của phiên bản 1.0.0.
Việc tách biệt truy vấn thông thường và Search/Vector là để không che giấu các hạn chế pipeline thực tế của MongoDB. $search và $vectorSearch có hạn chế ở stage đầu tiên, và các chức năng mà caller phải kiểm soát toàn bộ stage đầu tiên có thể hạ xuống aggregation().
Phạm vi sử dụng trong dự án thực tế
• Truyền các cặp không có điều kiện dưới dạng null để lắp ráp các điều kiện tìm kiếm động theo màn hình.
• Tách biệt data và totalCount bằng PageStream để duy trì quy trình xử lý khối trong trạng thái publisher reactive.
• Tạo các tác vụ an toàn cho việc thực thi lặp lại bằng atomicUpdate().upsertOne().document().setOnInsert(...).
• Lưu trữ dữ liệu thu thập và dữ liệu tích hợp bên ngoài bằng bulk upsert theo ID hoặc khóa công việc.
• Trả về kết quả lookup và tổng số bản ghi trong một pipeline bằng executeLookupAndCount.
• Sử dụng sequence token của Atlas Search và truy vấn embedding tự động của Vector Search.
• Các stage tổng hợp mới mà Driver cung cấp được kết nối trực tiếp bằng aggregation().stage(Bson) hoặc stage(Bson) của Search/Vector.
• Nếu cần tính nguyên tử giữa các thao tác DSL, hãy chỉ định phạm vi giao dịch ClientSession bằng getTxJob(...).