Hot questions for Spring Connector for MongoDB

Top 10 Java Open Source / Spring / Spring Connector for MongoDB

Question:

I am working with Spring MongoDb.

I create various entities using insert method: http://docs.spring.io/spring-data/mongodb/docs/current/api/org/springframework/data/mongodb/core/MongoOperations.html#insert-java.lang.Object-

However, all methods return void. I need to return the ObjectId of the inserted document.

What is the best way to get it?


Answer:

This is pretty interesting and thought I would share. I just figured out the solution for this with the help of BatScream comment above:

You would create an object and insert it into your MongoDB:

    Animal animal = new Animal();
    animal.setName(name);
    animal.setCat(cat);

    mongoTemplate.insert(animal);

Your animal class looks like this with getters and settings for all fields:

public class Animal {

    @Id
    @JsonProperty
    private String id;
    @JsonProperty
    private String name;
    @JsonProperty
    private String cat;

    public String getId() {
        return id;
    }
}

AFTER you have done the insert under mongoTemplate.insert(animal);, you can actually call the method animal.getId() and it will return back the ObjectId that was created.

Question:

In MongoDB I would like to use $gt and $lt comparision operators where the value could be null. When the operators did not work with null, I looked for documentation but found none. In both cases it returned no documents (even though $ne, $gte, and $lte did return documents; meaning there were documents that were both equal to and not equal to null).

I would expect $gt to essentially operate like $ne (as the null type Mongo comarison order is so low) and $lt to return nothing for the same reason.

I was hoping this would work as the value I pass to the query is variable (potentially null), and I don't want to have to write a special case for null.

Example of what I was expeccting, given the following collection:

{
  id: 1,
  colNum: null
}
{
  id: 2,
  colNum: 72
}
{
  id: 3
}

I would expect the following query:

db.testtable.find( { "colNum" { $gt : null } } )

To return:

{
  id: 2,
  colNum: 72
}

However, nothing was returned.

Is there a reason that $gt and $lt don't seem to work with null, or is it a MongoDB bug, or is it actually supposed to work and there is likely a user error?


Answer:

Nitty-Gritty Details

Reading through the latest Mongo source, there's basically 2 cases when doing comparisons involving null:

  1. If the canonical types of the BSON elements being compared are different, only equality comparisons (==, >=, <=) of null & undefined will return true; otherwise any comparison with null will return false. Note: No other BSON type has the same canonical type as null.
  2. If the canonical types are the same (i.e., both elements are null), then compareElementValues is called. For null, this just returns the difference between the canonical type of both BSON elements and then carries out the requested comparison against 0. For example, null > null would translate into (5-5) > 0 --> False because the canonical type of null is 5. Similarly, null < null would translate into (5-5) < 0 --> False.

This means null can only ever be equal to null or undefined. Any other comparison involving null will always return false.

Is this a Bug?

Updated Answer:

The documentation for the comparison operators ($gt, $lt) references the documentation which you originally linked, which implies that the comparison operators should work with null. Furthermore, query sorting (i.e., db.find().sort()) does accurately follow the documented Comparison/Sort behavior.

This is, at the very least, inconsistent. I think it would be worth submitting a bug report to MongoDB's JIRA site.


Original Answer:

I don't think this behavior is a bug.

The general consensus for Javascript is that undefined means unassigned while null means assigned but otherwise undefined. Value comparisons against undefined, aside from equality, don't make sense, at least in a mathematical sense.

Given that BSON draws heavily from JavaScript, this applies to MongoDB too.

Question:

My enums are stored as int in mongodb (from C# app). Now in Java, when I try to retrieve them, it throws an exception (it seems enum can be converted from string value only). Is there any way I can do it?

Also when I save some collections into mongodb (from Java), it converts enum values to string (not their value/cardinal). Is there any override available?

This can be achieved by writing mongodb-converter on class level but I don't want to write mondodb-converter for each class as these enums are in many different classes.

So do we have something on the field level?


Answer:

After a long digging in the spring-mongodb converter code, Ok i finished and now it's working :) here it is (if there is simpler solution i will be happy see as well, this is what i've done ) :

first define :

public interface IntEnumConvertable {
      public int getValue();    

}

and a simple enum that implements it :

public enum tester implements IntEnumConvertable{   
    vali(0),secondvali(1),thirdvali(5);

    private final int val;
    private tester(int num)
    {
        val = num;          
    }
    public int getValue(){
        return val;
    }
}

Ok, now you will now need 2 converters , one is simple , the other is more complex. the simple one (this simple baby is also handling the simple convert and returns a string when cast is not possible, that is great if you want to have enum stored as strings and for enum that are numbers to be stored as integers) :

public class IntegerEnumConverters {
    @WritingConverter
    public static class EnumToIntegerConverter implements Converter<Enum<?>, Object> {
        @Override
        public Object convert(Enum<?> source) {
            if(source instanceof IntEnumConvertable)
            {
                return ((IntEnumConvertable)(source)).getValue();
            }
            else
            {
                return source.name();
            }               
        }
    }   
 }

the more complex one , is actually a converter factory :

public class IntegerToEnumConverterFactory implements ConverterFactory<Integer, Enum> {
        @Override
        public <T extends Enum> Converter<Integer, T> getConverter(Class<T> targetType) {
            Class<?> enumType = targetType;
            while (enumType != null && !enumType.isEnum()) {
                enumType = enumType.getSuperclass();
            }
            if (enumType == null) {
                throw new IllegalArgumentException(
                        "The target type " + targetType.getName() + " does not refer to an enum");
            }
            return new IntegerToEnum(enumType);
        }
        @ReadingConverter
        public static class IntegerToEnum<T extends Enum>  implements Converter<Integer, Enum> {
            private final Class<T> enumType;

            public IntegerToEnum(Class<T> enumType) {
                this.enumType = enumType;
            }

            @Override
            public Enum convert(Integer source) {
                  for(T t : enumType.getEnumConstants()) {
                      if(t instanceof IntEnumConvertable)
                      {
                          if(((IntEnumConvertable)t).getValue() == source.intValue()) {
                                return t;
                            }                         
                      }                     
                    }
                    return null;   
            }
        }
}

and now for the hack part , i personnaly didnt find any "programmitacly" way to register a converter factory within a mongoConverter , so i digged in the code and with a little casting , here it is (put this 2 babies functions in your @Configuration class)

      @Bean
        public CustomConversions customConversions() {
            List<Converter<?, ?>> converters = new ArrayList<Converter<?, ?>>();
            converters.add(new IntegerEnumConverters.EnumToIntegerConverter());     
// this is a dummy registration , actually it's a work-around because
// spring-mongodb doesnt has the option to reg converter factory.
// so we reg the converter that our factory uses. 
converters.add(new IntegerToEnumConverterFactory.IntegerToEnum(null));      
            return new CustomConversions(converters);
        }

    @Bean
    public MappingMongoConverter mappingMongoConverter() throws Exception {
        MongoMappingContext mappingContext = new MongoMappingContext();
        mappingContext.setApplicationContext(appContext);
        DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory());
        MappingMongoConverter mongoConverter = new MappingMongoConverter(dbRefResolver, mappingContext);        
        mongoConverter.setCustomConversions(customConversions());       
        ConversionService convService = mongoConverter.getConversionService();
        ((GenericConversionService)convService).addConverterFactory(new IntegerToEnumConverterFactory());                  
        mongoConverter.afterPropertiesSet();
        return mongoConverter;
    } 

Question:

I am setting up a MongoDB Spring MVC application and trying to use Service, DAO pattern.

I read the Spring-Data-MongoDB refernce here, but I am not understanding what is the differnce between MongoFactoryBean and SimpleMongoDbFactory.

What would be a better way, and why, to create a MongoTemplate bean.

@Configuration
public class SpringMongoConfig {
    public @Bean
    MongoDbFactory mongoDbFactory() throws Exception {
       return new SimpleMongoDbFactory(new MongoClient(), "yourdb");
    }

    public @Bean
    MongoTemplate mongoTemplate() throws Exception {
       MongoTemplate mongoTemplate = new MongoTemplate(mongoDbFactory());
       return mongoTemplate;
    }
}

OR.

@Bean
public MongoFactoryBean mongo() {
    MongoFactoryBean mongo = new MongoFactoryBean();
    mongo.setHost(env.getProperty("db.host"));
    mongo.setPort(env.getProperty("db.port",Integer.class,27017));
    return mongo;
}

@Bean
public MongoTemplate mongoTemplate() throws Exception{
    return new MongoTemplate(mongo().getObject(),env.getProperty("db.name"));
}

When do i use MongoFactoryBean and when do i use MongoDbFactory? Do they have different use cases?

Also what would be the best way to bootstrap MongoDB into Spring MVC, in such a way that its highly scaleable and configurable, and also provides provision to plug in any other RDBMS( for same or different functionalty). (Two different DAO impls for various DB types perhaps?)


Answer:

I am surprised this question has still not been answered. And though this might not be the exact answer you are looking for, here is my take on things. I have found that using the second approach, MongoFactoryBean, is the best approach.

Simply because there are more configuration options. For instance, if you want to set an Exception Translator, you can do so easily with MongoFactoryBean.

If I remember correctly, and I may be wrong, the MongoFactoryBean is meant for convenience for MongoDbFactory. Meaning, it adds another layer of abstraction.

All in all, go with the second approach.

Question:

I'm a new Mongodb and I have a problem with $lookup with java spring.

I would like to use this shell in Spring data

db.NewFeed.aggregate([
    {
        $match : {username : "user001"}
    },
    {
      $lookup:
        {
          from: "NewfeedContent",
          localField: "content.contentId",
          foreignField: "_id",
          as: "NewfeedContent"
        }
   }
])

I found on Google but no answer yet.


Answer:

Joining Two Collections with Spring Data MongoDB

Employee Class

class Employee {
    private String _id;
    private String name;
    private String dept_id;
}

Department Class

class Department {
    private String _id;
    private String dept_name;
}

Employee Result Class

public class EmpDeptResult {

    private String _id;
    private String name;
    private List<Object> departments;
}

EmployeeService Class

public class EmployeeService {

    @Autowired
    private MongoTemplate mongoTemplate;

    private Logger LOGGER = LoggerFactory.getLogger(EmployeeService.class);

    public void lookupOperation(){
    LookupOperation lookupOperation = LookupOperation.newLookup()
                        .from("Department")
                        .localField("dept_id")
                        .foreignField("_id")
                        .as("departments");

    Aggregation aggregation = Aggregation.newAggregation(Aggregation.match(Criteria.where("_id").is("1")) , lookupOperation);
        List<EmpDeptResult> results = mongoTemplate.aggregate(aggregation, "Employee", EmpDeptResult.class).getMappedResults();
        LOGGER.info("Obj Size " +results.size());
    }
}

Question:

I want to add orderby to the following repository method in mongodb with spring. I tried in various methods, but didnt work

public interface StageRepository extends MongoRepository<Stage, String> {

     @Query("{$and: [ { 'categoryId': { $eq: ?0 } }, { 'isDeleted': { $eq: ?1 } } ]}")
     public List<Stage> findByCategoryIdAndIsNotDeleted(String categoryId, Boolean deleted);

}

I want to add orderby 'order' in the query.

Not sure how to do it.


Answer:

You can do like :

@Query("{$and: [ { 'categoryId': { $eq: ?0 } }, { 'isDeleted': { $eq: ?1 } } ]}")
public List<Stage> findByCategoryIdAndIsNotDeleted(String categoryId, Boolean deleted,org.springframework.data.domain.Sort sort);

And When you call this method ,create Sort object like below:

Sort sort = new Sort(Direction.ASC/DESC,"order");

Question:

I have a mongo aggregate group query:

db.wizard.aggregate(
{
$group: {
    _id: "$title",
    versions: { $push: {version:"$version", author:"$author", dateAdded:"$dateAdded"}}
    }
})

I need this query in Java Spring-Data-MongoDB, my current solution looks like this:

    Aggregation agg = Aggregation.newAggregation(
            Aggregation.group("title").
                    push("version").as("versions")
    );

Problem is that i don't know how to add more fields to push method (version, author, dateAdded). Is it possible with Spring-Data-MongoDB?


Answer:

You can directly pass the BasicDbObject to any of the aggregation pipeline stage.

Aggregation agg = newAggregation(
            group("title").
            push(new BasicDBObject
                   ("version", "$version").append
                   ("author", "$author").append
                   ("dateAdded", "$dateAdded")).as("versions"));

Question:

I am trying to create dynamic query with user input with and operation My code is I created List of criteria like:

List<Criteria> criterias = new ArrayList<Criteria>();

and added criteria to this list.And its getting added successfully. now I want to make and operator between each criteria.

 Criteria criteria = new Criteria().andOperator(criterias.get(0), criterias.get(1));

It works fine But my input is not fixed so I want it should added dynamically, I tried like

for(int i=0;i<criterias.size();i++)
  Criteria criteria = new Criteria().andOperator(criterias.get(i));

where I am missing?


Answer:

To unite all criterias from a list of criterias by "$and" operator :

Criteria criteria = new Criteria().andOperator(criterias.toArray(new Criteria[criterias.size()]));

here is docs

Question:

To be direct, how do i do this:

group._id = { 
    year: { $year : [{ $subtract: [ "$timestamp", 25200000 ]}] }, 
    month: { $month : [{ $subtract: [ "$timestamp", 25200000 ]}] }, 
    day: { $dayOfMonth : [{ $subtract: [ "$timestamp", 25200000 ]}] }
};

with spring Data

I tried this already and some other forms and were not successfull

Aggregation aggregation = Aggregation.newAggregation(
                Aggregation.match(c),
                Aggregation.project("programa", "custo", "duracao", "dataHora")
                    .andExpression("dataHora").minus(25200000).extractDayOfMonth().as("dia")
                    .andExpression("dataHora").minus(25200000).extractMonth().as("mes")
                    .andExpression("dataHora").minus(25200000).extractYear().as("ano"),
                Aggregation.group("programa", "ano", "mes", "dia")
                    .count().as("count")
                    .sum("custo").as("valorTotal")
                    .sum("duracao").as("duracaoTotal")
                    .first("dataHora").as("dataHora"),
                Aggregation.sort(Direction.ASC, "dataHora")
        );

I need to group by day, month and year in mongodb, or else i will need to convert all this grouped data in code.

Thanks in advance


Answer:

You are running into a limitation of spring mongo in what you can do for field calculations in a single $project stage, and indeed you are likely already writing as a separate $project since you discover that you cannot project custom named fields directly in a $group _id at present either.

So you would be better off keeping this all in the $group, as well as using a different method for rounding your adjusted dates to local time.

The better way to write your $group would therefore be:

{ "$group": {
  "_id": {
    "programa": "$programa",
    "dataHora": {
      "$add": [
        { "$subtract": [
          { "$subtract": [{ "$subtract": ["$dataHora", new Date(0)] }, 25200000 ] },
          { "$mod": [
            { "$subtract": [{ "$subtract": ["$dataHora", new Date(0)] }, 25200000 ] },
            1000 * 60 * 60 * 24
          ]}
        ]},
        new Date(0)
      ]
    }
  },
  "count": { "$sum": 1 },
  "valorTotal": { "$sum": "$custo" },
  "duracaoTotal": { "$sum": "$duracao" },
  "dataHora": { "$first": "$dataHora" }
}}

Of course to use this sort of structure with spring-mongo you need a custom implementation of the aggregation stage operation that can take a defined DBObject:

public class CustomGroupOperation implements AggregationOperation {
    private DBObject operation;

    public CustomGroupOperation (DBObject operation) {
        this.operation = operation;
    }

    @Override
    public DBObject toDBObject(AggregationOperationContext context) {
        return context.getMappedObject(operation);
    }
}

Which you then use in context like this:

    Aggregation aggregation = Aggregation.newAggregation(
            Aggregation.match(c),
            new CustomGroupOperation(
                new BasicDBObject("$group",
                    new BasicDBObject("_id",
                        new BasicDBObject("programa","$programa")
                            .append("dataHora",
                                new BasicDBObject("$add",Arrays.asList(
                                    new BasicDBObject("$subtract",Arrays.asList(
                                        new BasicDBObject("$subtract",Arrays.asList(
                                            new BasicDBObject("$subtract",Arrays.asList(
                                                "$dataHora", new Date(0)
                                            )),
                                            25200000
                                        )),
                                        new BasicDBObject("$mod",Arrays.asList(
                                            new BasicDBObject("$subtract",Arrays.asList(
                                                new BasicDBObject("$subtract",Arrays.asList(
                                                    "$dataHora", new Date(0)
                                                )),
                                                25200000
                                            )),
                                            1000 * 60 * 60 * 24
                                        ))
                                    )),
                                    new Date(0)
                                ))
                            )
                    )
                    .append("count",new BasicDBObject("$sum",1))
                    .append("valorTotal",new BasicDBObject("$sum","$custo"))
                    .append("duracaoTotal",new BasicDBObject("$sum","$duracao"))
                    .append("dataHora",new BasicDBObject("$first","$dataHora"))
                )
            ),
            Aggregation.sort(Direction.ASC,"_id.dataHora")
    );

Since the custom class abstracts from the same basic class used by the built in helper methods, it can be used alongside them as shown.

How the basic process with the date math works here is that when you $subtract one BSON Date object from another then the result is the milliseconds of difference, and in this case from the epoch date ( Date(0) ) which just extracts the milliseconds value. This allows you to do the math to round to the current date value by the modulo ( $mod ) from the number of milliseconds in one day.

Much as you originally tried, when you then $add that millisecond value to a BSON Date object the returned value is again a BSON Date. So adding to an object representing epoch returns a new Date Object, but rounded to the current date.

This is usually a lot more useful than extracting parts via date aggregation operators, and also works out to be a bit shorter to code, especially when adjusting the time from UTC as you are doing here.

Though the contruction of the $group here is a bit more terse than the helper functions of spring mongo are trying to avoid, it is a lot more efficient in the end than running a separate $project stage to transform the field values that you really only want in the $group stage anyway.