arraysmongodbgo

Saving a slice of strings to MongoDB


I am trying to add a userID to a likes field in MongoDB. This field will be an array of strings []string . It is currently null, but that should not be an issue since was using "$set". I also tried manually making the fields an array and using $setOnInsert and $addToSet instead of $setwith no avail.

I am creating the document using this struct:

comment := entity.Comment{
    PostID:    postID,
    Content:   content,
    AuthorID:  authorID,
    Likes:     []string{},
    Dislikes:  []string{},
    Votes:     []string{},
    CreatedAt: time.Now(),
}

The document in question looks like this in the database:

{"_id":{"$oid":"67de0487653132119ba14548"},"postid":"67de00a0eea2fe8f5a55e1da","content":"awesome Pizza day","authorid":"3204aa18-2006-4aa1-ad88-1b71256d8a36","createdat":{"$date":{"$numberLong":"1742603399198"}},"updatedat":{"$date":{"$numberLong":"1742603992797"}},"likes":null,"dislikes":null,"votes":null}

and right now I am trying to add a userID string to "likes":null,"dislikes":null.

It fails with a write exception at UpdateOne. The userID and comment.ObjectIdare not null and the comment.ObjectId does exist in the database. There is only one comment at the moment.

func (r *CommentsRepository) ToggleLikeComment(ctx context.Context, comment *entity.Comment, userID string) error {
    filter := bson.D{{Key: "_id", Value: comment.ObjectId}}
    update := bson.D{}

    // Check if user already disliked
    dislikeFilter := bson.D{{Key: "_id", Value: comment.ObjectId}, {Key: "dislikes", Value: userID}}
    dislikeCount, err := r.collection.CountDocuments(ctx, dislikeFilter)
    if err != nil {
        return err
    }
    if dislikeCount > 0 {
        // Remove dislike
        update = append(update, bson.E{Key: "$pull", Value: bson.D{{Key: "dislikes", Value: userID}}})
    }

    // Check if user already liked
    likeFilter := bson.D{{Key: "_id", Value: comment.ObjectId}, {Key: "likes", Value: userID}}
    likeCount, err := r.collection.CountDocuments(ctx, likeFilter)
    if err != nil {
        return err
    }
    if likeCount > 0 {
        // Remove like
        update = append(update, bson.E{Key: "$pull", Value: bson.D{{Key: "likes", Value: userID}}})
    } else {
        // Add like
        update = append(update, bson.E{Key: "$set", Value: bson.D{{Key: "likes", Value: userID}}})
    }
    update = append(update, bson.E{Key: "$set", Value: bson.D{{Key: "updatedat", Value: time.Now()}}})

    // fails to write here...
    _, err = r.collection.UpdateOne(ctx, filter, bson.D{{Key: "$set", Value: update}})
    return err
}

I was able to adapt the code with the help @aneroid. Here is the query that ended up working

likeFilter := bson.D{
        {Key: "_id", Value: objectID},
        {Key: "likes", Value: bson.D{{Key: "$ne", Value: userID}}},
    }
    likeUpdate := bson.A{
        bson.D{{Key: "$set", Value: bson.D{
            {Key: "likes", Value: bson.D{
                {Key: "$cond", Value: bson.D{
                    {Key: "if", Value: bson.D{{Key: "$eq", Value: bson.A{"$likes", nil}}}},
                    {Key: "then", Value: bson.A{userID}},
                    {Key: "else", Value: bson.D{{Key: "$concatArrays", Value: bson.A{"$likes", bson.A{userID}}}}},
                }},
            }},
            {Key: "updatedat", Value: time.Now()},
        }}},
    }
    result, err := r.collection.UpdateOne(ctx, likeFilter, likeUpdate)

Solution

  • Just to cover it off, if likes/dislikes is null, then you can't do a $push, as you've noted in your question.

    So in this case, you can use an update aggregation which checks for likes being null using $cond. Additionally, since you probably don't want repeat likes by the same user, add that to the update filter.

    (You can use MongoDB Compass to convert these to Golang.)

    db.collection.update({
      _id: ObjectId("67de0487653132119ba14548"),
      likes: { $ne: "theNewUser" }  // prevent repeat likes
    },
    [
      {
        $set: {
          likes: {
            $cond: {
              if: { $eq: ["$likes", null] },
              then: ["theNewUser"],  // array of single user
              else: { $concatArrays: ["$likes", ["theNewUser"]] }  // add the new user
            }
          }
        }
      }
    ])
    

    Mongo Playground to add a like. It's got examples of docs which have likes as: (a) null, (b) empty array, (c) array with other userIDs, (d) array with the new UserID already in it, which won't add it to the array again.

    Adding a dislike would be similar to that.

    For removing a like (or dislike), use $filter to keep only the values which are not equal to the new user ID:

    db.collection.update({
      _id: ObjectId("67de0487653132119ba14548"),
      likes: { $eq: "theNewUser" }  // remove only if user in likes
    },
    [
      {
        $set: {
          likes: {
            $filter: {
              input: "$likes",
              cond: { $ne: ["$$this", "theNewUser"] }
            }
          }
        }
      }
    ])
    

    Mongo Playground to remove a like. This one also has examples of docs which have likes as: (a) null, (b) empty array, (c) array with other userIDs, (d) array with the new User ID already in it and other users, (e) array with only the new User ID.

    Btw, since the filters already check for the new User already in or not in the likes array, you don't need to do the pre-like or pre-dislike check.

    Obviously, it would be easier if you bulk updated all the null likes and dislikes to be an empty array - so that $push and $pull would work as expected in a regular update query.