I am learning mongodb transactions using golang driver go.mongodb.org/mongo-driver/mongo
. I am following this SO answer and this example on github.
Example code given by @simagix:
if session, err = client.StartSession(); err != nil {
t.Fatal(err)
}
if err = session.StartTransaction(); err != nil {
t.Fatal(err)
}
if err = mongo.WithSession(ctx, session, func(sc mongo.SessionContext) error {
if result, err = collection.UpdateOne(sc, bson.M{"_id": id}, update); err != nil {
t.Fatal(err)
}
if result.MatchedCount != 1 || result.ModifiedCount != 1 {
t.Fatal("replace failed, expected 1 but got", result.MatchedCount)
}
// more interrelated operations ...
if err = session.CommitTransaction(sc); err != nil {
t.Fatal(err)
}
return nil
}); err != nil {
t.Fatal(err)
}
session.EndSession(ctx)
In both examples they do not rollback in case of error. I know this is a sample for demostration. But when I do the same in my code it works fine.
Is it okay to omit rollback in case of error (does the driver handle it)? Or am I missing something?
mongo.WithSession()
does not assume any active transactions, it "only" helps running a callback under the given session. So if you want it to be executed as part of your started transaction, you should deal with commits and aborts yourself. This allows finer control.
But if you're intention is to run a callback as a transaction, it's much simpler to use Session.WithTransaction()
as it handles the transaction and its life-cycle in a transparent way: it creates it and commits or aborts it based on the error returned by the callback. As an extra, it also handles retries. As its documentation also states:
If the callback fails, the driver will call AbortTransaction.
So here's a simple example how to execute a callback properly in a transaction:
var docToInsert, idToUpdate, updateDoc any
func doInTransactionExample(ctx context.Context, client *mongo.Client) error {
sess, err := client.StartSession(options.Session().SetDefaultReadConcern(readconcern.Majority()))
if err != nil {
return fmt.Errorf("client.StartSession() error: %w", err)
}
defer sess.EndSession(ctx)
result, err := sess.WithTransaction(
ctx,
func(sessCtx mongo.SessionContext) (any, error) {
// sessCtx must be used as context.Context for all operations to be run in the transaction.
var ctx context.Context = sessCtx // Shadow ctx on purpose!
c := client.Database("foo").Collection("bar")
// Insert example
if _, err := c.InsertOne(ctx, docToInsert); err != nil {
return nil, fmt.Errorf("InsertOne() failed: %w", err)
}
// Update example
if ur, err := c.UpdateByID(ctx, idToUpdate, updateDoc); err != nil {
return nil, fmt.Errorf("UpdateByID() failed: %w", err)
} else {
if ur.MatchedCount == 0 {
return nil, fmt.Errorf("UpdateByID() failed: %w", mongo.ErrNoDocuments)
}
}
return "arbitrary-result-to-return", nil
},
options.Transaction().SetReadPreference(readpref.PrimaryPreferred()),
)
if err != nil {
return fmt.Errorf("sess.WithTransaction() error: %w", err)
}
_ = result // Not using result
return nil
}