MongoDB:更新插入子文档

问题描述 投票:0回答:5

我有看起来像那样的文件,在

bars.name
上有一个唯一的索引:

{ name: 'foo', bars: [ { name: 'qux', somefield: 1 } ] }

.我想更新

{ name: 'foo', 'bars.name': 'qux' }
$set: { 'bars.$.somefield': 2 }
的子文档,或者在
{ name: 'qux', somefield: 2 }
下创建一个带有
{ name: 'foo' }
的新子文档。

是否可以使用带有 upsert 的单个查询来执行此操作,还是我必须发出两个单独的查询?

相关:'upsert' in an embedded document(建议更改模式以将子文档标识符作为键,但这是两年前的事,我想知道现在是否有更好的解决方案。)

mongodb mongodb-query upsert
5个回答
59
投票

不,真的没有更好的解决方案,所以也许有一个解释。

假设您有一个文档,其结构如您所示:

{ 
  "name": "foo", 
  "bars": [{ 
       "name": "qux", 
       "somefield": 1 
  }] 
}

如果你做这样的更新

db.foo.update(
    { "name": "foo", "bars.name": "qux" },
    { "$set": { "bars.$.somefield": 2 } },
    { "upsert": true }
)

然后一切都很好,因为找到了匹配的文档。但是如果你改变“bars.name”的值:

db.foo.update(
    { "name": "foo", "bars.name": "xyz" },
    { "$set": { "bars.$.somefield": 2 } },
    { "upsert": true }
)

然后你会失败。唯一真正改变的是在 MongoDB 2.6 及更高版本中,错误更加简洁:

WriteResult({
    "nMatched" : 0,
    "nUpserted" : 0,
    "nModified" : 0,
    "writeError" : {
        "code" : 16836,
        "errmsg" : "The positional operator did not find the match needed from the query. Unexpanded update: bars.$.somefield"
    }
})

这在某些方面更好,但无论如何你真的不想“upsert”。您要做的是将元素添加到当前不存在“名称”的数组中。

所以您真正想要的是没有“upsert”标志的更新尝试的“结果”,以查看是否有任何文档受到影响:

db.foo.update(
    { "name": "foo", "bars.name": "xyz" },
    { "$set": { "bars.$.somefield": 2 } }
)

屈服响应:

WriteResult({ "nMatched" : 0, "nUpserted" : 0, "nModified" : 0 })

所以当修改后的文件是

0
那么你知道你想要发布以下更新:

db.foo.update(
    { "name": "foo" },
    { "$push": { "bars": {
        "name": "xyz",
        "somefield": 2
    }}
)

确实没有其他方法可以完全按照您的意愿行事。由于数组的添加并不是严格意义上的“集合”类型的操作,因此您不能将

$addToSet
“批量更新” 功能结合使用,以便您可以“级联”您的更新请求。

在这种情况下,您似乎需要检查结果,或者接受阅读整个文档并检查是否更新或在代码中插入新的数组元素。


5
投票

如果你不介意稍微改变模式并拥有这样的结构:

{ "name": "foo", "bars": { "qux": { "somefield": 1 },
                           "xyz": { "somefield": 2 },
                  }
}

您可以一次完成您的操作。 在嵌入式文档中重申'upsert'以确保完整性


5
投票

我正在挖掘相同的功能,发现在 4.2 或更高版本中,MongoDB 提供了一个新功能,称为 Update with aggregation pipeline
如果与其他一些技术一起使用,此功能可以通过单个查询实现 upsert subdocument 操作。

这是一个非常冗长的查询,但我相信如果你知道你不会在 subCollection 上有太多记录,它是可行的。这是一个关于如何实现这一点的例子:

const documentQuery = { _id: '123' }
const subDocumentToUpsert = { name: 'xyz', id: '1' }

collection.update(documentQuery, [
    {
        $set: {
            sub_documents: {
                $cond: {
                    if: { $not: ['$sub_documents'] },
                    then: [subDocumentToUpsert],
                    else: {
                        $cond: {
                            if: { $in: [subDocumentToUpsert.id, '$sub_documents.id'] },
                            then: {
                                $map: {
                                    input: '$sub_documents',
                                    as: 'sub_document',
                                    in: {
                                        $cond: {
                                            if: { $eq: ['$$sub_document.id', subDocumentToUpsert.id] },
                                            then: subDocumentToUpsert,
                                            else: '$$sub_document',
                                        },
                                    },
                                },
                            },
                            else: { $concatArrays: ['$sub_documents', [subDocumentToUpsert]] },
                        },
                    },
                },
            },
        },
    },
])

2
投票

有一种方法可以在两个查询中完成 - 但它仍然可以在

bulkWrite
.

这很重要,因为在我的情况下,无法对其进行批处理是最大的问题。使用此解决方案,您无需收集第一个查询的结果,这允许您在需要时进行批量操作。

以下是为您的示例运行的两个连续查询:

// Update subdocument if existing
collection.updateMany({
    name: 'foo', 'bars.name': 'qux' 
}, {
    $set: { 
        'bars.$.somefield': 2 
    }
})
// Insert subdocument otherwise
collection.updateMany({
    name: 'foo', $not: {'bars.name': 'qux' }
}, {
    $push: { 
        bars: {
            somefield: 2, name: 'qux'
        }
    }
})

如果多个应用程序同时写入数据库,这还有一个额外的好处,即不会损坏数据/竞争条件。如果两个应用程序同时运行相同的查询,您将不会冒险在文档中得到两个

bars: {somefield: 2, name: 'qux'}
子文档。


0
投票

我试图做类似的事情——如果文档不存在,我想创建它,如果没有子文档,我想将子文档添加到数组中,或者如果子文档已经存在,则增加子文档中的计数字段大批。基于这些和其他答案,这是我使用 Spring Boot MongoTemplate 想到的。我相信这是原子的,因为如果另一个进程同时更新元素,它不会导致重复并仍然达到预期的结果。不幸的是,它需要 3 个数据库事务;我不确定是否有更有效的方法来做到这一点。


@Document(collection = "dealerships")
public class Dealership {

    public static class Car {
        String carBrand;
        int count;
        public Car(Sstring brand) {
            this.carBrand = brand;
            count = 1;
        }
    }

    String id;
    List<Car> carsOnLot = new ArrayList<>();    //important - create an empty array when inserting new document

    //Constructor, getters, setters...
}

//First, let's try inserting the a new Dealership document, if that Dealership already exists this will fail
try {
    mongoTemplate.insert(new Dealership("Annapolis"));
} catch (org.springframework.dao.DuplicateKeyException dex) {            
    System.out.println("Annapolis dealer exists!");
}

//----- At this point the Annapolis dealer document exists, but we don't know if our Car is in the carsOnLot array ------------

//This is the query and update for adding to the array "carsOnLot", if the particular car is not already listed
Query addCarQuery = new Query().addCriteria(Criteria.where("id").is("Annapolis"));
addCarQuery.addCriteria(Criteria.where("carsOnLot.carBrand").ne("Audi"));

Update addCarUpdate = new Update();
addCarUpdate.addToSet("carsOnLot", new Car("Audi"));    //this will not duplicate an existing element


//Let's try adding this car brand to the array
UpdateResult ur = mongoTemplate.updateFirst(addCarQuery, addCarUpdate, Dealership.class);
if (ur.getModifiedCount() == 1)  // we added it - our job is done
    return;
else   
    System.out.println("Already Audis on the lot!");


//------ At this point we already have an entry for our brand, so let's increment the count

//This is the query for incrementing the count of cars
Query updateQuery = new Query().addCriteria(Criteria.where("id").is("Annapolis"));
updateQuery.addCriteria(Criteria.where("carsOnLot").elemMatch(Criteria.where("carBrand").is("Audi")));

Update updateUpdate = new Update().inc("carsOnLot.$.count", 1); //increment the count for Audis by 1

 ur = mongoTemplate.updateFirst(updateQuery, updateUpdate, Dealership.class);
 if (ur.getModifiedCount() == 1)    // we incremented it - our job is done
     return;

// Log a failure, something isn't right
© www.soinside.com 2019 - 2024. All rights reserved.