loading...

MongoDB cross DB update

greenroommate profile image Haris Secic ・2 min read

Why?

I found myself in funny situation:

  1. Spring Data for MongoDB adds _class field to anything
  2. I use interface as a property in some cases
  3. Interface relies on @JsonSubTypes from Jackson so I can easily have anything in DB as that property and let Jackson and Spring do the rest
  4. I refactored code changing package names
  5. Why write about it here? I need somewhere to put this piece of code so I don't have to Google if I do the same mistake again.

The problem

First of all, problem wasn't in the fact that aggregating class had different package name. No, in fact it works with main class easily as it's linked to a collection and there's only 1 specific class used for that particular collection. Issue is with this dynamic property supporting many different implementations while Spring is unable to detect what to instantiate since the _class field contains incorrect value. I got something like this:

org.springframework.data.mapping.model.MappingInstantiationException: Failed to instantiate com.your.old.package.name using constructor NO_CONSTRUCTOR with arguments

I did spend a lot of time trying to guess what was updated and what to change in code and somehow, it clicked. I forgot how I remembered it but I know that I just went straight to DB and started changing some values. It's easy for 1 collection in 1 database you put in

db.someCollection.updateMany(...)

and viola. But I have multitenant system. And yes I do use DB(s) per customer. Good thing only Mongo was affected by this as I do have more than 1 DB system for each customer.

So I wanted to update all of this by just one single command (not line necessarily)

The solution

Minimal version 4.2 for updateMany() with $set option

db.adminCommand( { listDatabases: 1 } ).databases.map(x => x.name).forEach(dbn => {
    let dbc = db.getSiblingDB(dbn)
    dbc.geoJsonFeature.updateMany(
        {"dynamicProperty._class": {$regex : /.*redp\.GeoPolygon.*/}},
        {$set: { "dynamicProperty._class": "some.package.name.ImplementingClass"}}
    )
})

Let's break it down a bit

  • db.adminCommand( { listDatabases: 1 } ) will return object containing list of databases so adding .databases will list databases as simple objects.
  • map the name only
  • use the for each to loop through all names
  • db.getSiblingDB(dbn) will switch which DB is used
  • updateMany with $set because replaceAll and replaceOne are from 4.4 version

It's a bit nasty "solution" but if you need to update multi tenant system where you need something trivial and all tenants have same schema for a certain collection this is quick and dirty solution for it.

You can also combine it with

db.getCollectionNames().forEach(c => {
    db.getCollection(c)...
})

to go wild :D.

That's all

Posted on by:

greenroommate profile

Haris Secic

@greenroommate

software developer doing some architecture

Discussion

markdown guide