Custom BSON marshal and unmarshal using mongo-driver - mongodb

I have a struct field like this below. I also store raw protobuf of the same struct in db. Now every time fetch or save data to mongo. I have to update ReallyBigRaw, from the proto when I want to save to DB and when fetch I have to unmarshal ReallyBigRaw to ReallyBigObj to give out responses. Is there a way I can implement some interface or provide some callback functions so that the mongo driver does this automatically before saving or fetching data from DB.
Also, I am using the offical golang mongo driver not mgo, I have read some answers where can be done in mgo golang library.
import (
"github.com/golang/protobuf/jsonpb"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
proto "github.com/dinesh/api/go"
)
type ReallyBig struct {
ID string `bson:"_id,omitempty"`
DraftID string `bson:"draft_id,omitempty"`
// Marshaled ReallyBigObj proto to map[string]interface{} stored in DB
ReallyBigRaw map[string]interface{} `bson:"raw,omitempty"`
ReallyBigObj *proto.ReallyBig `bson:"-"`
CreatedAt primitive.DateTime `bson:"created_at,omitempty"`
UpdatedAt primitive.DateTime `bson:"updated_at,omitempty"`
}
func (r *ReallyBig) GetProto() (*proto.ReallyBig, error) {
if r.ReallyBigObj != nil {
return r.ReallyBigObj, nil
}
Obj, err := getProto(r.ReallyBigRaw)
if err != nil {
return nil, err
}
r.ReallyBigObj = Obj
return r.ReallyBigObj, nil
}
func getRaw(r *proto.ReallyBig) (map[string]interface{}, error) {
m := jsonpb.Marshaler{}
b := bytes.NewBuffer([]byte{})
// marshals proto to json format
err := m.Marshal(b, r)
if err != nil {
return nil, err
}
var raw map[string]interface{}
// unmarshal the raw data to an interface
err = json.Unmarshal(b.Bytes(), &raw)
if err != nil {
return nil, err
}
return raw, nil
}
func getProto(raw map[string]interface{}) (*proto.ReallyBig, error) {
b, err := json.Marshal(raw)
if err != nil {
return nil, err
}
u := jsonpb.Unmarshaler{}
var reallyBigProto proto.ReallyBig
err = u.Unmarshal(bytes.NewReader(b), &recipeProto)
if err != nil {
return nil, err
}
return &reallyBigProto, nil
}

I implemented the Marshaler and Unmarshaler interface. Since mongo driver calls MarshalBSON and UnmarshalBSON if the type implements Marshaler and Unmarshaler we also end up in infinite loop. To avoid that we create a Alias of the type. Alias in Golang inherit only the fields not the methods so we end up calling normal bson.Marshal and bson.Unmarshal
func (r *ReallyBig) MarshalBSON() ([]byte, error) {
type ReallyBigAlias ReallyBig
reallyBigRaw, err := getRaw(r.ReallyBigObj)
if err != nil {
return nil, err
}
r.ReallyBigRaw = reallyBigRaw
return bson.Marshal((*ReallyBigAlias)(r))
}
func (r *ReallyBig) UnmarshalBSON(data []byte) error {
type ReallyBigAlias ReallyBig
err := bson.Unmarshal(data, (*ReallyBigAlias)(r))
if err != nil {
return err
}
reallyBigProto, err := getProto(r.ReallyBigRaw)
if err != nil {
return err
}
r.ReallyBigObj = reallyBigProto
return nil
}

Related

(Golang) InvalidBSON when using custom MarshalBSON()

I am currently migrating some of my Python Services to GoLang, where I encountered some problems with the integration with the existing database.
When it comes to saving, the formatting of the date string is different, and I am not planning to change the format of my database entries.
So I decided to implement a time type, with custom (Un)MarshalBSON() methods, to keep pythons time formatting.
However when saving it, I get the following error:
2022/09/10 20:57:29 (InvalidBSON) Unrecognized BSON type 45 in element with field name 'created_at.09-10 20:57:27.798545' in object with _id: ObjectId('631cde192c2aad6a49bc52af')
I don't know how the field created_at.09-10 20:57:27.798545 came up.
Thank you for advance four your advice, here is my source code:
main.go:
package main
import (
"context"
"example/customdate/user"
"fmt"
"log"
)
func main() {
ctx := context.Background()
repo := user.NewRepository(ctx)
u1 := user.NewUser("Max")
u1, err := repo.Add(ctx, u1)
if err != nil {
log.Fatalln(err)
}
fmt.Println(u1)
}
user/repository.go:
package user
import (
"context"
"log"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type Repository interface {
Get(ctx context.Context) ([]User, error)
Add(ctx context.Context, user *User) (*User, error)
}
type repository struct {
client *mongo.Client
}
func NewRepository(ctx context.Context) repository {
c, err := mongo.NewClient(options.Client().ApplyURI("mongodb://127.0.0.1:27017/"))
if err != nil {
log.Fatalln(err)
}
err = c.Connect(ctx)
if err != nil {
log.Fatal(err)
}
return repository{
client: c,
}
}
func (r *repository) Get(ctx context.Context) ([]User, error) {
result, err := r.client.Database("db").Collection("user").Find(ctx, map[string]interface{}{})
if err != nil {
return nil, err
}
var users []User
if err = result.All(ctx, &users); err != nil {
return nil, err
}
return users, nil
}
func (r *repository) Add(ctx context.Context, user *User) (*User, error) {
result, err := r.client.Database("db").Collection("user").InsertOne(ctx, user)
if err != nil {
return nil, err
}
user.ID = result.InsertedID
return user, nil
}
user/dto.go:
package user
import "time"
type CustomTime struct {
time.Time
}
func (t *CustomTime) MarshalBSON() ([]byte, error) {
str := t.Format("2006-01-02 15:04:05.999999")
return []byte(str), nil
}
func (t *CustomTime) UnmarshalBSON(raw []byte) error {
parsed, err := time.Parse("2006-01-02 15:04:05.999999", string(raw))
if err != nil {
return err
}
t = &CustomTime{parsed}
return nil
}
func Now() CustomTime {
return CustomTime{time.Now()}
}
type User struct {
ID interface{}
Name string `bson:"name"`
CreatedAt CustomTime `bson:"created_at"`
}
func NewUser(name string) *User {
return &User{
Name: name,
CreatedAt: Now(),
}
}

How to have mongodb decode struct passed into function

The Decode step of the following code does not populate the original document object correctly. It overwrites it with a bson object.
func main() {
c := Call{}
dbGetObject("collection", &c)
}
func dbGetObject(collectionName string, document interface{}) (err error) {
uri, creds, auth := dbGetAuth()
clientOpts := options.Client().ApplyURI(uri).SetAuth(creds)
client, err := mongo.Connect(context.TODO(), clientOpts)
if err != nil {
log.Fatal(err)
return err
}
defer client.Disconnect(context.TODO())
collection := client.Database(auth.Database).Collection(collectionName)
err = collection.FindOne(context.TODO(), bson.M{"number": "12345"}).Decode(&document)
if err != nil {
log.Fatal(err)
return err
}
return nil
}
Yet the following code does work properly:
func dbGetObject(collectionName string) (err error) {
uri, creds, auth := dbGetAuth()
clientOpts := options.Client().ApplyURI(uri).SetAuth(creds)
client, err := mongo.Connect(context.TODO(), clientOpts)
if err != nil {
log.Fatal(err)
return err
}
defer client.Disconnect(context.TODO())
collection := client.Database(auth.Database).Collection(collectionName)
c := Call{}
err = collection.FindOne(context.TODO(), bson.M{"number": "12345"}).Decode(&c)
if err != nil {
log.Fatal(err)
return err
}
return nil
}
The only difference being that the instance of the struct is passed into the function vs instantiated in the dbGetObject function. What am I doing wrong?
In the first example, the type of document is interface.
if you fix the argument type as below, it will work correctly.
func dbGetObject(collectionName string, document *Call)
I actually realized what is going on. In the call to the function and then to Decode I am passing a pointer. So, it's actually passing a pointer to a pointer into the Decode call. So the fix is to change the decode call from:
err = collection.FindOne(context.TODO(), bson.M{"number": "12345"}).Decode(&document)
to
err = collection.FindOne(context.TODO(), bson.M{"number": "12345"}).Decode(document)

how to create a mongo db package

I'd like to build an infrastructure, which is a package for the project. So that other developers can import this package to perform CRUD operations on the DB.
But I've got an error during the test:
type Students struct {
Name string
Age int
}
type InsertOneResult struct {
InsertedID interface{}
}
func dbGetOne(coll, document interface{}) (*InsertOneResult, error) {
...
}
func dbUpdateOne(coll, document interface{}) (*InsertOneResult, error) {
...
}
func dbDeleteOne(coll, document interface{}) (*InsertOneResult, error) {
...
}
func dbInsertOne(coll, document interface{}) (*InsertOneResult, error) {
res, err := coll.InsertOne(context.TODO(), document)
if err != nil {
log.Fatal(err)
}
return &InsertOneResult{InsertedID: res[0]}, err
}
func main() {
client, err := mongo.NewClient(options.Client().ApplyURI("mongodb://<user>:<password>#<host>:<port>/<dbname>"))
if err != nil {
log.Fatal(err)
}
ctx, _ := context.WithTimeout(context.Background(), 30*time.Second)
err = client.Connect(ctx)
if err != nil {
log.Fatal(err)
}
coll := client.Database("db").Collection("students")
data := Students{"Amy", 10}
res, err := dbInsertOne(coll, data)
if err != nil {
log.Fatal(err)
}
fmt.Printf("inserted document with ID %v\n", res.InsertedID)
}
Here's the error:
./main.go:24:18: coll.InsertOne undefined (type interface {} is interface with no methods)
Is there any way to solve this? Thanks in advance.
Hey it looks like the error could be coming from a type conversion issue. The solution would be to clearly define the type for coll as *mongo.Collection in the dbInsertOne() function. This allows the compiler at compile time to figure out the structure of the input instead of having to rely on an abstract interface.
func dbInsertOne(coll *mongo.Collection, document interface{}) (*InsertOneResult, error) {
res, err := coll.InsertOne(context.TODO(), document)
if err != nil {
log.Fatal(err)
}
return &InsertOneResult{InsertedID: res.InsertedID}, err
}
I would further suggest that the 2nd argument document should also be a typed known term if possible. e.g.
func dbInsertOne(coll *mongo.Collection, document Students)
Static typing will help quite a bit and clear up any confusion.

Deserialize cursor into array with mongo-go-driver and interface

I create an api using golang, i would like to create some functionnal test, for that i create an interface to abstract my database. But for that i need to be able to convert the cursor to an array without knowing the type.
func (self *KeyController) GetKey(c echo.Context) (err error) {
var res []dto.Key
err = db.Keys.Find(bson.M{}, 10, 0, &res)
if err != nil {
fmt.Println(err)
return c.String(http.StatusInternalServerError, "internal error")
}
c.JSON(http.StatusOK, res)
return
}
//THE FIND FUNCTION ON THE DB PACKAGE
func (s MongoCollection) Find(filter bson.M, limit int, offset int, res interface{}) (err error) {
ctx := context.Background()
var cursor *mongo.Cursor
l := int64(limit)
o := int64(offset)
objectType := reflect.TypeOf(res).Elem()
cursor, err = s.c.Find(ctx, filter, &options.FindOptions{
Limit: &l,
Skip: &o,
})
if err != nil {
return
}
defer cursor.Close(ctx)
for cursor.Next(ctx) {
result := reflect.New(objectType).Interface()
err := cursor.Decode(&result)
if err != nil {
panic(err)
}
res = append(res.([]interface{}), result)
}
return
}
Does someone have an idea?
You can call directly the "All" method:
ctx := context.Background()
err = cursor.All(ctx, res)
if err != nil {
fmt.Println(err.Error())
}
For reference:
https://godoc.org/go.mongodb.org/mongo-driver/mongo#Cursor.All
i think you want to encapsulate the Find method for mongo query.
Using the reflect package i have improved your code by adding an additional parameter that serves as a template to instantiate new instances of slice items.
func (m *MongoDbModel) FindAll(database string, colname string, obj interface{}, parameter map[string]interface{}) ([]interface{}, error) {
var list = make([]interface{}, 0)
collection, err := m.Client.Database(database).Collection(colname).Clone()
objectType := reflect.TypeOf(obj).Elem()
fmt.Println("objectype", objectType)
if err != nil {
log.Println(err)
return nil, err
}
filter := bson.M{}
filter["$and"] = []bson.M{}
for key, value := range parameter {
filter["$and"] = append(filter["$and"].([]bson.M), bson.M{key: value})
}
cur, err := collection.Find(context.Background(), filter)
if err != nil {
log.Fatal(err)
}
defer cur.Close(context.Background())
for cur.Next(context.Background()) {
result := reflect.New(objectType).Interface()
err := cur.Decode(result)
if err != nil {
log.Println(err)
return nil, err
}
list = append(list, result)
}
if err := cur.Err(); err != nil {
return nil, err
}
return list, nil
}
The difference is that FindAll method returns []interface{}, where err := cur.Decode(result) directly consumes a pointer like the result variable.

Golang Mongodb %!(EXTRA

I'm trying to marshal a struct into JSON and then insert it into my Mongo database, but keep on getting this error: %!(EXTRA main.Test={575590180 Me}). What am I doing wrong? I took this code exactly from another project I worked on which could insert documents without any problems.
package main
import (
"utils"
"hash/fnv"
"log"
"gopkg.in/mgo.v2"
"encoding/json"
)
type Test struct {
Id uint32
Name string
}
func ConnectDB() *mgo.Session {
session, err := mgo.Dial("localhost:27017")
if err != nil {
panic(err)
}
return session
}
func SaveMgoDoc(dbName string, collectionName string, file Test) bool {
session, err := mgo.Dial("localhost:27017")
if err != nil {
panic(err)
}
defer session.Close()
fileJson, err := json.Marshal(file)
if err != nil {
log.Printf("failed to marshal struct to json...\n", file)
return false
}
collection := session.DB(dbName).C(collectionName)
err = collection.Insert(&fileJson)
if err != nil {
log.Printf("failed to insert doc into database...\n", file)
return false
}
return true
}
func hash(s string) uint32 {
h := fnv.New32a()
h.Write([]byte(s))
return h.Sum32()
}
func main() {
utils.SaveMgoDoc("mydb", "mydoc", Test{hash("Me"), "Me"})
}
Insert expects a pointer to a struct, not a json string. So, in this case, just use:
err = collection.Insert(&file)