41

I am trying to use Spring Data JPA 1.8 with the Java 8 Date/Time API JSR-310.

Everything seems to work, until I try to get all Vehicles between two LocalDateTimes. The number of Entities returned seems to only have a loose correlation with the number it should.

Entity

@Repository
public interface VehicleRepository extends JpaRepository<Vehicle, Long> {

    List<Vehicle> findByDateTimeBetween(LocalDateTime begin, LocalDateTime end);

}

Repository

@Entity
@Table(name = "VEHICLE")
public class Vehicle implements Serializable {

  private static final long serialVersionUID = 1L;

  @Id
  @Column(name = "IDX", nullable = false, unique = true)
  @GeneratedValue(strategy = GenerationType.AUTO)
  private long vehicleId;

  @Column(name = "DATE_TIME", nullable = false)
  private LocalDateTime dateTime = LocalDateTime.now();

  // Getters and Setters

}

Relevant Dependencies

    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-jpa</artifactId>
        <version>1.8.0.RELEASE</version>
    </dependency>
    <dependency> 
        <groupId>org.springframework</groupId> 
        <artifactId>spring-aspects</artifactId> 
        <version>4.0.9.RELEASE</version> 
    </dependency>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-core</artifactId>
        <version>4.3.8.Final</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-entitymanager</artifactId>
        <version>4.3.8.Final</version>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <version>1.4.186</version>
    </dependency>
    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>HikariCP</artifactId>
        <version>2.3.5</version>
    </dependency>

Failing Test Example

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath*:applicationContextTesting.xml"})
@Transactional
public class VehicleRepositoryTest {

    @Autowired
    private VehicleRepository vehicleRepository;

    @Test
    public void testVehicleBetween() {
        // Given
        Vehicle vehicleMarch1Twelve = new Vehicle();
        Vehicle vehicleMarch1Eighteen = new Vehicle();
        Vehicle vehicleMarch2Five = new Vehicle();
        Vehicle vehicleMarch2Six = new Vehicle();
        LocalDateTime march1Twelve = LocalDateTime.of(2015, Month.MARCH, 1, 12, 0);
        LocalDateTime march1Eighteen = LocalDateTime.of(2015, Month.MARCH, 1, 18, 0);
        LocalDateTime march2Five = LocalDateTime.of(2015, Month.MARCH, 2, 5, 0);
        LocalDateTime march2Six = LocalDateTime.of(2015, Month.MARCH, 2, 6, 0);
        vehicleMarch1Twelve.setDateTime(march1Twelve);
        vehicleMarch1Eighteen.setDateTime(march1Eighteen);
        vehicleMarch2Five.setDateTime(march2Five);
        vehicleMarch2Six.setDateTime(march2Six);

        vehicleRepository.save(vehicleMarch1Twelve);
        vehicleRepository.save(vehicleMarch1Eighteen);
        vehicleRepository.save(vehicleMarch2Five);
        vehicleRepository.save(vehicleMarch2Six);
        vehicleRepository.flush();

        // when
        List<Vehicle> allVehicles = vehicleRepository.findByDateTimeBetween(
            march1Twelve,
            march2Six);
        List<Vehicle> allVehicles2 = vehicleRepository.findByDateTimeBetween(
            march1Twelve.minusMinutes(2),
            march2Six.plusMinutes(2));
        List<Vehicle> threeVehicles = vehicleRepository.findByDateTimeBetween(
            march1Twelve.plusMinutes(2),
            march2Six);
        List<Vehicle> twoVehicles = vehicleRepository.findByDateTimeBetween(
            march1Twelve.plusMinutes(2),
            march2Six.minusMinutes(2));
        List<Vehicle> oneVehicles = vehicleRepository.findByDateTimeBetween(
            march1Twelve.plusMinutes(2),
            march2Six.minusHours(3));

        // then
        Assert.assertTrue("size was " + allVehicles.size(), allVehicles.size() == 4);
        Assert.assertTrue("size was " + allVehicles2.size(), allVehicles2.size() == 4);
        Assert.assertTrue("size was " + threeVehicles.size(), threeVehicles.size() == 3);
        Assert.assertTrue("size was " + twoVehicles.size(), twoVehicles.size() == 2);
        Assert.assertTrue("size was " + oneVehicles.size(), oneVehicles.size() == 1);
        Assert.assertTrue(oneVehicles.get(0).getDateTime().equals(march1Eighteen));
    }

}

The first List contains 2 elements (should be 4). All other List contain 0 elements! Given the second request is for a greater timespan than the first.

Can someone tell me what I am doing wrong?


Edit

Thank you @Oliver Gierke for the quick answer. I was able to fix the issue by adding "org.springframework.data.jpa.convert.threeten" to the packagesToScan property. Now it seems to work properly.

As reference here is my working Database related (testing) configuration.

<bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
    <property name="driverClassName" value="org.h2.Driver"/>
    <property name="jdbcUrl" value="jdbc:h2:mem:testing"/>
    <property name="username" value="interface"/>
    <property name="password" value=""/>
    <property name="connectionTestQuery" value="SELECT 1" />
</bean>

<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource">
    <constructor-arg index="0" ref="hikariConfig"/>
</bean>

<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>

<tx:annotation-driven/>

<bean id="hibernateJpaVendorAdapter" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/>

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="jpaVendorAdapter" ref="hibernateJpaVendorAdapter"/>
    <property name="packagesToScan" value="com.company.project.domain,org.springframework.data.jpa.convert.threeten"/>
    <property name="jpaProperties">
        <props>
            <prop key="hibernate.dialect">org.hibernate.dialect.H2Dialect</prop>
            <prop key="hibernate.hbm2ddl.auto">update</prop>
        </props>
    </property>
</bean>

<jpa:repositories base-package="com.company.project.dao" transaction-manager-ref="transactionManager"
                  entity-manager-factory-ref="entityManagerFactory"/>
Community
  • 1
  • 1
tobijdc
  • 1,215
  • 1
  • 14
  • 21

3 Answers3

48

UPDATE: The answer below is valid if you need to stay on a Hibernate version < 5.0. Hibernate 5.0 supports persisting JSR-310 date/time types out of the box. I.e. if you are on Hibernate 5.0 or newer, Adam's answer is the way to go. Everyone else, read on.

The root cause for this at none of the widely used JPA providers actually support JSR-310 types out of the box yet. However, as of Spring Data JPA 1.8.0 we ship JPA 2.0 converters that will translate non-time-zoned JSR-310 types into a legacy Date so that they can be persisted as is.

To get this working, simply register org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters as one of the managed JPA classes with your provider. There's a couple of way to do that: in a very standard JPA setup you list it in your persistence.xml. In a LocalContainerEntityManagerFactoryBean based setup you can just add the package of the class to the packagesToScan property. If you're using Spring Boot adding the class to the @EntityScan annotation does the trick.

The latter is described in a bit more detail in the blog post covering the new features the Spring Data release train named Fowler ships

Oliver Drotbohm
  • 80,157
  • 18
  • 225
  • 211
  • 1
    Correction: DataNucleus JPA supports java.time types out of the box and has since a year ago (when Java8 came out), and I've been using that support. – Neil Stockton Apr 09 '15 at 18:26
  • With JPA 2.2 this bug will be fix, will have any problem with this class of spring data? – nole Sep 28 '15 at 07:30
  • That's not a bug, it's just how things work. Unfortunately I am not able to predict 2018 ;). – Oliver Drotbohm Sep 29 '15 at 23:24
  • Just a hint for spring-boot users. I'm using `springBootVersion = '1.2.7.RELEASE'` which is currently the latest and it comes with `spring-data-jpa:1.7.4.RELEASE` - so due to the answer you need to update the data-jpa dependency if you are going with spring boot – Tarion Nov 12 '15 at 23:50
  • thank you for the answer, adding `Jsr310JpaConverters` to `@EntityScan` in a Spring Boot app made the trick for me. However, when I added hibernate envers for auditing and marked an entity with JSR310 type to be audited, it throws `Caused by: org.hibernate.MappingException: Could not determine type for: BasicType adapter for AttributeConverter, at table:`, I guess envers is not aware of these converters, is there an elegant way to fix this? I am using spring boot `1.3.3.RELEASE` and cloud `Brixton.M4`. thank you. – burcakulug Mar 23 '16 at 14:00
  • thank you for the answer, adding Jsr310JpaConverters,it throw `classnotfoundexception: org.threeten.bp.localdatetime`.Need I add `org.threeten` dependency? – Alan May 13 '16 at 10:13
34

When using Hibernate >=5.0, <5.2, you can drop in

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-java8</artifactId>
    <version>${hibernate.version}</version>
</dependency>

on your classpath which will automatically register Types corresponding to the JSR310 classes.

(Thanks @AbhijitSarkar) Since 5.2, "the hibernate-java8 module has been merged into hibernate-core and the Java 8 date/time types are now natively supported." (Migration Guide 5.2)

Adam Michalik
  • 9,678
  • 13
  • 71
  • 102
21

It took me quite a while to figure out how to use LocalDateTime in my JPA Entity. I was on the latest Spring boot version. And debugged a lot into the ConversionServices.

Oliver Gierkes answer helped me a lot in getting to the final working setup:

Add Spring-data-jpa 1.8.0 or higher to your dependency management

compile("org.springframework.data:spring-data-jpa:1.8.2.RELEASE")

Enable @EntityScan for the Jsr310JpaConverters + (at least) your Application.class

@EntityScan(
  basePackageClasses = { Application.class, Jsr310JpaConverters.class }
)
@SpringBootApplication
class Application { … }
Neeme Praks
  • 8,956
  • 5
  • 47
  • 47
Tarion
  • 16,283
  • 13
  • 71
  • 107
  • 1
    works for me. there is a sample project on java8 features on github too. https://github.com/spring-projects/spring-data-examples/tree/master/jpa/java8 – shane lee Mar 09 '16 at 03:17
  • I could not get this to work with curated dependencies of Spring-boot 1.5.8. (it comes with hibernate-core-5.0.12.Final.jar). I had to add org.hibernate:hibernate-java8:5.1.0.Final to get it to work. – Witold Kaczurba Dec 03 '18 at 17:25
  • Can I use it if mysql datatype is `TIMESTAMP`? – Naanavanalla Jan 23 '20 at 19:24