自定义mgo的upsert操作

huangapple go评论96阅读模式
英文:

Customize mgo upsert operation

问题

我有一个游戏分析的 REST API,它存储了玩家的平均表现统计数据。当有新的统计数据到达时,我想通过将新的增量合并到现有文档中来更新 MongoDB 中的现有游戏记录。我也存储了过去的分析数据,这样我就可以返回类似于玩家统计数据自上次更新以来是增加还是减少的数据。

**问题是:**当我想要使用 mgo 将新的游戏数据 upsert 到 MongoDB 中时,它会覆盖掉玩家的所有统计数据数组。实际上,这是预期的行为。如果我可以修改 mgo 尝试 upsert 到 MongoDB 的文档,我知道如何解决这个问题。

**问题是:**我如何自定义 mgo 的 upsert 行为?这样我就可以在 Player.Stats 前面添加 $push 操作符,以防止 MongoDB 抹去文档内的 stats 数组。

**我的真正问题是:**我将使用哪些 Mongo 命令并不重要,我会想办法解决。我真正想知道的是:在 upsert 之前如何自定义 mgo 的行为。

**一些解决方案:**我之前尝试过一些解决方案。比如,将 Game 结构体编码/解码为 bson.M 来自定义它。然而,我发现这样做很麻烦和混乱。如果没有其他办法,我会使用它。

**阻碍:**我不想手动编写所有的结构体字段,只为了在一个字段上使用 $push 操作符。因为有很多字段,那样会容易出错,并增加我的代码复杂性。


示例:

// 假设这是 MongoDB 中的一个现有游戏:
existingGame := Game{
	ID: 1,
	Name: "Existing game",
	// 游戏只有一个玩家
	Players: []Player{
		// 玩家有一些统计数据。最新的是 2.0。
		{1, "foo", []float64{3.5, 2.0}},
	}
}

// 这是一个新的请求到达我的 API
// 我想将其 upsert 到现有的游戏中
newGame := Game{
	ID: 1,
	Players: []Player{
		// 预期地,这将重置玩家 foo 的统计数据为 5.0
		//
		// 在 upsert 之后,我希望它变为:
		//
		// []float64{3.5, 2.0, 5.0}
		//
		// 在 MongoDB 中
		{1, "foo", []float64{5.0}},
	}
}

// 示例 2:
// 如果新的游戏请求如下:
newGame := Game{ID: 1, Players: []Player{{1, "foo", []float64{5.0}},{1, "bar", []float64{6.7}}}}
// 我期望的结果是:
Game{ID: 1, Players: []Player{{1, "foo", []float64{3.5, 2.0, 5.0}},{1, "bar", []float64{6.7}}}}

func (db *Store) Merge(newGame *Game) error {
	sess := db.session.Copy()
	defer sess.Close()

	col := sess.DB("foo").C("games")
	// 我想在这里修改 newGame,添加一个 $push 操作符
	// 到一个新的 `bson.M` 或 `bson.D` 中,以使 mgo 在 upsert 时
	// 不重置玩家的统计数据
	_, err := col.UpsertId(newGame.ID, newGame)

	return err
}

type Game struct {
	ID int `bson:"_id"`
	Name string
	Players []Player `bson:",omitempty"`
	// ...我在这里省略了其他细节以简化...
}

type Player struct {
    // 这将玩家与游戏连接起来
    GameID int `bson:"game_id"`
	Name string
	// 我想保留统计数据的先前值
	// 所以我在这里使用了一个数组
	Stats []float64
	// ...
}

我在控制台中尝试了这个 MongoDB 命令来更新特定游戏的玩家:

db.competitions.update({
   _id: 1,
   "players.game_id": 1
}, {
   $push: { 
       "players.$.stats": 3
   }
}, {
   upsert: true
})
英文:

I've a game analytics rest API which stores the average performance statistics of the players. When a new statistic arrives, I want to update the existing game record in Mongodb by merging the new delta onto the existing document. I'm storing the past analytics data as well. So that, I can return data like the player's stats are decreasing or increasing since the game's last update.

The problem is: When I want to upsert my new game data into Mongodb with mgo, it overwrites all of a player's stats array. Actually, this is expected. I know how to fix it if I can modify my document that mgo tries to upsert into Mongodb.

Question: How can I customize mgo upsert behaviour? So that I can add a $push operator in front of Player.Stats to prevent Mongodb erasing the stats array inside the document.

My Real Question: It doesn't matter which Mongo commands I'm going to use. I'll figure it out somehow. What I actually want to know is: How can I customize the behaviour of mgo before upsert?

Some Solutions: I've tried some solutions myself before. Like, encoding/decoding Game struct into bson.M to customize it. However, I found it cumbersome and messy. If there's no other way, I'd use it.

Blocks: I don't want to hand-write all of my structs fields with bson.M, just to use a $push operator on one field. Because there are dozens of fields, that would be error-prone and will increase my code complexity.


Example:

// Assume that, this is an existing game in Mongodb:
existingGame := Game{
	ID: 1,
	Name: "Existing game",
	// The game has just one player
	Players: []Player{
		// The player has some stats. The newest one is 2.0.
		{1, "foo", []{3.5, 2.0}},
	}
}

// This is a new request coming to my API
// I want to upsert this into the existing Game
newGame := Game{
	ID: 1,
	Players: []Player{
		// As expectedly, this will reset player foo's stats to 5.0
		//
		// After upserting, I want it to be as: 
		//
		// []{3.5, 2.0, 5.0}
		//
		// in Mongodb
		{1, "foo", []{5.0}},
	}
}

// Example 2:
// If new Game request like this:
newGame := Game{ID: 1, Players: []Player{{1, "foo", []{5.0},{1, "bar", []{6.7}}}}
// I'm expecting this result:
Game{ID: 1, Players: []Player{{1, "foo", []{3.5, 2.0, 5.0},{1, "bar", []{6.7}}}}

func (db *Store) Merge(newGame *Game) error {
	sess := db.session.Copy()
	defer sess.Close()

	col := sess.DB("foo").C("games")
	// I want to modify newGame here to add a $push operator
	// into a new `bson.M` or `bson.D` to make mgo to upsert
	// my new delta without resetting the player stats
	_, err := col.UpsertId(newGame.ID, newGame)

	return err
}

type Game struct {
	ID int `bson:"_id"`
	Name string
	Players []Player `bson:",omitempty"`
	// ...I omitted other details for simplicity here...
}

type Player struct {
    // This connects the player to the game
    GameID int `bson:"game_id"`
	Name string
	// I want to keep the previous values of stats
	// So, that's why I'm using an array here
	Stats []float64
	// ...
}

I tried this Mongodb command in the console to update the specific game's player:

db.competitions.update({
   _id: 1,
   "players.game_id": 1
}, {
   $push: { 
       "players.$.stats": 3
   }
}, {
   upsert: true
})

答案1

得分: 2

回答你的问题:“如何自定义 mgo 在 upsert 之前的行为?”- 你可以通过定义 bson Getter 来自定义 bson 编组(marshalling)到模型中。

为了说明它是如何工作的,让我们简化模型以避免嵌套文档:

type Game struct {
    ID    int       `bson:"_id"`
    Name  string
    Stats []float64
}

使用以下的 newGame:

newGame := Game{
    ID:    1,
    Name:  "foo",
    Stats: []float64{5.0},
}

默认情况下,col.UpsertId(newGame.ID, newGame) 将 newGame 编组为 JSON,生成类似以下的 MongoDB 查询:

update({_id:1}, {name: "foo", stats: [5]}, {upsert: true});

为了使用 $set$push 等操作符,你可以定义一个自定义的 bson getter。例如:

func (g Game) GetBSON() (interface{}, error) {
    return bson.M{
        "$set":  bson.M{"name": g.Name},
        "$push": bson.M{"stats": bson.M{"$each": g.Stats}},
    }, nil
}

因此,col.UpsertId(newGame.ID, newGame) 将生成以下的 MongoDB 查询:

update({_id:1}, {$set: {name: "foo"}, $push: {stats: {$each: [5]}}}, {upsert: true});

为了更清楚地说明,自定义的编组器将在所有的 mgo 查询中使用,所以你可能不希望直接将其定义到模型中,而是定义到其派生模型中,仅在 upsert 操作中使用:

type UpdatedGame struct {
    Game
}

func (g UpdatedGame) GetBSON() (interface{}, error) {
    return bson.M{....}, nil
}

......

newGame := Game{
    ID:    1,
    Name:  "foo",
    Stats: []float64{5.0},
}

col.UpsertId(newGame.ID, UpdatedGame{newGame})
英文:

To answer the "My Real Question: How can I customize the behaviour of mgo before upsert?" - you can customise bson marshalling by defining bson Getter to the model.

To illustrate how it works, lets simplify the model to avoid nested documents:

type Game struct {
    ID int `bson:"_id"`
    Name string
    Stats [] float64
}

With newGame as following:

newGame := Game{
    ID: 1,
    Name: "foo",
    Stats: []{5.0}
}

The update col.UpsertId(newGame.ID, newGame) by default marshals newGame into JSON, producing mongo query like:

update({_id:1}, {name: "foo", stats: [5]}, {upsert: true});

To make use of $set, $push etc, you can define a custom bson getter. E.g.

func (g Game) GetBSON() (interface{}, error) {
    return bson.M{
        "$set": bson.M{"name": g.Name}, 
        "$push": bson.M{"stats": bson.M{"$each": g.Stats}},
    }, nil
}

So the update col.UpsertId(newGame.ID, newGame) will produce a mongodb query

update({_id:1}, {$set: {name: "foo"}, $push: {stats: {$each: [5]}}}, {upsert: true});

To make it crystal clear - the custom marshaler will be used in all mgo queries, so you probably don't want to define it directly to the model, but to its derivative to use in upsert operations only:

type UpdatedGame struct {
    Game
}

func (g UpdatedGame) GetBSON() (interface{}, error) {
    return bson.M{....}
}

.....

newGame := Game{
    ID: 1,
    Name: "foo",
    Stats: []{5.0}
}

col.UpsertId(newGame.ID, UpdatedGame{newGame})

huangapple
  • 本文由 发表于 2017年8月17日 00:19:13
  • 转载请务必保留本文链接:https://go.coder-hub.com/45718580.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定