在 Spring Boot 和 MongoDB 中使用 OffsetDateTime 会导致 MappingException

Posted

技术标签:

【中文标题】在 Spring Boot 和 MongoDB 中使用 OffsetDateTime 会导致 MappingException【英文标题】:Usage of OffsetDateTime with Spring Boot and MongoDB results in MappingException 【发布时间】:2017-09-18 14:03:59 【问题描述】:

我正在尝试使用 MongoDB 数据库设置 Spring Boot 应用程序。这是我拥有的依赖项的摘录(以 Gradle 表示)。

compile("org.springframework.boot:spring-boot-starter-web:1.5.1.RELEASE")
compile("org.springframework.boot:spring-boot-starter-data-jpa:1.5.1.RELEASE")
compile("org.springframework.boot:spring-boot-starter-data-mongodb:1.5.1.RELEASE")
compile("org.springframework.boot:spring-boot-starter-hateoas:1.5.1.RELEASE")
compile("org.springframework.boot:spring-boot-starter-security:1.5.1.RELEASE")
compile("org.springframework.security:spring-security-test:1.5.1.RELEASE)
testCompile("org.springframework.boot:spring-boot-starter-test:1.5.1.RELEASE")
compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.8.8")

我的@Document 注释 Java 类包含一个 OffsetDateTime 属性。

@Document(collection = "reports")
public class ReportDocument implements Serializable 

    @Id private String id;
    @Version private Long version;
    //...
    private OffsetDateTime start;
    private OffsetDateTime end;
    //...

当我调用检索这些文档的 REST-Controller 时,它失败并出现异常

org.springframework.data.mapping.model.MappingException: No property null found on entity class java.time.OffsetDateTime to bind constructor parameter to!
at org.springframework.data.mapping.model.PersistentEntityParameterValueProvider.getParameterValue(PersistentEntityParameterValueProvider.java:74) ~[spring-data-commons-1.13.0.RELEASE.jar:na]
at org.springframework.data.mapping.model.SpELExpressionParameterValueProvider.getParameterValue(SpELExpressionParameterValueProvider.java:63) ~[spring-data-commons-1.13.0.RELEASE.jar:na]
at org.springframework.data.convert.ReflectionEntityInstantiator.createInstance(ReflectionEntityInstantiator.java:71) ~[spring-data-commons-1.13.0.RELEASE.jar:na]
at org.springframework.data.convert.ClassGeneratingEntityInstantiator.createInstance(ClassGeneratingEntityInstantiator.java:83) ~[spring-data-commons-1.13.0.RELEASE.jar:na]
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:257) ~[spring-data-mongodb-1.10.0.RELEASE.jar:na]

我阅读了很多论坛。有些人用 Joda-libraries DateTime 替换了 OffsetDateTime。这不是我要走的路,因为 Joda 声明使用 Java 8 DateTime-Types。

我做错了什么(我知道问题总是在电脑面前),我该如何解决?有人对此有任何想法吗?

更新(从 2017 年 4 月 22 日开始) 我确实像@Veeram 所说的那样,并使用转换器更新了我的应用程序(日期 -> OffsetDateTime,反之亦然)。

package com.my.personal.app.converter;

import org.springframework.core.convert.converter.Converter;

import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.util.Date;

public class DateToOffsetDateTimeConverter implements Converter<Date, OffsetDateTime> 

    @Override
    public OffsetDateTime convert(Date source) 
        return source == null ? null : OffsetDateTime.ofInstant(source.toInstant(), ZoneId.systemDefault());
    


package com.my.personal.app.converter;

import org.springframework.core.convert.converter.Converter;

import java.time.OffsetDateTime;
import java.util.Date;

public class OffsetDateTimeToDateConverter implements Converter<OffsetDateTime, Date> 

    @Override
    public Date convert(OffsetDateTime source) 
        return source == null ? null : Date.from(source.toInstant());
    


注册转换器

package com.my.personal.app;

import com.my.personal.app.converter.DateToOffsetDateTimeConverter;
import com.my.personal.app.converter.OffsetDateTimeToDateConverter;
import com.mongodb.Mongo;
import com.mongodb.MongoClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.mongodb.config.AbstractMongoConfiguration;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.convert.CustomConversions;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;

import java.util.ArrayList;
import java.util.List;

@Configuration
public class MongoConfig extends AbstractMongoConfiguration 


    @Override
    protected String getDatabaseName() 
        return "my-personal-database";
    

    @Override
    public Mongo mongo() throws Exception 
        return new MongoClient("localhost");
    

    @Bean
    @Override
    public CustomConversions customConversions() 
        List<Converter<?, ?>> converterList = new ArrayList<Converter<?, ?>>();
        converterList.add(new DateToOffsetDateTimeConverter());
        converterList.add(new OffsetDateTimeToDateConverter());
        return new CustomConversions(converterList);
    

    @Bean
    @Override
    public MongoTemplate mongoTemplate() throws Exception 
        MappingMongoConverter converter = new MappingMongoConverter(
                new DefaultDbRefResolver(mongoDbFactory()), new MongoMappingContext());
        converter.setCustomConversions(customConversions());
        converter.afterPropertiesSet();
        return new MongoTemplate(mongoDbFactory(), converter);
    



但再次导致异常

org.springframework.data.mapping.model.MappingException: No property null found on entity class java.time.OffsetDateTime to bind constructor parameter to!
    at org.springframework.data.mapping.model.PersistentEntityParameterValueProvider.getParameterValue(PersistentEntityParameterValueProvider.java:74) ~[spring-data-commons-1.13.0.RELEASE.jar:na]
    at org.springframework.data.mapping.model.SpELExpressionParameterValueProvider.getParameterValue(SpELExpressionParameterValueProvider.java:63) ~[spring-data-commons-1.13.0.RELEASE.jar:na]
    at org.springframework.data.convert.ReflectionEntityInstantiator.createInstance(ReflectionEntityInstantiator.java:71) ~[spring-data-commons-1.13.0.RELEASE.jar:na]

我是否遗漏或误解了某事。或做某事。错了吗?

更新我在集合中的文档

这是我收藏的文件的基本部分的摘录

[
  
    "_id": 
      "$oid": "58f8b107affb5f08e0a78a96"
    ,
    "_class": "com.my.personal.app.document.ReportDocument",
    "version": 0,
    "checklistId": 2,
    "vehicleGuid": "some-vehicle-guid",
    "userGuid": "some-user-guid",
    "name": "Report 123",
    "start": 
      "dateTime": 
        "$date": "2017-04-20T12:00:55.930Z"
      ,
      "offset": "+02:00"
    ,
    "stations": [
      
        "_id": 1,
        "name": "Front"
      
    ]
  ,
  
    "_id": 
      "$oid": "58f8bf78affb5f2dec896acf"
    ,
    "_class": "com.my.personal.app.document.ReportDocument",
    "version": 0,
    "checklistId": 2,
    "vehicleGuid": "some-vehicle-guid",
    "userGuid": "some-user-guid",
    "name": "Report 123",
    "start": 
      "dateTime": 
        "$date": "2017-04-20T10:02:32.930Z"
      ,
      "offset": "+02:00"
    ,
    "stations": [
      
        "_id": 1,
        "name": "Front"
      
    ]
  
]

这是试图调用文档的 REST 控制器

@RequestMapping(value = "/mongoreports")
public class MongoReportController 

    @Autowired
    private MongoReportRepository repository;

    @RequestMapping(
            method = RequestMethod.GET,
            produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public ResponseEntity<List<ReportDocument>> show(
            @RequestParam(name = "vehicleGuid") Optional<String> vehicleGuid,
            @RequestParam(name = "userGuid") Optional<String> userGuid) 
        if (vehicleGuid.isPresent() && !userGuid.isPresent()) 
            List<ReportDocument> reportDocuments = repository.findByVehicleGuidOrderByStartAsc(vehicleGuid.get());
            return ResponseEntity.ok(reportDocuments);
        
        if (!vehicleGuid.isPresent() && userGuid.isPresent()) 
            List<ReportDocument> reportDocuments = repository.findByUserGuidOrderByStartAsc(userGuid.get());
            return ResponseEntity.ok(reportDocuments);
        
        if (vehicleGuid.isPresent() && userGuid.isPresent()) 
            List<ReportDocument> reportDocuments = repository.findByUserGuidAndVehicleGuidOrderByStartAsc(vehicleGuid.get(), userGuid.get());
            return ResponseEntity.ok(reportDocuments);
        
        return ResponseEntity.badRequest().build();
    

以及相应的 MongoRepository

package com.my.personal.app.repository;

import com.my.personal.app.document.ReportDocument;
import org.springframework.data.mongodb.repository.MongoRepository;

import java.util.List;

public interface MongoReportRepository extends MongoRepository<ReportDocument, String> 

    List<ReportDocument> findByVehicleGuidOrderByStartAsc(String vehicleGuid);

    List<ReportDocument> findByUserGuidOrderByStartAsc(String userGuid);

    List<ReportDocument> findByUserGuidAndVehicleGuidOrderByStartAsc(String userGuid, String vehicleGuid);


【问题讨论】:

编写您的自定义转换器并注册到 MongoConfiguration 您可以按照此处的步骤在此处创建自定义转换。 ***.com/questions/41127665/zoneddatetime-with-mongodb。另请阅读jira.spring.io/browse/DATACMNS-698 @Veraam :我调整了转换器并注册了它们。但应用程序不受影响,并且发生相同的异常。有没有。我错过了什么? 我可以从您的收藏中查看示例文档以及调用代码吗?字段如何保存在数据库中? 你的配置是否被拾取? 【参考方案1】:

我为此挣扎了几个小时。我得到的错误是:

No converter found capable of converting from type [java.util.Date] to type [java.time.OffsetDateTime]

我最终想出了以下配置类,它转换为 java.util.Date 而不是字符串:

import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.Date;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;

@Configuration
public class MongoConfig 

    @Bean
    public MongoCustomConversions mongoCustomConversions() 
        return new MongoCustomConversions(Arrays.asList(
            new OffsetDateTimeReadConverter(),
            new OffsetDateTimeWriteConverter()
        ));
    

    static class OffsetDateTimeWriteConverter implements Converter<OffsetDateTime, Date> 

        @Override
        public Date convert(OffsetDateTime source) 
            return source == null ? null : Date.from(source.toInstant().atZone(ZoneOffset.UTC).toInstant());
        
    

    static class OffsetDateTimeReadConverter implements Converter<Date, OffsetDateTime> 

        @Override
        public OffsetDateTime convert(Date source) 
            return source == null ? null : source.toInstant().atOffset(ZoneOffset.UTC);
        
    

这在构建时对我有用:

 <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.1.RELEASE</version>
    <relativePath/>
  </parent>

【讨论】:

我怀疑,默认情况下不存在此转换的原因是因为从 OffsetDateTime 到 Date 的转换是有损的。如果您将 12:00:00-07:00 转换为日期,则存储在 DB 中的只是 05:00:00Z。当您从 db 中获取它时,您不知道原始偏移量。您可以创建一个新的 OffsetDateTime,但它不会具有原始偏移量。要完全存储它,您至少需要两列/字段(即时 + 偏移量)。这是必须有意识地去做的事情。

以上是关于在 Spring Boot 和 MongoDB 中使用 OffsetDateTime 会导致 MappingException的主要内容,如果未能解决你的问题,请参考以下文章

在 Spring-Boot 中,我们如何在同一个项目中连接两个数据库(Mysql 数据库和 MongoDB)?

如何在 spring-boot 中禁用 spring-data-mongodb 自动配置

Spring Boot 和 MongoDB,查询嵌套映射的键

Spring Boot + Mongodb + Shiro配置

Spring Boot MongoDB:可以使用@GeneratedValue 和@Column 注解吗?

如何使用 Spring Boot 和 MongoDB 从 JSON 列表中删除对象