I have the following model:
type UserModel struct {
Id string `bson:"_id,omitempty"`
CreatedAt *time.Time `bson:"createdAt,omitempty"`
BasicInfo *UserBasicInfoModel `bson:"basicInfo,omitempty"`
}
// *Embedded document*
type UserBasicInfoModel struct {
FirstName *string `bson:"firstName,omitempty"`
LastName *string `bson:"lastName,omitempty"`
}
I am using pointers, in order to be able to distinguish between a missing value (nil
) and default value (eg empty strings, false values etc). I also use omitempty
to be able to do partial updates.
When I create a user I get the following (correct) response back:
"id": "aba19b45-5e84-55e0-84f8-90fad41712f6",
"createdAt": "2018-05-26T15:08:56.764453386+03:00",
"basicInfo": {
"firstName": "Initial first name",
"lastName": "Initial last name"
}
When I try to update the document though I have an issue.
I send the changes as a new UserModel
, to only change the FirstName
field in the embedded document like this:
newFirstName := "New Value"
UserModel{
BasicInfo: &UserBasicInfoModel{
FirstName: &newFirstName,
},
}
The code I use to do the update is the following:
UpdateId(id, bson.M{"$set": changes})
The response I get back is the following:
"id": "aba19b45-5e84-55e0-84f8-90fad41712f6",
"createdAt": "2018-05-26T12:08:56.764Z",
"basicInfo": {
"firstName": "New Value",
"lastName": null
}
The createdAt
value is not null
(as I expected) however the lastName
value is null
(which is not what I expected)
I would have expected to get back the following:
"id": "aba19b45-5e84-55e0-84f8-90fad41712f6",
"createdAt": "2018-05-26T12:08:56.764Z",
"basicInfo": {
"firstName": "New Value",
"lastName": "Initial last name"
}
What can I do to achieve a partial update in a subdocument using mgo?
First let's quickly explain your createdAt
field. This is the value you save: 2018-05-26T15:08:56.764453386+03:00
. Know that MongoDB stores dates with millisecond precision, and in UTC timezone. So this date when saved and retrieved from MongoDB becomes 2018-05-26T12:08:56.764Z
, this is the "same" time instant, just in UTC zone and precision truncated to milliseconds.
Now on to updating embedded documents:
The short and unfortunate answer is that we can't do this directly with the mgo
library and Go models.
When we use the ,omitempty
option, and we leave some pointer fields at their zero value (that is, being nil
), it's like if we were using a value whose type didn't even have those fields.
So in your example, if you only change the BasicInfo.FirstName
field, and you use this value to update, it is equivalent to using these structures:
type UserModel struct {
Id string `bson:"_id,omitempty"`
BasicInfo *UserBasicInfoModel `bson:"basicInfo,omitempty"`
}
type UserBasicInfoModel struct {
FirstName *string `bson:"firstName,omitempty"`
}
So the effect of your issued update
command will be the following:
db.users.update({_id: "aba19b45-5e84-55e0-84f8-90fad41712f6"},
{$set:{
"_id": "aba19b45-5e84-55e0-84f8-90fad41712f6",
"basicInfo": {
"firstName": "New Value"
}
}}
)
What does this mean? To set the _id
to the same value (it won't change), and to set the basicInfo
field to an embedded document which only has a single firstName
property. This will erase the lastName
field of the embedded basicInfo
document. So when you unmarshal the document after the update into a value of your UserModel
type, the LastName
field will remain nil
(because it is not present in MongoDB anymore).
What can we do?
One trivial solution is to not use an embedded document, but add fields of UserBasicInfoModel
to UserModel
:
type UserModel struct {
Id string `bson:"_id,omitempty"`
CreatedAt *time.Time `bson:"createdAt,omitempty"`
FirstName *string `bson:"firstName,omitempty"`
LastName *string `bson:"lastName,omitempty"`
}
,inline
optionThis solution keeps the separate Go struct, but in MongoDB it will not be an embedded document (BasicInfo
will be flattened just like in the previous example):
type UserModel struct {
Id string `bson:"_id,omitempty"`
CreatedAt *time.Time `bson:"createdAt,omitempty"`
BasicInfo UserBasicInfoModel `bson:"basicInfo,omitempty,inline"`
}
Note that BasicInfo
needs to be a non-pointer if ,inline
is used. This is not a problem, as we can leave it being an empty struct if its fields are not to be changed, since its fields are pointers, so leaving them nil
will not change them.
If you do need to use embedded document, the mgo
library allows you to update specific fields of embedded documents, but then you have to "manually" construct the update document, like in this example:
c.UpdateId(Id, bson.M{"$set": bson.M{
"basicInfo.firstName": newFirstName,
}})
Yes, this isn't convenient at all. If you do need this many times with different types, you may create a utility function that uses reflection, iterates over the fields recursively, and assemble the update document from fields which are not nil
. Then you could pass that dynamically generated update doc to UpdateId()
for example.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With