Daniel Taub
Daniel Taub

Reputation: 5389

Spring CriteriaBuilder search enum by it's name

When I'm trying to search enum by his name with Specification in my DB using Spring @Repository, I'm getting the following exception:

Caused by: java.lang.IllegalArgumentException: Parameter value [HELLO] did not match expected type [application.springEnum.Hello (n/a)]

But in the DB the enum saved as VARCHAR(255) so why I can search the enum with String, why It's need to by a Enum type?

DTO class

@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class DTO {
    @Id
    private String id;
    @Enumerated(EnumType.STRING)
    private Hello helloEnum; // My Enum
}

DataBase connector

@Repository
public interface Connector extends JpaRepository<DTO, String>, JpaSpecificationExecutor<DTO> {
}

Starter

@Component
public class Starter {
    @Autowired
    private Connector connector;

    @PostConstruct
    public void init(){
        // Create DTO entity
        DTO dto = DTO.builder()
                .id(UUID.randomUUID().toString())
                .helloEnum(Hello.HELLO)
                .build();
        // Save the entity in the db
        connector.save(dto);

        // Search by the name, here I get the excpetion
        List<DTO> result = connector.findAll((root, query, cb) ->
                cb.equal(root.get("helloEnum"), "HELLO")
        );
    }
}

I would appreciate for an explanation.

Upvotes: 12

Views: 14859

Answers (2)

J-Alex
J-Alex

Reputation: 7117

You're trying to compare Enum and String.

Try this way:

List<DTO> result = connector.findAll((root, query, cb) ->
                cb.equal(root.get("helloEnum"), Hello.HELLO);

I will try to provide some explanations why this is happening. Hibernate fetches ResultSet from Database to Class signature using Reflection.

Observing stacktrace you will see something like:

org.hibernate.query.spi.QueryParameterBindingValidator.validate(QueryParameterBindingValidator.java:54) ~[hibernate-core-5.2.16.Final.jar:5.2.16.Final] at org.hibernate.query.spi.QueryParameterBindingValidator.validate(QueryParameterBindingValidator.java:27) ~[hibernate-core-5.2.16.Final.jar:5.2.16.Final] at org.hibernate.query.internal.QueryParameterBindingImpl.validate(QueryParameterBindingImpl.java:90) ~[hibernate-core-5.2.16.Final.jar:5.2.16.Final] at org.hibernate.query.internal.QueryParameterBindingImpl.setBindValue(QueryParameterBindingImpl.java:55) ~[hibernate-core-5.2.16.Final.jar:5.2.16.Final] at org.hibernate.query.internal.AbstractProducedQuery.setParameter(AbstractProducedQuery.java:486) ~[hibernate-core-5.2.16.Final.jar:5.2.16.Final] at org.hibernate.query.internal.AbstractProducedQuery.setParameter(AbstractProducedQuery.java:104) ~[hibernate-core-5.2.16.Final.jar:5.2.16.Final]

Hibernate performs a bunch of validations before setting a parameter.

Here is the last method which initializes a root cause for Exception:

public <P> void validate(Type paramType, Object bind, TemporalType temporalType) {
        if ( bind == null || paramType == null ) {
            // nothing we can check
            return;
        }
        final Class parameterType = paramType.getReturnedClass();
        if ( parameterType == null ) {
            // nothing we can check
            return;
        }

        if ( Collection.class.isInstance( bind ) && !Collection.class.isAssignableFrom( parameterType ) ) {
            // we have a collection passed in where we are expecting a non-collection.
            //      NOTE : this can happen in Hibernate's notion of "parameter list" binding
            //      NOTE2 : the case of a collection value and an expected collection (if that can even happen)
            //          will fall through to the main check.
            validateCollectionValuedParameterBinding( parameterType, (Collection) bind, temporalType );
        }
        else if ( bind.getClass().isArray() ) {
            validateArrayValuedParameterBinding( parameterType, bind, temporalType );
        }
        else {
            if ( !isValidBindValue( parameterType, bind, temporalType ) ) {
                throw new IllegalArgumentException(
                        String.format(
                                "Parameter value [%s] did not match expected type [%s (%s)]",
                                bind,
                                parameterType.getName(),
                                extractName( temporalType )
                        )
                );
            }
        }
    }

The method private static boolean isValidBindValue(Class expectedType, Object value, TemporalType temporalType) which has a bunch of checks retuns false because your expected type is class com.whatever.Hello and value to check is HELLO what is String, but Enum type and String are incompatible!

If you will put proper Enum in your search criteria, validation will pass because private static boolean isValidBindValue(Class expectedType, Object value, TemporalType temporalType) contains isInstance check which will pass:

else if ( expectedType.isInstance( value ) ) {
    return true;
}

After all the checks Hibernate extracts values from ResultSet and builds the List, in this particular case, elements of List are fetched using reflection.

Upvotes: 9

Peteef
Peteef

Reputation: 405

public class Main {
enum Hello {
    HELLO
}
public static void main(String[] args) {
    Hello hello = Hello.HELLO;
    System.out.println(hello.toString().equals("HELLO")); //true
    System.out.println("HELLO".equals(hello.toString())); //true
    System.out.println(hello.toString() == "HELLO"); //true
    System.out.println(hello.equals("HELLO")); //false
    System.out.println("HELLO".equals(hello)); //false
//        System.out.println(hello == "HELLO"); //incompatible types
}
}

Not completely sure but equal() parameters have to be in same type.

root.get("helloEnum")

is instance of Hello

"HELLO"

is instance of String.

There is no automagic toString() cast on enum here. Just to be sure try:

// Search by the name, here I get the excpetion
    List<DTO> result = connector.findAll((root, query, cb) ->
            cb.equal(root.get("helloEnum").toString(), "HELLO")
    );

Upvotes: -2

Related Questions