如何实现带版本校验的条件式Upsert:旧版本无操作
带版本校验的条件式Upsert实现需求
需要对MongoDB(兼容Azure Cosmos DB)集合执行条件式Upsert操作:匹配Id不存在则插入新文档,存在则更新,但需满足一个核心限制:若集合中已持久化文档的Version整数属性值大于或等于待Upsert文档的Version,则该Upsert直接无任何操作。
测试文档定义
public record TestDocument(string Id, int Version, string SomeDataString);
预期行为
- 若集合中无
Id="Foo"的文档,Upsert任意Version的Id="Foo"文档,成功插入新文档 - 若集合中存在
Version=1的Id="Foo"文档,UpsertVersion=2的Id="Foo"文档,成功替换现有文档 - 若集合中存在
Version≥2的Id="Foo"文档,UpsertVersion=2的Id="Foo"文档,无任何操作
当前实现方案及问题
目前通过「匹配Id且Version小于待Upsert文档的过滤器执行更新,捕获重复键异常」的方式实现,但该方案存在严重缺陷:
var idMatchFilter = Builders<TestDocument>.Filter.Eq(e => e.Id, upsertDocument.Id); var versionFilter = Builders<TestDocument>.Filter.Lt(e => e.Version, upsertDocument.Version); var combinedFilter = Builders<TestDocument>.Filter.And(idMatchFilter, versionFilter); var replaceOptions = new ReplaceOptions() { IsUpsert = true }; try { await testCollection.ReplaceOneAsync(combinedFilter, upsertDocument, replaceOptions); } catch (MongoWriteException mongoWriteException) when (mongoWriteException.Message.Contains("A write operation resulted in an error. WriteError: { Category : \"DuplicateKey\", Code : 11000, Message : \"E11000 duplicate key error collection")) { // 插入旧版本文档时会触发预期异常,但理想状态下应直接无操作而非触发异常 }
- 批量Upsert场景(如1000个文档)下,频繁捕获异常会导致性能极低,可行性差
- 文档结构复杂时,需要替换整个文档而非修改个别属性,该方案无法适配更优的批量操作逻辑
测试用例(Xunit)
以下测试用例当前仅通过捕获MongoWriteException才能通过,目标是实现「旧版本文档写入时直接无操作」的逻辑,无需捕获异常:
[Theory] // 插入:无持久化文档 [InlineData(null, 1, true)] // 更新:持久化版本更旧 [InlineData(1, 2, true)] // 更新:持久化版本相同 [InlineData(2, 2, false)] // 更新:持久化版本更新 [InlineData(2, 1, false)] public async Task ConditionalUpsert(int? persistedVersion, int upsertVersion, bool shouldUpdate) { // 初始化 var testCollection = GetTestCollection(); var emptyFilter = Builders<TestDocument>.Filter.Empty; await testCollection.DeleteManyAsync(emptyFilter); if (persistedVersion.HasValue) { // 预存指定版本的测试文档 var testDocument = new TestDocument("Foo", persistedVersion.Value, "persisted document dummy payload"); await testCollection.InsertOneAsync(testDocument); } var upsertDocument = new TestDocument("Foo", upsertVersion, "new document dummy payload"); // 执行操作 var idMatchFilter = Builders<TestDocument>.Filter.Eq(e => e.Id, upsertDocument.Id); var versionFilter = Builders<TestDocument>.Filter.Lt(e => e.Version, upsertDocument.Version); var combinedFilter = Builders<TestDocument>.Filter.And(idMatchFilter, versionFilter); // 错误实现:写入旧版本文档时会错误更新已有文档 //var findOneAndReplaceOptions = new FindOneAndReplaceOptions<TestDocument> { IsUpsert = true }; //await testCollection.FindOneAndReplaceAsync(idMatchFilter, upsertDocument, findOneAndReplaceOptions); // 错误实现:写入旧版本文档时会尝试插入新文档,触发重复键异常 // Command findAndModify failed: E11000 duplicate key error collection: SomeDatabase.SomeCollection. Failed _id or unique index constraint //var findOneAndReplaceOptions = new FindOneAndReplaceOptions<TestDocument> { IsUpsert = true }; //await testCollection.FindOneAndReplaceAsync(combinedFilter, upsertDocument, findOneAndReplaceOptions); var replaceOptions = new ReplaceOptions() { IsUpsert = true }; try { // 错误逻辑:当已有文档版本更新时,过滤器匹配不到,会尝试插入旧版本文档,触发异常 await testCollection.ReplaceOneAsync(combinedFilter, upsertDocument, replaceOptions); } catch (MongoWriteException mongoWriteException) when (mongoWriteException.Message.Contains("A write operation resulted in an error. WriteError: { Category : \"DuplicateKey\", Code : 11000, Message : \"E11000 duplicate key error collection")) { // 插入旧版本文档时的预期异常,理想状态下应直接无操作 } // 断言验证 var allDocuments = await testCollection.Find(emptyFilter).ToListAsync(); var queriedDocument = Assert.Single(allDocuments); var expectedVersion = shouldUpdate ? upsertVersion : persistedVersion; Assert.Equal(expectedVersion, queriedDocument.Version); }
内容的提问来源于stack exchange,提问作者MicrosoftStackDev
相关产品推荐
相关产品推荐

