Skip to content

AttributeConverter is not called anymore for null parameters in query in 2.7.0 #2549

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
CyrilChevalier opened this issue May 25, 2022 · 16 comments
Assignees
Labels
status: feedback-provided Feedback has been provided

Comments

@CyrilChevalier
Copy link

CyrilChevalier commented May 25, 2022

Context

We have some entites that used AttributeConverter to convert a numeric type to a LocalDate type like this one :

public class MyEntity implements Serializable {

    // Serial
    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "my_entity_generator")
    @SequenceGenerator(name = "my_entity_generator", sequenceName = "my_entity_seq", schema = "public", allocationSize = 1)
    private Long id;

    @Column
    private String name;

    @Column
    @Convert(converter = MyConverter.class)
    private LocalDate date;
}

With this kind of AttributeConverter :

public class MyConverter implements AttributeConverter<LocalDate, Long> {

    @Override
    public Long convertToDatabaseColumn(LocalDate attribute) {
        return attribute !=null ? attribute.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli() : 0L;
    }

    @Override
    public LocalDate convertToEntityAttribute(Long dbData) {
        return dbData!=null && dbData!=0 ? Instant.ofEpochMilli(dbData).atZone(ZoneOffset.UTC).toLocalDate() : null;
    }
}

This is an example of query we can use in the repo :

public interface MyEntityRepo extends JpaRepository<MyEntity, Long> {

    /**
     * Find by date (nullable)
     * @param date
     * @return
     */
    @Query("select e from MyEntity e WHERE e.date = :date")
    List<MyEntity> findByDate(@Param("date") LocalDate date);
}

Problem

In 2.7.0 version, when I call the method findByDate with a null parameter with a postgresql database, the query failed with this message :

org.springframework.dao.InvalidDataAccessResourceUsageException: could not extract ResultSet; SQL [n/a]; nested exception is org.hibernate.exception.SQLGrammarException: could not extract ResultSet

	at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:259)
	at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:233)
	at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:551)
	at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
	at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242)
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:152)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:145)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
	at jdk.proxy2/jdk.proxy2.$Proxy104.findByDate(Unknown Source)
	at com.example.attributeconverterdemo.AttributeConverterDemoApplicationTests.contextLoads(AttributeConverterDemoApplicationTests.java:22)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:725)
	at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
	at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
	at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:214)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:210)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:66)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
	at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
	at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53)
	at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
	at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)
Caused by: org.hibernate.exception.SQLGrammarException: could not extract ResultSet
	at org.hibernate.exception.internal.SQLStateConversionDelegate.convert(SQLStateConversionDelegate.java:103)
	at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:37)
	at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:113)
	at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:99)
	at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.extract(ResultSetReturnImpl.java:67)
	at org.hibernate.loader.Loader.getResultSet(Loader.java:2322)
	at org.hibernate.loader.Loader.executeQueryStatement(Loader.java:2075)
	at org.hibernate.loader.Loader.executeQueryStatement(Loader.java:2037)
	at org.hibernate.loader.Loader.doQuery(Loader.java:956)
	at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:357)
	at org.hibernate.loader.Loader.doList(Loader.java:2868)
	at org.hibernate.loader.Loader.doList(Loader.java:2850)
	at org.hibernate.loader.Loader.listIgnoreQueryCache(Loader.java:2682)
	at org.hibernate.loader.Loader.list(Loader.java:2677)
	at org.hibernate.loader.hql.QueryLoader.list(QueryLoader.java:540)
	at org.hibernate.hql.internal.ast.QueryTranslatorImpl.list(QueryTranslatorImpl.java:400)
	at org.hibernate.engine.query.spi.HQLQueryPlan.performList(HQLQueryPlan.java:219)
	at org.hibernate.internal.SessionImpl.list(SessionImpl.java:1459)
	at org.hibernate.query.internal.AbstractProducedQuery.doList(AbstractProducedQuery.java:1649)
	at org.hibernate.query.internal.AbstractProducedQuery.list(AbstractProducedQuery.java:1617)
	at org.hibernate.query.Query.getResultList(Query.java:165)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at org.springframework.orm.jpa.SharedEntityManagerCreator$DeferredQueryInvocationHandler.invoke(SharedEntityManagerCreator.java:406)
	at jdk.proxy2/jdk.proxy2.$Proxy121.getResultList(Unknown Source)
	at org.springframework.data.jpa.repository.query.JpaQueryExecution$CollectionExecution.doExecute(JpaQueryExecution.java:128)
	at org.springframework.data.jpa.repository.query.JpaQueryExecution.execute(JpaQueryExecution.java:90)
	at org.springframework.data.jpa.repository.query.AbstractJpaQuery.doExecute(AbstractJpaQuery.java:156)
	at org.springframework.data.jpa.repository.query.AbstractJpaQuery.execute(AbstractJpaQuery.java:144)
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:137)
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:121)
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:159)
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:138)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:80)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137)
	... 77 more
Caused by: org.postgresql.util.PSQLException: ERREUR: l'opérateur n'existe pas : numeric = date
  Indice : Aucun opérateur ne correspond au nom donné et aux types d'arguments.
Vous devez ajouter des conversions explicites de type.
  Position : 137
	at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2675)
	at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2365)
	at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:355)
	at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:490)
	at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:408)
	at org.postgresql.jdbc.PgPreparedStatement.executeWithFlags(PgPreparedStatement.java:167)
	at org.postgresql.jdbc.PgPreparedStatement.executeQuery(PgPreparedStatement.java:119)
	at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeQuery(ProxyPreparedStatement.java:52)
	at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeQuery(HikariProxyPreparedStatement.java)
	at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.extract(ResultSetReturnImpl.java:57)
	... 115 more
``

With the previous versions, the query works perfectly.

I think the problem comes from the new class HibernateJpaParametersParameterAccessor which transforms null values into TypedParameterValue objects. The AttributeConverter for these values is not called anymore.

I have prepared a project to reproduce the problem : spring-data-jpa-convert-null.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label May 25, 2022
@instagibb
Copy link

instagibb commented May 26, 2022

I am seeing a similar error when using the Hibernate annotation @Type(type="yes_no") mapping boolean to a 'Y' or 'N' char

@gregturn gregturn self-assigned this May 31, 2022
@gregturn
Copy link
Contributor

gregturn commented Jun 1, 2022

@CyrilChevalier So far, using Spring Data 3.0 with HSQL, I have it working just fine. I borrowed your entity type, repository definition, and converter:

@ExtendWith(SpringExtension.class)
@ContextConfiguration
public class AttributeConverterIntegrationTests {

	@Autowired MyEntityRepository repository;

	@Test
	void nullBasedFinderShouldBlowUp() {

		LocalDate now = LocalDate.now();

		repository.save(new MyEntity("TEST", now));
		repository.save(new MyEntity("Null", null));

		List<MyEntity> dates = repository.findByDate(now);
		dates = repository.findByDate(null);
	}

	@Entity
	@Table(name = "my_entity")
	@Data
	@NoArgsConstructor(access = AccessLevel.PROTECTED)
	static class MyEntity {

		@Id
		@GeneratedValue private Long id;

		private String value;

		@Convert(converter = CustomAttributeConverter.class) //
		private LocalDate date;

		public MyEntity(String value, LocalDate date) {

			this.value = value;
			this.date = date;
		}
	}

	interface MyEntityRepository extends JpaRepository<MyEntity, Long> {

		@Query("select e from AttributeConverterIntegrationTests$MyEntity e WHERE e.date = :date")
		List<MyEntity> findByDate(@Nullable @Param("date") LocalDate date);
	}

	static class CustomAttributeConverter implements AttributeConverter<LocalDate, Long> {

		@Override
		public Long convertToDatabaseColumn(LocalDate attribute) {
			long convertedValue = attribute != null ? attribute.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli() : 0L;
			System.out.println("Converted " + attribute + " to " + convertedValue + " for the database.");
			return convertedValue;
		}

		@Override
		public LocalDate convertToEntityAttribute(Long dbData) {
			LocalDate convertedValue = dbData != null && dbData != 0
					? Instant.ofEpochMilli(dbData).atZone(ZoneOffset.UTC).toLocalDate()
					: null;
			System.out.println("Converted database value " + dbData + " into " + convertedValue);
			return convertedValue;
		}
	}

	@Configuration
	@ImportResource("classpath:infrastructure.xml")
	@EnableJpaRepositories(basePackageClasses = MyEntityRepository.class, considerNestedRepositories = true,
			includeFilters = @ComponentScan.Filter(value = { MyEntityRepository.class }, type = FilterType.ASSIGNABLE_TYPE))
	static class Config {

		@Bean
		CustomAttributeConverter customAttributeConverter() {
			return new CustomAttributeConverter();
		}
	}
}

It produced:

Converted 2022-06-01 to 1654041600000 for the database.
Converted null to 0 for the database.
Converted 2022-06-01 to 1654041600000 for the database.
Converted database value 1654041600000 into 2022-06-01

There are some extra infrastructure bits not shown since we don't have access to Spring Boot. It should be pointed out that I had to add @Nullable to the repository method. I'll attempt the same thing but against Spring Data 2.7.0 and see what happens.

@gregturn
Copy link
Contributor

gregturn commented Jun 1, 2022

Okay, I ran the same thing against Spring Data 2.7.x, and it still passed:

@ExtendWith(SpringExtension.class)
@ContextConfiguration
public class AttributeConverterIntegrationTests {

	@Autowired MyEntityRepository repository;

	@Test
	void nullBasedFinderShouldBlowUp() {

		LocalDate now = LocalDate.now();

		repository.save(new MyEntity("TEST", now));
		repository.save(new MyEntity("Null", null));

		List<MyEntity> dates = repository.findByDate(now);
		dates = repository.findByDate(null);
	}

	@Entity
	@Table(name = "my_entity")
	@Data
	@NoArgsConstructor(access = AccessLevel.PROTECTED)
	static class MyEntity {

		@Id
		@GeneratedValue private Long id;

		private String value;

		@Convert(converter = CustomAttributeConverter.class) //
		private LocalDate date;

		public MyEntity(String value, LocalDate date) {

			this.value = value;
			this.date = date;
		}
	}

	interface MyEntityRepository extends JpaRepository<MyEntity, Long> {

		@Query("select e from AttributeConverterIntegrationTests$MyEntity e WHERE e.date = :date")
		List<MyEntity> findByDate(@Nullable @Param("date") LocalDate date);
	}

	static class CustomAttributeConverter implements AttributeConverter<LocalDate, Long> {

		@Override
		public Long convertToDatabaseColumn(LocalDate attribute) {
			long convertedValue = attribute != null ? attribute.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli() : 0L;
			System.out.println("Converted " + attribute + " to " + convertedValue + " for the database.");
			return convertedValue;
		}

		@Override
		public LocalDate convertToEntityAttribute(Long dbData) {
			LocalDate convertedValue = dbData != null && dbData != 0
					? Instant.ofEpochMilli(dbData).atZone(ZoneOffset.UTC).toLocalDate()
					: null;
			System.out.println("Converted database value " + dbData + " into " + convertedValue);
			return convertedValue;
		}
	}

	@Configuration
	@ImportResource("classpath:infrastructure.xml")
	@EnableJpaRepositories(basePackageClasses = MyEntityRepository.class, considerNestedRepositories = true,
			includeFilters = @ComponentScan.Filter(value = { MyEntityRepository.class }, type = FilterType.ASSIGNABLE_TYPE))
	static class Config {

		@Bean
		CustomAttributeConverter customAttributeConverter() {
			return new CustomAttributeConverter();
		}
	}
}

Produces:

Converted 2022-06-01 to 1654041600000 for the database.
Converted null to 0 for the database.
Converted 2022-06-01 to 1654041600000 for the database.
Converted database value 1654041600000 into 2022-06-01

Not sure if there is something else happening that is causing havoc. The biggest differences are see are A) you're using postgresql while I'm using HSQL and B) I added @Nullable to the input on that finder method.

@gregturn
Copy link
Contributor

gregturn commented Jun 1, 2022

If I don't put in that @Nullable, this happens:

java.lang.IllegalArgumentException: Parameter date in AttributeConverterIntegrationTests.MyEntityRepository.findByDate must not be null!

	at org.springframework.data.repository.core.support.MethodInvocationValidator.invoke(MethodInvocationValidator.java:93)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
	at org.springframework.data.jpa.domain.support.$Proxy119.findByDate(Unknown Source)
	at org.springframework.data.jpa.domain.support.AttributeConverterIntegrationTests.nullBasedFinderShouldBlowUp(AttributeConverterIntegrationTests.java:49)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:725)
	at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
	at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
	at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:214)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:210)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:66)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.util.ArrayList.forEach(ArrayList.java:1259)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.util.ArrayList.forEach(ArrayList.java:1259)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
	at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
	at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53)
	at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)

@gregturn
Copy link
Contributor

gregturn commented Jun 1, 2022

Part of my concern is what you're expecting to happen when you invoke findByDate(null).

select e from AttributeConverterIntegrationTests$MyEntity e WHERE e.date = :date flat out shouldn't work if :date is null. It's a classic issue with relational databases that null doesn't equal null, hence the need for IS NULL checks. Typically you'd need something like: select e from AttributeConverterIntegrationTests$MyEntity e WHERE e.date = :date OR :date IS NULL to properly handle this.

I guess this isn't coming up because your database engine is blowing up sooner?

@gregturn
Copy link
Contributor

gregturn commented Jun 2, 2022

Okay, I swapped out HSQL and plugged in Testcontainers so I could use PostgreSQL, and what do you know...

Converted 2022-06-02 to 1654128000000 for the database.
Converted null to 0 for the database.
Converted 2022-06-02 to 1654128000000 for the database.
Converted database value 1654128000000 into 2022-06-02
2022-06-02 10:09:29,626 ERROR rnate.engine.jdbc.spi.SqlExceptionHelper: 142 - ERROR: operator does not exist: bigint = date
  Hint: No operator matches the given name and argument type(s). You might need to add explicit type casts.
  Position: 149

org.springframework.dao.InvalidDataAccessResourceUsageException: could not extract ResultSet; SQL [n/a]; nested exception is org.hibernate.exception.SQLGrammarException: could not extract ResultSet

This operator does not exist: bigint = date thing has caught my eye.

Looks very similar to yours:

Caused by: org.postgresql.util.PSQLException: ERREUR: l'opérateur n'existe pas : numeric = date
  Indice : Aucun opérateur ne correspond au nom donné et aux types d'arguments.
Vous devez ajouter des conversions explicites de type.

Investigating further.

@gregturn
Copy link
Contributor

gregturn commented Jun 3, 2022

More digging has revealed that Hibernate does NOT apply attribute converters when null values are present. There appear to have been multiple tickets opened in the past and then closed, with the team citing JPA specs as not explicitly saying that null values have to be routed through attribute converters.

Related: https://hibernate.atlassian.net/browse/HHH-8697, https://hibernate.atlassian.net/browse/HHH-9901, https://hibernate.atlassian.net/browse/HHH-9320

@quaff
Copy link
Contributor

quaff commented Jun 6, 2022

@gregturn Maybe regression introduced by #2461.

@CyrilChevalier
Copy link
Author

CyrilChevalier commented Jun 10, 2022

Hi @gregturn,

Sorry for the late answer and thank you for all the research.

I had seen that it worked with HSQL and not with Postgresql. Hibernate not using AttributeConverter for null values is a concern. I've made a workaround in our code for these cases.

For @quaff : indeed, as I previously said, I think the problem comes from the changes in the way Spring Data processes the null values. The new class HibernateJpaParametersParameterAccessor transforms null values into TypedParameterValue objects. As a consequence, the AttributeConverter for these values is not called anymore by Hibernate.

Thanks again for your work, guys!

@gregturn
Copy link
Contributor

@CyrilChevalier I just patched the way null values are handled inside Hibernate for LIKE and contains operations. Take a look and see if your issue is still pressing.

@gregturn
Copy link
Contributor

If you could provide us with a reproducer showing that attribute converters are indeed invoke for null values, I would appreciate that.

@gregturn gregturn added status: waiting-for-feedback We need additional information before we can continue and removed status: waiting-for-triage An issue we've not yet triaged labels Jul 18, 2022
@therealaleko
Copy link

@gregturn I've not dug into all the information given here, but I opened #2601 which was closed due to relation to this issue.

The piece I am querying about is the fact that this says AttributeConverter is not called anymore. If this is true for my issue, then can you explain why the workaround I give in my issue works?

Simply moving the null check to the last conditional allows binding to work properly.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Jul 20, 2022
@schauder
Copy link
Contributor

@therealaleko #2601 wasn't closed due to relation to this issue, but because it is a duplicate of #2548.

@schauder schauder added status: waiting-for-feedback We need additional information before we can continue and removed status: feedback-provided Feedback has been provided labels Jul 20, 2022
@therealaleko
Copy link

therealaleko commented Jul 20, 2022

@therealaleko #2601 wasn't closed due to relation to this issue, but because it is a duplicate of #2548.

@schauder
Right, but inside that issue it says
"Related: #2549"

and it was closed without information from what I can tell. So I'm left confused as to what/where the root cause of my issue is. Can you help me pin point that so I can track it, please?

@therealaleko
Copy link

therealaleko commented Jul 20, 2022

@schauder nevermind, I found the trail. Sorry for my confusion!
3d8b287

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Jul 20, 2022
@gregturn
Copy link
Contributor

This fix was backported to 2.7.x with f0216b0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: feedback-provided Feedback has been provided
Projects
None yet
Development

No branches or pull requests

7 participants