26

I have a field in mongodb that's a string: {"field": "some text"}. I want to convert them all into single-element arrays containing the string: {"field": ["some text"]}.

I know I can just loop through all the documents, get the field, then update, but I'm wondering if there's a cleaner way.

Xavier Guihot
  • 54,987
  • 21
  • 291
  • 190
Harry
  • 52,711
  • 71
  • 177
  • 261

9 Answers9

16

Starting in Mongo 4.2, db.collection.update() can accept an aggregation pipeline, finally allowing the update of a field based on its current value:

// { field: "some text" }
db.collection.updateMany(
  {},
  [{ $set: { field: ["$field"] } }]
)
// { field: [ "some text" ] }
  • The first part {} is the match query, filtering which documents to update (in this case all documents).

  • The second part [{ $set: { field: { ["$field"] } } }] is the update aggregation pipeline (note the squared brackets signifying the use of an aggregation pipeline). $set (alias of $addFields) is a new aggregation operator which in this case replaces the field's value (simply wrapping it into an array). Note how field is modified directly based on its own value ($field).

Xavier Guihot
  • 54,987
  • 21
  • 291
  • 190
  • Doesn't this also need a corresponding `$unset` to remove the existing field? – M. Justin Jun 08 '23 at 22:55
  • 1
    @M.Justin is there a field you need to unset? `$set` creates or in this case replaces the specified field. – Xavier Guihot Jun 09 '23 at 05:28
  • My bad, I somehow got it in my head that it was a field rename + a type change (since that's what I'm currently dealing with). In my case, I needed the unset. For this question, you're quite right that `$set` will replace the existing field. – M. Justin Jun 09 '23 at 06:26
9

Nitin Garg's answer above almost works, except his example converts from a string to a hash, NOT a string to an array.

Taking into account Joel Harris's comments, the proper solution would look like:

db.jobs.find( { "jobLocationCity" : { $type : 2 } } ).snapshot().forEach( function (x) {
    x.jobLocationCity = [ jobLocationCity ];
    db.jobs.save(x);
});

Or if using db.eval:

function f() {
    db.jobs.find( { "jobLocationCity" : { $type : 2 } } ).snapshot().forEach( function (x) {
        x.jobLocationCity = [ jobLocationCity ];
        db.jobs.save(x);
    });
}
db.eval(f);
Joe Chen
  • 502
  • 6
  • 5
6

Actually, the find( { "jobLocationCity" : { $type : 2 } } ) will not work properly, because if you'll run update script next time, it will treat ['mystring'] elements again as string type.

You should use something like this to prevent it:

db.message_info.find( { "jobLocationCity" : { $type : 2 } }  ).snapshot().forEach(
  function (x) {
    if (!Array.isArray(x.jobLocationCity)){
        x.jobLocationCity = [ x.jobLocationCity  ];
        db.jobs.save(x);
    }
  }
)

see http://docs.mongodb.org/manual/reference/operators/

Vitamon
  • 538
  • 7
  • 18
3

You could do it in a Reduce function of map/reduce to keep all the processing in mongodb. Essentially you would use map/reduce to put the results into a new collection and then you could copy them back to the old collection (or delete old one and rename the new one). This has the advantage of keeping everything inside of mongo.

Update: Another option might be for you to use db.eval for this. db.eval executes on the server so the updates would be done on the server without any traffic/latency.

I think the only other option is as you described - do it on the client by querying and updating each one.

Scott Willeke
  • 8,884
  • 1
  • 40
  • 52
  • Do you think the map reduce in mongo will be faster? – Harry Sep 13 '11 at 18:58
  • Mostly it depends on how many documents and how much data you're moving between the server & client. If there is a lot of data moving between server/client then you want to keep the processing inside of MongoDB with mapReduce or try db.eval (as I just added in an edit above). – Scott Willeke Sep 13 '11 at 19:48
  • 1
    Assuming the original poster is already working in the MongoDB shell, the reason to use `db.eval()` would be so that the operation is performed atomically. [By default, `db.eval()` takes a global write lock while evaluating the JavaScript function](http://docs.mongodb.org/manual/reference/method/db.eval/). If the code is actually running outside the MongoDB server JS thread (in a Node.js or Meteor app), then yes, less traffic/latency using `db.eval()`. – Adam Monsen Jan 07 '15 at 00:37
2

try this instead

This is to change the type of a field from string to array in mongoDB

db.jobs.find( { "jobLocationCity" : { $type : 2 } } ).forEach( function (x) {
    x.jobLocationCity = {"Location":x.jobLocationCity};
    db.jobs.save(x);
});

see the link for $type's possible values

Nitin Garg
  • 39
  • 7
  • This answer led me to a solution that I feel is very succinct. The problem I had with the sample provided here is that because a call to save() is being made within the forEach loop, the cursor was getting messed up and the function would get called multiple times for the same document. The solution is to call snapshot() before the foreach: db.jobs.find(blah).snapshot().forEach() – Joel Harris Aug 03 '12 at 19:00
0

I think that's the correct answer:

db.collection.update({},[{$set:{field:["$field"]}}],{ multi: true })
adriangc
  • 157
  • 1
  • 1
  • 6
  • @XavierGuihot Nope, isn't the same, check the curly brackets near to (field:) and compare it with my answer – adriangc Nov 09 '20 at 14:51
0

SOLVED for MongoDB 4.4.

db.products.find( { "images" : { $type : 2 } } ).forEach( function (x) { x.images = [ x.images ]; db.products.save(x); });
Flavio Troia
  • 2,451
  • 1
  • 25
  • 27
0

but I'm wondering if there's a cleaner way..

The short answer is no.

MongoDB does not have any single operation or command to perform a "change type".

The quickest way to do this is likely to use one of the drivers and make the change. You can use the shell and write a for loop, but in terms of raw speed, the other drivers may be faster.

That stated, the slowest part of the process is going to be loading all of the data from disk into memory to be changed and then flushing that data back to disk. This would be true even with a magic "change type" command.

Gates VP
  • 44,957
  • 11
  • 105
  • 108
0

UPDATED ANSWER AUGUST 2023

MONGODB V7.0

1- Query 01

The save() function is depreciated in new MongoDB releases, and it won't work.So instead of save() we can use update() in the following way

Let's assume our collection name is products

db.products.find({}).forEach( function(x)
{
  x.field = [ x.field ];
  db.products.update( { _id : x._id } , { $set:{ field : x.field } } ); 
});

The field is the property of the schema you want to convert from string to string[], and want to add the old present values into new string[]

When you will run this query in mongosh, you will not get nay response, after running this query, you have to again run a find query to check the updated results, or if you have any GUI(i.e mongodb compass) ,you can check there as well.

02- Query 02

There's another workaround query with updateMany() , which will return some response after you run the query

db.products.updateMany({}, [
  {
    $set: {
      field: {
        $map: {
          input: ["A"], // A dummy array to iterate over once
          as: "el",
          in: "$field"
        }
      }
    }
  }
]);

1-$map: This is an aggregation operator that allows you to apply a specified expression to each element of an array. In this case, we are using it to create a new array field containing the value of the field.

2-input: ["A"]: This is a dummy array containing a single element ("A"). It's used to provide a source for the $map operation. Since we are not using the actual elements of the array, it serves as a placeholder.

03-as: "el": This defines an alias ("el") for the current element during the $map operation. In this context, it's not actually used, but it's required syntax for the $map operation.

04-in: "$field": This is the expression that specifies what to do with each element of the dummy array. Here, we are using the value of the existing field in each document to create a new array.

M Nouman
  • 437
  • 1
  • 5
  • 22