如何使用 mgo 插入文档并获取返回值

How to insert a document with mgo and get the value returned

郑重声明,我正在学习围棋。我正在尝试使用 mgo 包,我想向用户插入一个新文档和 return 这个新创建的文档(我正在尝试编写一个基本的 API)。我写了以下代码:

编辑:这是模型的结构:

type Book struct {
  ISBN    string   `json:"isbn"`
  Title   string   `json:"title"`
  Authors []string `json:"authors"`
  Price   string   `json:"price"`
}

session := s.Copy()
defer session.Close()

var book Book
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&book)
if err != nil {
    ErrorWithJSON(w, "Incorrect body", http.StatusBadRequest)
    return
}

c := session.DB("store").C("books")

info, err := c.Upsert(nil, book)

if err != nil {
    ErrorWithJSON(w, "Database error", http.StatusInternalServerError)
log.Println("Failed insert book: ", err)
    return
}

respBody, err := json.MarshalIndent(info, "", "  ")
if err != nil {
    log.Fatal(err)
}

ResponseWithJSON(w, respBody, http.StatusOK)

请注意 Book 是我之前创建的结构。上面的代码确实有效,但它 returns 是像这样的 upsert 结果:

{
    "Updated": 1,
    "Removed": 0,
    "Matched": 1,
    "UpsertedId": null
}

这不是最近创建的对象。如何将最近创建的对象作为响应获取到 return(请注意,理想情况下我希望确认文档已成功插入。我已经看到其他问题,其中 ID 是预先生成的,但是为了什么我看到它没有确认文档已创建,是吗?)

我们先理清概念。在 MongoDB 中,每个文档必须有一个 _id 属性 作为其在集合中的唯一文档标识符。要么您提供此 _id 的值,要么由 MongoDB.

自动分配

因此,您的模型类型最好(或强烈建议)为 _id 文档标识符包含一个字段。由于我们在这里讨论的是书籍,书籍已经有一个称为 ISBN 的唯一标识符,您可以选择将其用作 _id 字段的值。

MongoDB 字段和 Go struct 字段之间的映射必须使用 bson 标签指定(而不是 json)。因此,您应该提供 bson 标签值以及 json 标签。

因此将您的模型更改为:

type Book struct {
  ISBN    string   `json:"isbn" bson:"_id"`
  Title   string   `json:"title" bson:"title"`
  Authors []string `json:"authors" bson:"authors"`
  Price   string   `json:"price" bson:"price"`
}

如果你想插入一个新文档(一本新书),你应该总是使用Collection.Insert()

新插入的文档的 ID 是什么?您设置为 Book.ISBN 字段的字段,因为我们声明它是带有 bson:"_id" 标签的文档 ID。

如果您不确定文档是否已经存在,但无论哪种方式您都希望它是您手头的文档,您应该只使用 Collection.Upsert()Collection.Upsert() 将尝试查找要更新的文档,如果找到,则会进行更新。如果没有找到文档,则执行插入操作。第一个参数是用于查找要更新的文档的选择器。由于您通过了 nil,这意味着任何文档都可能符合条件,因此将选择一个 "randomly"。因此,如果您已经保存了书籍,则任何书籍都可能会被选中并被覆盖。这肯定不是你想要的。

既然现在 ISBN 就是 ID,您应该指定一个按 ISBN 过滤的选择器,如下所示:

info, err := c.Upsert(bson.M{"_id": book.ISBN}, book)

或者因为我们是按 ID 过滤,所以使用更方便的 Collection.UpsertId():

info, err := c.UpsertId(book.ISBN, book)

如果您想更新现有文档,您可以使用 Collection.Update(). This is similar to Collection.Upsert(), but the difference is that if no documents match the selector, an insert will not be performed. Updating a document matched by ID can also be done with the more convenient Collection.UpdateId()(类似于 Collection.UpsertId())。

对于自然没有唯一标识符的其他文档(如具有 ISBN 的书籍),您可以使用生成的 ID。 mgo 库提供了 bson.NewObjectId() function for such purpose, which returns you a value of type bson.ObjectId。使用 Collection.Insert() 保存新文档时,您可以使用 bson.NewObjectId() 获取一个新的唯一 ID,并将其分配给映射到 MongoDB _id [=78= 的结构字段].如果插入成功,则可以确定文档的 ID 是您在调用 Collection.Insert() 之前设置的 ID。此 ID 生成旨在即使在分布式环境中也能工作,因此即使您的 2 个节点同时尝试生成 ID,它也会生成唯一的 ID。

例如,如果您在保存一本书时没有 ISBN,那么您必须在 Book 类型中有一个单独的指定 ID 字段,例如:

type Book struct {
  ID      bson.ObjectId `bson:"_id"`
  ISBN    string        `json:"isbn" bson:"isbn"`
  Title   string        `json:"title" bson:"title"`
  Authors []string      `json:"authors" bson:"authors"`
  Price   string        `json:"price" bson:"price"`
}

保存新书时:

var book Book
// Fill what you have about the book
book.ID = bson.NewObjectId()

c := session.DB("store").C("books")
err = c.Insert(book)
// check error
// If no error, you can refer to this document via book.ID

为此我的脑袋撞了一会儿。

info, err := c.Upsert(nil, book)

upsert 的 nil 选择器将匹配所有内容,因此如果您的集合为空,选择器将不匹配,一切都会好起来的,信息对象将在 UpsertedId 字段中包含 ObjectId,但是每个后续的带有 nil 选择器的 upsert 集合都会有记录,并且 upsert 的 nil 选择器将匹配,因此它不会 return 你是 UpsertedId,你将更新第一个匹配的记录。

您可以使用永不匹配的选择器 进行更新插入(这是我的解决方案),但请注意,这样您只会插入和获取插入的 ObjectId,而不会更新记录.

您可以查看以下测试:

func TestFoo(t *testing.T) {
    foo := struct {
        Bar string `bson:"bar"`
    }{
        Bar: "barrr",
    }

    session, _ := mgo.DialWithInfo(&mgo.DialInfo{
        Addrs: []string{"127.0.0.1:27017"},
    })

    coll := session.DB("foo").C("bar")
    coll.DropCollection()

    info, _ := coll.Upsert(nil, foo) // will insert
    count, _ := coll.Count()
    fmt.Printf("%+v, collecton records:%v\n", info, count) // &{Updated:0 Removed:0 Matched:0 UpsertedId:ObjectIdHex("5c35e8ee1f5b80c932b44afb")}, collection records:1

    info, _ = coll.Upsert(bson.M{}, foo) // will update
    count, _ = coll.Count()
    fmt.Printf("%+v, collecton records:%v\n", info, count) // &{Updated:1 Removed:0 Matched:1 UpsertedId:<nil>}, collection records:1

    info, _ = coll.Upsert(bson.M{"nonsense": -1}, foo) // will insert duplicate record (due to the selector that will never match anything)
    count, _ = coll.Count()
    fmt.Printf("%+v, collecton records:%v\n", info, count) // &{Updated:0 Removed:0 Matched:0 UpsertedId:ObjectIdHex("5c35ea2a1f5b80c932b44b1d")}, collection records:2

    foo.Bar = "baz"
    info, _ = coll.Upsert(nil, foo) // will update the first matched (since the selector matches everything)
    count, _ = coll.Count()
    fmt.Printf("%+v, collecton records:%v\n", info, count)

    // after the test the collection will have the following records
    //> use foo
    //> db.bar.find()
    //{ "_id" : ObjectId("5c35ebf41f5b80c932b44b81"), "bar" : "baz" } // the first matched on the last update with nil selector
    //{ "_id" : ObjectId("5c35ebf41f5b80c932b44b86"), "bar" : "barrr" } // the duplicated record with the selector that never matches anything

}

编辑: 注意你应该在永不匹配的字段上有一个索引,否则如果你有很多记录,你的插入查询将花费太长时间,因为更新插入没有索引filed 将扫描集合中的所有文档。