How can I change a MongoDB field to a single element array?
Asked Answered
H

9

29

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.

Heptachord answered 13/9, 2011 at 11:52 Comment(0)
F
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.

Fiona answered 13/9, 2011 at 14:47 Comment(3)
Do you think the map reduce in mongo will be faster?Heptachord
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).Fiona
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. 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().Dunbar
V
19

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).

Vespine answered 13/6, 2019 at 19:27 Comment(3)
Doesn't this also need a corresponding $unset to remove the existing field?Pup
@M.Justin is there a field you need to unset? $set creates or in this case replaces the specified field.Vespine
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.Pup
I
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);
Inventory answered 24/9, 2012 at 0:54 Comment(1)
small fix: x.jobLocationCity = [ x.jobLocationCity ];Vegetate
V
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/

Vegetate answered 14/1, 2013 at 9:7 Comment(0)
F
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.

Fiona answered 13/9, 2011 at 14:47 Comment(3)
Do you think the map reduce in mongo will be faster?Heptachord
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).Fiona
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. 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().Dunbar
S
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

Selffulfillment answered 24/1, 2012 at 9:49 Comment(1)
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()Slum
P
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.

Podiatry answered 14/9, 2011 at 7:44 Comment(0)
N
0

I think that's the correct answer:

db.collection.update({},[{$set:{field:["$field"]}}],{ multi: true })
Nicks answered 9/11, 2020 at 14:34 Comment(1)
@XavierGuihot Nope, isn't the same, check the curly brackets near to (field:) and compare it with my answerNicks
E
0

SOLVED for MongoDB 4.4.

db.products.find( { "images" : { $type : 2 } } ).forEach( function (x) { x.images = [ x.images ]; db.products.save(x); });
Egocentrism answered 9/7, 2021 at 11:16 Comment(0)
C
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.

Charqui answered 9/8, 2023 at 5:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.