6

I'm using Criteria API to build named queries using filters. It works on normal String comparisons but when filtering on UUID it throws the following error:

org.springframework.dao.InvalidDataAccessApiUsageException: Parameter value [67279329-5096-4196-9E73-748B33122CE2] did not match expected type [java.util.UUID (n/a)]; nested exception is java.lang.IllegalArgumentException: Parameter value [67279329-5096-4196-9E73-748B33122CE2] did not match expected type [java.util.UUID (n/a)]

There are several questions addressing this issue but none of them worked, I tried the following:

  • adding @Column(name = "id", updatable = false, nullable = false) to the entity field
  • adding @Type(type="org.hibernate.type.UUIDCharType") to the entity field
  • adding @Type(type="uuid-char") to the entity field

Foo entity:

@Entity
//lombok stuff
public class Foo {

    @Id
    private UUID id;
    private String name;
    //...
}

SQL Variant:

CREATE TABLE foo
(
    id                    UUID    NOT NULL PRIMARY KEY,
    name                  VARCHAR NOT NULL,
    ...
);

FooController:

@GetMapping(value = "/foo")
public ResponseEntity<List<Foo>> findFoos(@RequestParam Map<String, String> filterArguments) {
    FooFilter filter = filterMapper.map(filterArguments);
    FooSpecification spec = new FooSpecification(filter);

    List<Foo> foos = fooRepo.findAll(spec);
    //...
}

FooSpecification:

public class FooSpecification extends SpecificationHelper implements Specification<Foo> {
    private final FooFilter filter;

    public FooSpecification(FooFilter filter) {
        this.filter = filter;
    }

    @Override
    public Predicate toPredicate(Root<Foo> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
        Predicate predicate = null;

        predicate = createIdPredicate(root, filter, criteriaBuilder, predicate);
        predicate = createNamePredicate(root, filter, criteriaBuilder, predicate);
        // ...

        return predicate;
    }

    private Predicate createIdPredicate(Root<Foo> foo, FooFilter filter, CriteriaBuilder cb, Predicate predicate) {
        Predicate returnPredicate = predicate;
        if (StringUtils.isNotBlank(filter.getId()))
            returnPredicate = addAndPredicate(cb, predicate, cb.like(cb.upper(foo.get("id")), "%" + filter.getId().toUpperCase() + "%"));
        return returnPredicate;
    }

    private Predicate createNamePredicate(Root<Foo> foo, FooFilter filter, CriteriaBuilder cb, Predicate predicate) {
        Predicate returnPredicate = predicate;
        if (StringUtils.isNotBlank(filter.getName()))
            returnPredicate = addAndPredicate(cb, predicate, cb.like(cb.upper(foo.get("name")), "%" + filter.getName().toUpperCase() + "%"));
        return returnPredicate;
    }
}

addAndPredicate is a simple helper method that just uses criteriaBuilder.and(predicate,newPredicate)

FooFilter only has String fields

Edito
  • 3,030
  • 13
  • 35
  • 67
  • 2
    Looks like createIdPredicate creates predicate for id as a String rather than UUID. Try to parse string from FooFilter into UUID: UUID.fromString(filter.getId()) – tentacle Mar 10 '20 at 16:42
  • @tentacle I don't quite understand your comment, CriteriaBuilder's `like()` method only accepts string based `Expression`s. Only way your solution would word is by adding `"" + UUID.fromString(filter.getId())` which yields the same result as before. I also updated the question, the uuid should also be compared with '%' wildcards. – Edito Mar 11 '20 at 07:49

3 Answers3

4

The problem is that your FooFilter contains only String fields, and you are trying to compare String id with a UUID object in createIdPredicate(). And that's why the exception is thrown. There are 2 solutions:

  • Either, you should replace filter.getId().toUpperCase() part in createIdPredicate() with UUID.fromString(filter.getId()) as suggested by @tentacle
  • Or, change your FooFilter filter so that the id would be of type java.util.UUID.

By the way, IMHO, comparing UUIDs with like is not a good idea, because one UUID can never be like another UUID, each UUID object is unique. Your predicate should check the equality of ids transferred by FooFilter and the one from DB.

Armine
  • 1,675
  • 2
  • 24
  • 40
  • - First solution will not work as explained in my original comment. - Second could work but what I'm trying to achieve is a simple String comparison, all my params coming from the controller are Strings, sure I could (and probably should) map the strings to an actual object, same thing for dates etc. I follow your opinion 100%, but sadly as a programmer I have to follow business requirements, and if bussines persists in filtering on id's _I just do what I gotta do_ – Edito Mar 11 '20 at 10:42
2

I fixed the issue using typecasting provided by the Criteria API.

Before:

addAndPredicate(..., ..., cb.like(cb.upper(foo.get("id")), ...));

After:

addAndPredicate(..., ..., cb.like(cb.upper(foo.get("id").as(String.class)), ...));

From: https://docs.oracle.com/javaee/6/api/javax/persistence/criteria/Expression.html#as(java.lang.Class)

<X> Expression<X> as(java.lang.Class<X> type)

Perform a typecast upon the expression, returning a new expression object. This method does not cause type conversion: the runtime type is not changed. Warning: may result in a runtime failure.

Edito
  • 3,030
  • 13
  • 35
  • 67
0

I just had a similar trouble and @edward answer actually helped me a lot :)

One thing that I have changed is the use of cb.equal instead of the cb.like, because when searching for a specific id the equal way seems to be more efficient.

One other thing I've done is to parse only once my filter param with UUID.fromString(uuid_string_param) so that I just parse the param once and dont have to cast database UUID field to string in order to get the match.

Cheers.