18

Trying to use ZonedDateTime with MongoDB. I'm able to save ZonedDateTime in MongoDB but when i look at the record it has so much unnecessary stuffs in there:

> "timestamp" : {
>             "dateTime" : ISODate("2016-12-13T13:45:53.991Z"),
>             "offset" : {
>                 "_id" : "-05:00",
>                 "totalSeconds" : -18000
>             },
>             "zone" : {
>                 "_class" : "java.time.ZoneRegion",
>                 "_id" : "America/New_York",
>                 "rules" : {
>                     "standardTransitions" : [ 
>                         NumberLong(-2717650800)
>                     ],
>                     "standardOffsets" : [ 
>                         {
>                             "_id" : "-04:56:02",
>                             "totalSeconds" : -17762
>                         }, 
>                         {
>                             "_id" : "-05:00",
>                             "totalSeconds" : -18000
>                         }
>                     ],
>                     "savingsInstantTransitions" : [ 
>                         NumberLong(-2717650800), 
>                         NumberLong(-1633280400), 
>                         NumberLong(-1615140000), 
>                         NumberLong(-1601830800), 
>                         NumberLong(-1583690400), 
>                         NumberLong(-1570381200),
> and so on....

Also when i try to retrieve this same date, it gives me following:

> org.springframework.data.mapping.model.MappingException: No property
> null found on entity class java.time.ZonedDateTime to bind constructor
> parameter to!

I didn't have this problem when working with LocalDateTime. First question is can we change some settings somewhere that will only persist ISODate with ZonedDateTime? Second question, is there something like Jsr310JpaConverters for mongodb?

UPDATE: Referencing the following questionaire I created custom converters and registered them,however, the issue still persists. Spring Data MongoDB with Java 8 LocalDate MappingException

public class ZonedDateTimeToLocalDateTimeConverter implements Converter<ZonedDateTime, LocalDateTime> {
    @Override
    public LocalDateTime convert(ZonedDateTime source) {
        return source == null ? null : LocalDateTime.ofInstant(source.toInstant(), ZoneId
                .systemDefault());
    }
}

And

public class LocalDateTimeToZonedDateTimeConverter implements Converter<LocalDateTime,
        ZonedDateTime> {
    @Override
    public ZonedDateTime convert(LocalDateTime source) {
        return source == null ? null : ZonedDateTime.of(source, ZoneId.systemDefault());
    }
}

Registered them as follows:

@Bean
public CustomConversions customConversions(){
        List<Converter<?,?>> converters = new ArrayList<Converter<?,?>>();
        converters.add(new ZonedDateTimeToLocalDateTimeConverter());
        converters.add(new LocalDateTimeToZonedDateTimeConverter());
        return new CustomConversions(converters);
    }

@Bean
public MongoTemplate getMongoTemplate() throws UnknownHostException {
        MappingMongoConverter converter = new MappingMongoConverter(
                new DefaultDbRefResolver(getMongoDbFactory()), new MongoMappingContext());
        converter.setCustomConversions(customConversions());
        converter.afterPropertiesSet();
        return new MongoTemplate(getMongoDbFactory(), converter);
    }
Community
  • 1
  • 1
Gurkha
  • 1,104
  • 4
  • 20
  • 37
  • And the relevance of JPA API to this? If using Spring-Data-MongoDB then there is no use at all of JPA API. If using something else then DEFINE what you are using – Neil Stockton Dec 13 '16 at 18:42
  • @NeilStockton I'm using `spring-data-mongodb`. In case of JPA the converter mentioned does the conversion but with this do I need to create a custom converter? – Gurkha Dec 13 '16 at 19:17
  • Spring Data MongoDB is nothing to do with JPA API. Consequently there is no AttributeConverter. It uses MongoDB's own API. Look at Spring Data MongoDB docs – Neil Stockton Dec 13 '16 at 19:23
  • Im getting an error "cannot resolve method getMongoDbFactory()" when registering this convertors, should I add or call any other class? – Rolando F Nov 02 '17 at 18:22

2 Answers2

15

Looks like Spring has support for all the java time converter except ZonedDateTime converter. You can register one as follows.

@Bean
public CustomConversions customConversions(){
    List<Converter<?,?>> converters = new ArrayList<>();
    converters.add(new DateToZonedDateTimeConverter());
    converters.add(new ZonedDateTimeToDateConverter());
    return new CustomConversions(converters);
}

@Bean
public MongoTemplate getMongoTemplate() throws UnknownHostException {
    MappingMongoConverter converter = new MappingMongoConverter(
            new DefaultDbRefResolver(getMongoDbFactory()), new MongoMappingContext());
    converter.setCustomConversions(customConversions());
    converter.afterPropertiesSet();
    return new MongoTemplate(getMongoDbFactory(), converter);
}
    
class DateToZonedDateTimeConverter implements Converter<Date, ZonedDateTime> {
    
     @Override
     public ZonedDateTime convert(Date source) {
              return source == null ? null : ofInstant(source.toInstant(), systemDefault());
         }
     }
    
class ZonedDateTimeToDateConverter implements Converter<ZonedDateTime, Date> {
    
    @Override
    public Date convert(ZonedDateTime source) {
             return source == null ? null : Date.from(source.toInstant());
       }
   }

One other alternative solution would be to just use the ZonedDateTime and change it to date while persisting into MongoDB. You can easily change it back from date back to Zoned Date Time while fetching.

Below are the relavant methods to help with conversions.

ZoneId zoneID = ZoneId.of("America/Chicago");

From ZonedDateTime to java util date.

Instant instant = Instant.now();
ZonedDateTime zonedDateTime = instant.atZone(zoneId);
Date date = Date.from(zdt.toInstant());

From Date to ZonedDateTime

Instant instant = date.toInstant();
ZonedDateTime zonedDateTime = instant.atZone(zoneId);

The other alternative is to implement custom codec to help with conversions. I've created one for YearMonth at Filtering YearMonth from Mongo document. I'll leave it as exercise to the reader if they want to create custom codec for Zoned Date Time.

You can use below library for codec based approach.

https://github.com/ylemoigne/mongo-jackson-codec

s7vr
  • 73,656
  • 11
  • 106
  • 127
  • Sagar, looks like we posted at the exact same time, there was an article which i followed which is exactly what u posted. take a look. thanks for your effort though. Will upvote for ur effort. – Gurkha Dec 14 '16 at 15:52
  • oh I just realised that too. Can you please try my version and let me know ? The trick is to convert to ZonedDateTime to date not LocalDateTime and vice versa. – s7vr Dec 14 '16 at 15:53
  • Okay, lemme try that. – Gurkha Dec 14 '16 at 15:57
  • It works for ZonedDateTime to Date but with my mongo template config, your template approach game me 'unsupported converter' exception. Could you update your answer with my template, I will mark it as correct. – Gurkha Dec 14 '16 at 16:15
  • Sorry accidentally remove my previous comment. Np. – s7vr Dec 14 '16 at 16:23
  • I believe the two @Bean entries go in my Application class, right? – Thom Feb 10 '18 at 16:34
  • @Thom Yes should go in Application config class. – s7vr Feb 10 '18 at 20:07
  • I am working on a Spring boot application with Spring data. Is there a way to pass timezone offset from the request and get the date fields converted to corresponding time zone? – Nagaraja JB Jan 20 '20 at 07:14
  • Yes that way you can persist it, **but you loose precision**! ["The class Date represents a specific instant in time, with millisecond precision. "](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Date.html) Now look what the docs for ZonedDateTime says [" This class stores all date and time fields, to a precision of nanoseconds,..."](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/ZonedDateTime.html) – Andi Aug 26 '22 at 05:11
  • To prevent UTC conversion when saving to the database and keep local date time (i.e., LocalDateTime with a separate timezone property) in Mongo database, I used the technique described by Chamindu at chamindu.dev/posts/localdatetime-spring-mongodb. Just make sure to extend AbstractMongoClientConfiguration instead of AbstractMongoConfiguration as he indicates. – tlarson Jun 25 '23 at 15:21
12

After spending way too much time debugging this, I finally found a working solution for latest version of spring boot / spring data. This is currently working for me on Spring Boot 2.0.0.M7.

With the accepted answer from veeram, I was getting Couldn't find PersistentEntity for type

I hope this helps someone avoid going down the rabbit hole.

@Configuration
public class MongoConfiguration {

    @Bean
    public MongoCustomConversions customConversions(){
        List<Converter<?,?>> converters = new ArrayList<>();
        converters.add(DateToZonedDateTimeConverter.INSTANCE);
        converters.add( ZonedDateTimeToDateConverter.INSTANCE);
        return new MongoCustomConversions(converters);
    }

    enum DateToZonedDateTimeConverter implements Converter<Date, ZonedDateTime> {

        INSTANCE;

        @Override
        public ZonedDateTime convert(Date source) {
            return ofInstant(source.toInstant(), systemDefault());
        }
    }

    enum ZonedDateTimeToDateConverter implements Converter<ZonedDateTime, Date> {

        INSTANCE;

        @Override
        public Date convert(ZonedDateTime source) {
            return Date.from(source.toInstant());
        }
    }
}
Alex Spence
  • 121
  • 1
  • 2
  • 4
    Your solution fixed my problem. Spring Boot 2.0.0.M7 can use `@Bean public CustomConversions customConversions(){...}`. But when I upgrade to Spring Boot 2.0.1.RELEASE, this not work. Need to use `@Bean public MongoCustomConversions customConversions(){...}`. I wasted a lot of time on it. – RJ.Hwang Apr 24 '18 at 10:51