MongoDB | Go using a pipeline to listen to updates on a document by id - mongodb

I'm trying to make a function that watches the database for a certain document with a certain id to update but it does not work. It just stays alive while updating the document while the function should return. I've tried multiple things and the rest of the code works fine. When i remove the id part and listen for all document updates in that collection the function does as it should
func iterateChangeStream(routineCtx context.Context,stream *mongo.ChangeStream, chn chan string) {
defer stream.Close(routineCtx)
for stream.Next(routineCtx) {
var data bson.M
if err := stream.Decode(&data); err != nil {
fmt.Println(err)
}
chn <- "updated"
err := stream.Close(routineCtx)
if err != nil {
return
}
return
}
return
}
func (s Storage) ListenForScannerUpdateById(id primitive.ObjectID) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
chn := make(chan string)
coll := s.db.Collection("scanners")
scan, err := s.GetScannerById(id)
fmt.Println(scan)
matchPipeline := bson.D{
{
"$match", bson.D{
{"operationType", "update"},
{"fullDocument._id", bson.D{
{"$eq", id},
}},
},
},
}
scannerStream, err := coll.Watch(ctx, mongo.Pipeline{matchPipeline})
if err != nil {
err := scannerStream.Close(ctx)
if err != nil {
panic( err)
}
fmt.Printf("err: %v", err)
}
routineCtx, _ := context.WithCancel(context.Background())
go iterateChangeStream(routineCtx, scannerStream, chn)
msg, _ := <- chn
defer close(chn)
fmt.Println(msg)
return
}

Ok, so after reading the documentation for a seccond time i found this:
For update operations, this field only appears if you configured the change stream with fullDocument set to updateLookup. This field then represents the most current majority-committed version of the document modified by the update operation. This document may differ from the changes described in updateDescription if other majority-committed operations modified the document between the original update operation and the full document lookup.
so after setting the fullDocument option to updateLookup like this it works perfect:
scannerStream, err := coll.Watch(ctx, mongo.Pipeline{matchPipeline}, options.ChangeStream().SetFullDocument(options.UpdateLookup))

Related

error from convert ObjectIDFromHex from mongodb

I have a problem finding an objectid through the query param called id.
I can see the id that arrives at the function until the moment of doing the query.
But when I try to use ObjectIDFromHex it returns 00000000000000000000000 and doesn't get the document from mongodb.
I'll leave a screenshot so you can see the full problem.
screenshot with IDE
The code is this.
func RetrieveUser(ID string) (models.User, error) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
db := MongoCN.Database("mydatabase")
col := db.Collection("users")
var user models.User
objID, _ := primitive.ObjectIDFromHex(ID)
fmt.Println(ID)
fmt.Println(objID)
condition := bson.M{
"_id": objID,
}
err := col.FindOne(ctx, condition).Decode(&user)
user.Password = ""
if err != nil {
fmt.Println("User not found" + err.Error())
return user, err
}
return user, nil
}

Mongodb doesn't retrieve all documents in a collection with 2 million records using cursor

I have a collections of 2,000,000 records
> db.events.count(); │
2000000
and I use golang mongodb client to connect to the database
package main
import (
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27888").SetAuth(options.Credential{
Username: "mongoadmin",
Password: "secret",
}))
if err != nil {
panic(err)
}
defer func() {
if err = client.Disconnect(ctx); err != nil {
panic(err)
}
}()
collection := client.Database("test").Collection("events")
var bs int32 = 10000
var b = true
cur, err := collection.Find(context.Background(), bson.D{}, &options.FindOptions{
BatchSize: &bs, NoCursorTimeout: &b})
if err != nil {
log.Fatal(err)
}
defer cur.Close(ctx)
s, n := runningtime("retrive db from mongo and publish to kafka")
count := 0
for cur.Next(ctx) {
var result bson.M
err := cur.Decode(&result)
if err != nil {
log.Fatal(err)
}
bytes, err := json.Marshal(result)
if err != nil {
log.Fatal(err)
}
count++
msg := &sarama.ProducerMessage{
Topic: "hello",
// Key: sarama.StringEncoder("aKey"),
Value: sarama.ByteEncoder(bytes),
}
asyncProducer.Input() <- msg
}
But the the program only retrives only about 600,000 records instead of 2,000,000 every times I ran the program.
$ go run main.go
done
count = 605426
nErrors = 0
2020/09/18 11:23:43 End: retrive db from mongo and publish to kafka took 10.080603336s
I don't know why? I want to retrives all 2,000,000 records. Thanks for any help.
Your loop fetching the results may end early because you are using the same ctx context for iterating over the results which has a 10 seconds timeout.
Which means if retrieving and processing the 2 million records (including connecting) takes more than 10 seconds, the context will be cancelled and thus the cursor will also report an error.
Note that setting FindOptions.NoCursorTimeout to true is only to prevent cursor timeout for inactivity, it does not override the used context's timeout.
Use another context for executing the query and iterating over the results, one that does not have a timeout, e.g. context.Background().
Also note that for constructing the options for find, use the helper methods, so it may look as simple and as elegant as this:
options.Find().SetBatchSize(10000).SetNoCursorTimeout(true)
So the working code:
ctx2 := context.Background()
cur, err := collection.Find(ctx2, bson.D{},
options.Find().SetBatchSize(10000).SetNoCursorTimeout(true))
// ...
for cur.Next(ctx2) {
// ...
}
// Also check error after the loop:
if err := cur.Err(); err != nil {
log.Printf("Iterating over results failed: %v", err)
}

Cursor option required

I am using Go and MongoDB for my Project.I used
db.collection.aggregate([{$match:{}},{$lookup:{}},{$addFields:{}}])
it is working fine in MongoDB, but when I use pipeline in Go, it gives below error
The 'cursor' option is required, except for aggregate with the explain argument
Go code is
matchStage:=bson.M{"$match":bson.M{}}
pipeline := collection.Pipe([]bson.M{matchStage})
err = pipeline.All(&resp)
This is not how you are supposed to implement Mongo-Aggregation query in Go.
It should be of the format
cursor, err := collection.Aggregate(
ctx,
mongo.Pipeline{<PIPELINE-STAGES>},
options.Aggregate().SetAllowDiskUse(true),
)
Hence your code should be:
ctx, _ = context.WithTimeout(context.Background(), 2*time.Second)
matchStage := bson.D{
{"$match", bson.D{}},
}
lookupStage := bson.D{
{"from", ""},
{"let": bson.D{{}}},
{"pipeline": bson.A{}},
{"as": ""},
}
addFieldsStage := bson.D{
{"$addFields", bson.D{}},
}
cursor, err := collection.Aggregate(
ctx,
mongo.Pipeline{matchStage, lookupStage, addFieldsStage},
options.Aggregate().SetAllowDiskUse(true), // Mongo-Aggregate options if any
)
if err != nil {
panic(err)
}
for cursor.Next(ctx) {
var cursorResult bson.M
err := cursor.Decode(&cursorResult) // I world recommend to decode it using a struct instead
if err != nil {
panic(err)
}
fmt.Printf("Decoded Cursor: %v", cursorResult)
}
err = cursor.Close(ctx)
if err != nil {
panic(err)
}
Note: I haven't tested the code in my local. So let me know in case of errors.

Find all documents in a collection with mongo go driver

I checked out the answer here but this uses the old and unmaintained mgo. How can I find all documents in a collection using the mongo-go-driver?
I tried passing a nil filter, but this does not return any documents and instead returns nil. I also checked the documentation but did not see any mention of returning all documents. Here is what I've tried with aforementioned result.
client, err := mongo.Connect(context.TODO(), "mongodb://localhost:27017")
coll := client.Database("test").Collection("albums")
if err != nil { fmt.Println(err) }
// we can assume we're connected...right?
fmt.Println("connected to mongodb")
var results []*Album
findOptions := options.Find()
cursor, err := coll.Find(context.TODO(), nil, findOptions)
if err != nil {
fmt.Println(err) // prints 'document is nil'
}
Also, I'm about confused about why I need to specify findOptions when I've called the Find() function on the collection (or do I not need to specify?).
Here is what I came up with using the official MongoDB driver for golang. I am using godotenv (https://github.com/joho/godotenv) to pass the database parameters.
//Find multiple documents
func FindRecords() {
err := godotenv.Load()
if err != nil {
fmt.Println(err)
}
//Get database settings from env file
//dbUser := os.Getenv("db_username")
//dbPass := os.Getenv("db_pass")
dbName := os.Getenv("db_name")
docCollection := "retailMembers"
dbHost := os.Getenv("db_host")
dbPort := os.Getenv("db_port")
dbEngine := os.Getenv("db_type")
//set client options
clientOptions := options.Client().ApplyURI("mongodb://" + dbHost + ":" + dbPort)
//connect to MongoDB
client, err := mongo.Connect(context.TODO(), clientOptions)
if err != nil {
log.Fatal(err)
}
//check the connection
err = client.Ping(context.TODO(), nil)
if err != nil {
log.Fatal(err)
}
fmt.Println("Connected to " + dbEngine)
db := client.Database(dbName).Collection(docCollection)
//find records
//pass these options to the Find method
findOptions := options.Find()
//Set the limit of the number of record to find
findOptions.SetLimit(5)
//Define an array in which you can store the decoded documents
var results []Member
//Passing the bson.D{{}} as the filter matches documents in the collection
cur, err := db.Find(context.TODO(), bson.D{{}}, findOptions)
if err !=nil {
log.Fatal(err)
}
//Finding multiple documents returns a cursor
//Iterate through the cursor allows us to decode documents one at a time
for cur.Next(context.TODO()) {
//Create a value into which the single document can be decoded
var elem Member
err := cur.Decode(&elem)
if err != nil {
log.Fatal(err)
}
results =append(results, elem)
}
if err := cur.Err(); err != nil {
log.Fatal(err)
}
//Close the cursor once finished
cur.Close(context.TODO())
fmt.Printf("Found multiple documents: %+v\n", results)
}
Try passing an empty bson.D instead of nil:
cursor, err := coll.Find(context.TODO(), bson.D{})
Also, FindOptions is optional.
Disclaimer: I've never used the official driver, but there are a few examples at https://godoc.org/go.mongodb.org/mongo-driver/mongo
Seems like their tutorial is outdated :/

bulkwrite does not support multi-document transaction by using mongo-go-driver

I's using mongo-go-driver 0.0.18 to build a bulk write which is consisted of a "NewUpdateManyModel" and several "NewInsertOneModel". My mongo server is atlas M10 with replica sets. I built some goroutines to test if the transactions are atomic, the result shows that each bulk write is not atomic, they would interfered with each other. I am wondering if mongo-go-driver supports for multi-document transaction?
func insertUpdateQuery(counter int, col *mongo.Collection, group *sync.WaitGroup){
var operations []mongo.WriteModel
var items = []item{}
items=append(items,item{"Name":strconv.Itoa(counter),"Description":"latest one"})
for _,v := range items{
operations = append(operations, mongo.NewInsertOneModel().Document(v))
}
updateOperation := mongo.NewUpdateManyModel()
updateOperation.Filter(bson.D{
{"Name", bson.D{
{"$ne", strconv.Itoa(counter)},
}},
})
updateOperation.Update(bson.D{
{"$set", bson.D{
{"Description", strconv.Itoa(counter)},
}},
},)
operations = append(operations,updateOperation)
bulkOps:=options.BulkWrite()
result, err := col.BulkWrite(
context.Background(),
operations,
bulkOps,
)
if err != nil{
fmt.Println("err:",err)
}else{
fmt.Printf("IU: %+v \n",result)
}
group.Done()
}
func retrieveQuery(group *sync.WaitGroup, col *mongo.Collection){
var results []item
qctx:=context.Background()
qctx, c := context.WithTimeout(qctx, 10*time.Second)
defer c()
cur, err := col.Find(qctx, nil)
if err != nil {
log.Fatal(err)
}
defer cur.Close(context.Background())
res := item{}
for cur.Next(context.Background()) {
err := cur.Decode(&res)
if err != nil {
log.Println(err)
}else {
results=append(results,res)
}
}
if err := cur.Err(); err != nil {
log.Println(err)
}
fmt.Println("res:",results)
group.Done()
}
func main() {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx,10*time.Second)
defer cancel()
uri := "..."
client, err := mongo.NewClient(uri)
if err != nil {
fmt.Printf("todo: couldn't connect to mongo: %v", err)
}
defer cancel()
err = client.Connect(ctx)
if err != nil {
fmt.Printf("todo: mongo client couldn't connect with background context: %v", err)
}
col:=client.Database("jistest").Collection("Rules")
wg :=&sync.WaitGroup{}
for i:=0; i<100; i++{
wg.Add(2)
go insertUpdateQuery(i,col,wg)
go retrieveQuery(wg,col)
}
wg.Wait()
fmt.Println("All Done!")
}
I am wondering if mongo-go-driver supports for multi-document transaction?
mongo-go-driver does support multi-document transaction since v0.0.12 (currently in beta version 0.1.0).
MongoDB multi-document transactions are associated with a session. That is, you start a transaction for a session. When using any of MongoDB officially supported drivers, you must pass the session to each operation in the transaction.
Your example does not seem to utilise session nor transactions. An example of multi-document transaction in mongo-go-driver (v0.1.0) is as below:
client, err := mongo.NewClient("<MONGODB URI>")
if err != nil { return err }
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
err = client.Connect(ctx)
if err != nil { return err }
session, err := client.StartSession()
database := client.Database("databaseName")
collection := database.Collection("collectionName")
err = mongo.WithSession(ctx, session, func(sctx mongo.SessionContext) error {
// Start a transaction in the session
sctx.StartTransaction()
var operations []mongo.WriteModel
// Create an insert one operation
operations = append(operations,
mongo.NewInsertOneModel().Document(
bson.D{{"Name", counter},
{"Description", "latest"}}))
// Create an update many operation
updateOperation := mongo.NewUpdateManyModel()
updateOperation.Filter(bson.D{{"Name", bson.D{
{"$ne", counter},
}}})
updateOperation.Update(bson.D{
{"$set", bson.D{
{"Description", counter},
}},
})
operations = append(operations, updateOperation)
// Execute bulkWrite operation in a transactional session.
_, err := collection.BulkWrite(sctx, operations)
if err != nil {
fmt.Println(err)
return err
}
// Committing transaction
session.CommitTransaction(sctx)
return nil
})
session.EndSession(ctx)
See also Transactions and Retryable Writes for examples to retry transactions.
I built some goroutines to test if the transactions are atomic
Just be mindful about how the processes are executed. For example, depending on the racing condition, you may get the latest performance overwriting the result. i.e.
transaction 1 finished
transaction 2 finished
transaction 3 and transaction 4 conflict
transaction 5 finished
...