Efficient transaction wrapper function with mongodb go driver

Viewed 61

I'm currently integrating the transaction logic into my go+mongodb api.

I already created this example endpoint. It allows you to retrieve a user document and send it back to the client with json encoding.

func GetUser(w http.ResponseWriter, r *http.Request) {
    id := mux.Vars(r)["user-id"]
    objectId, _ := primitive.ObjectIDFromHex(id)

    user, err := UserById(objectId)
    if err != nil {
        // ...
    }

    // do some stuff with the user, whatever

    // eventually send it back
    json.NewEncoder(w).Encode(user)
}

func UserById(id primitive.ObjectID) (models.StoredUser, error) {
    session, err := mongoClient.StartSession()
    if err != nil {
        return models.StoredUser{}, fmt.Errorf("failed starting session for transaction | %s", err.Error())
    }
    defer session.EndSession(context.TODO())

    callback := func(ctx mongo.SessionContext) (any, error) {
        res := usersCollection.FindOne(
            ctx,
            bson.M{
                "_id": id,
            },
        )

        if res.Err() != nil {
            return models.StoredUser{}, fmt.Errorf("failed querying db | %s", res.Err())
        }

        return res, nil
    }

    result, err := session.WithTransaction(context.TODO(), callback)
    if err != nil {
        return models.StoredUser{}, fmt.Errorf("failed executing transaction | %s", err.Error())
    }

    asserted := result.(*mongo.SingleResult)
    var ret models.StoredUser
    if err := asserted.Decode(&ret); err != nil {
        return models.StoredUser{}, fmt.Errorf("failed parsing user data in struct | %s", err.Error())
    }

    return ret, nil
}

Here are the big steps :

  1. Parse the request content to get the user id
  2. Create a session to perform the transaction
  3. Declare the callback function using the id argument
  4. Call the callback function from a transaction
  5. Get back the *mongo.SingleResult as an interface{} and parsing it back to its original type
  6. Decode the bson document contained in the *mongo.SingleResult to put it in the return struct

This function works, but is very verbose. The code is very duplicated.

I wonder if there is a way of not repeating the same code for each function I wanna make. My previous wrapper function attempts didn't lead to anything, as I actually need the variables where they are now at each call.

Still, the steps 2 and 5 especially seem very redundant and inefficient.

Anyone got any idea ?

1 Answers

Ok I found the following :

func Transaction(callback func(ctx mongo.SessionContext) (any, error)) (any, error) {
    session, err := mongoClient.StartSession()
    if err != nil {
        return nil, fmt.Errorf("failed creating session | %s", err.Error())
    }

    defer session.EndSession(context.TODO())

    res, err := session.WithTransaction(ctx, callback)
    if err != nil {
        return nil, fmt.Errorf("failed executing transaction | %s", err.Error())
    }

    return res, nil
}

Let's say i want then to fetch user objects :


func GetUsers() ([]models.User, error) {
    callback := func(ctx mongo.SessionContext) (any, error) {
        res, err := usersCollection.Find(ctx, bson.M{})
        if err != nil {
            return nil, fmt.Errorf("failed querying users collection | %s", err.Error())
        }

        var ret []models.User
        if err := res.All(context.TODO(), &ret); err != nil {
            return nil, fmt.Errorf("failed parsing results in struct | %s", err.Error())
        }

        return ret, nil
    }

    result, err := Transaction(callback)
    if err != nil {
        return []models.User{}, fmt.Errorf("failed executing transaction | %s", err.Error())
    }

    classes, _ := result.([]models.StoredClass)
    return classes, nil
}

Related