mkczyk
mkczyk

Reputation: 2700

Spring Data: Query By Example and Converter

I have an entity:

import javax.persistence.Convert;

@Entity(name = "my_entity")
public class MyEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @Convert(converter = StringListConverter.class)
    private List<String> emails;

    // next fields omitted...

}

Converter which is used by emails field in entity (typical implementation like LocalDateTimeConverter):

import javax.persistence.Converter;

@Converter
public class StringListConverter implements AttributeConverter<List<String>, String> {
    private static final String SPLIT_CHAR = ";";

    @Override
    public String convertToDatabaseColumn(List<String> stringList) {
        if (CollectionUtils.isNotEmpty(stringList)) {
            return String.join(SPLIT_CHAR, stringList);
        } else {
            return null;
        }
    }

    @Override
    public List<String> convertToEntityAttribute(String string) {
        if (StringUtils.isNotBlank(string)) {
            return Arrays.asList(string.split(SPLIT_CHAR));
        } else {
            return Collections.emptyList();
        }
    }

}

(I store emails separated by semicolons in one column. StringListConverter do that conversion.)

And Spring Data repository:

import org.springframework.data.domain.Example;

public interface MyRepository extends JpaRepository<MyEntity, Long> {

    default List<MyEntity> findMatchingMyEntity(MyEntity myEntity) {
        Example<MyEntity> example = Example.of(myEntity);
        return findAll(example);
    }

}

I use Query by Example mechanism from Spring Data. When I have fields without @Convert (like String name) it works. But when I have field with @Convert (AttributeConverter) like List<String> emails it causes InvalidDataAccessApiUsageException.

org.springframework.dao.InvalidDataAccessApiUsageException: Parameter value [[email protected]] did not match expected type [java.util.List (n/a)]; nested exception is java.lang.IllegalArgumentException: Parameter value [[email protected]] did not match expected type [java.util.List (n/a)]
    at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:374) ~[spring-orm-5.2.1.RELEASE.jar:5.2.1.RELEASE]
    at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:257) ~[spring-orm-5.2.1.RELEASE.jar:5.2.1.RELEASE]
    at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:528) ~[spring-orm-5.2.1.RELEASE.jar:5.2.1.RELEASE]
    at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61) ~[spring-tx-5.2.1.RELEASE.jar:5.2.1.RELEASE]
    ...
Caused by: java.lang.IllegalArgumentException: Parameter value [[email protected]] did not match expected type [java.util.List (n/a)]
    at org.hibernate.query.spi.QueryParameterBindingValidator.validate(QueryParameterBindingValidator.java:54) ~[hibernate-core-5.4.8.Final.jar:5.4.8.Final]
    at org.hibernate.query.spi.QueryParameterBindingValidator.validate(QueryParameterBindingValidator.java:27) ~[hibernate-core-5.4.8.Final.jar:5.4.8.Final]
    at org.hibernate.query.internal.QueryParameterBindingImpl.validate(QueryParameterBindingImpl.java:90) ~[hibernate-core-5.4.8.Final.jar:5.4.8.Final]
    ... 146 common frames omitted

(message is weird because I've tried search with that list: ["[email protected]", "[email protected]"], but in message is only one email)


I've tried to implement transform in ExampleMatcher:

import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;

public interface MyRepository extends JpaRepository<MyEntity, Long> {

    default List<MyEntity> findMatchingMyEntity(MyEntity myEntity) {
        ExampleMatcher matcher = ExampleMatcher.matching()
                .withMatcher("emails",
                        match -> match.transform(emailsOptional -> {
                            if (emailsOptional.isPresent()) {
                                List<String> emails = (List<String>) emailsOptional.get();
                                return Optional.ofNullable(new StringListConverter().convertToDatabaseColumn(emails));
                            }
                            return emailsOptional;
                        }));
        Example<MyEntity> example = Example.of(myEntity, matcher);
        return findAll(example);
    }

}

But is causes InvalidDataAccessApiUsageException too, but with different message than previous one (there are two emails that I've set):

org.springframework.dao.InvalidDataAccessApiUsageException: Parameter value [[email protected];[email protected]] did not match expected type [java.util.List (n/a)]; nested exception is java.lang.IllegalArgumentException: Parameter value [[email protected];[email protected]] did not match expected type [java.util.List (n/a)]
    at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:374) ~[spring-orm-5.2.1.RELEASE.jar:5.2.1.RELEASE]
    at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:257) ~[spring-orm-5.2.1.RELEASE.jar:5.2.1.RELEASE]
    at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:528) ~[spring-orm-5.2.1.RELEASE.jar:5.2.1.RELEASE]
Caused by: java.lang.IllegalArgumentException: Parameter value [[email protected];[email protected]] did not match expected type [java.util.List (n/a)]
    at org.hibernate.query.spi.QueryParameterBindingValidator.validate(QueryParameterBindingValidator.java:54) ~[hibernate-core-5.4.8.Final.jar:5.4.8.Final]
    at org.hibernate.query.spi.QueryParameterBindingValidator.validate(QueryParameterBindingValidator.java:27) ~[hibernate-core-5.4.8.Final.jar:5.4.8.Final]
    ... 146 common frames omitted

Upvotes: 4

Views: 3401

Answers (1)

Bartosz Szymański
Bartosz Szymański

Reputation: 447

It seems that for some reason Hibernate is trying to split the array of emails into multiple conditions just like in IN query in SQL using expandListValuedParameters method. Note - with your solution doing query like findAllByEmailsIn(List<String> emailsList) also won't work.

Method expandListValuedParameters is deprecated since Hibernate 5.2, so it may contains some problems and for sure will be implemented differently in Hibernate 6.0.

I haven't found a fix for your problem, but there are some workarounds:

  1. Wrap your List<String> emails in another class

Wrapper class:

public class EmailList {

   private List<String> emails;
     
   // getters, setters, constructors ommited

}

Updated model class:

import javax.persistence.Convert;
  
@Entity(name = "my_entity")
public class MyEntity {
  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @Convert(converter = StringEmailListConverter.class)
    private EmailList emailList;

    // next fields omitted...
}

Updated converter class:

import javax.persistence.Converter;

@Converter
public class StringEmailListConverter implements AttributeConverter<EmailList, String> {
    private static final String SPLIT_CHAR = ";";

    @Override
    public String convertToDatabaseColumn(EmailList emailList) {
        if (emailList != null && CollectionUtils.isNotEmpty(emailList.getEmails())) {
            return String.join(SPLIT_CHAR, emailList.getEmails());
        } else {
            return null;
        }
    }

    @Override
    public EmailList convertToEntityAttribute(String string) {
        if (StringUtils.isNotBlank(string)) {
            return new EmailList(Arrays.asList(string.split(SPLIT_CHAR)));
        } else {
            return new EmailList(Collections.emptyList());
        }
    }

}

And Spring Data repository will work fine with this code - no need for using transform:

import org.springframework.data.domain.Example;

public interface MyRepository extends JpaRepository<MyEntity, Long> {

    default List<MyEntity> findMatchingMyEntity(MyEntity myEntity) {
        Example<MyEntity> example = Example.of(myEntity);
        return findAll(example);
    }

}
  1. Use String[] emails instead of List<String> emails You need to change MyEntity and Converter respectively to use String[]. Of course using String[] sometimes is not an option because you specifically need a List.

Upvotes: 1

Related Questions