Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JPA2: Case-insensitive like matching anywhere

I have been using Hibernate Restrictions in JPA 1.0 ( Hibernate driver ). There is defined Restrictions.ilike("column","keyword", MatchMode.ANYWHERE) which tests if the keyword matching the column anywhere and it is case-insensitive.

Now, I am using JPA 2.0 with EclipseLink as driver so I have to use "Restrictions" build-in JPA 2.0. I found CriteriaBuilder and method like, I have also found out how to make it matching anywhere ( although it is aweful and manual ), but still I haven't figured out how to do it case-insensitive.

There is my current aweful solution:

CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<User> query = builder.createQuery(User.class);
EntityType<User> type = em.getMetamodel().entity(User.class);
Root<User> root = query.from(User.class);

// Where   
// important passage of code for question  
query.where(builder.or(builder.like(root.get(type.getDeclaredSingularAttribute("username", String.class)), "%" + keyword + "%"),
        builder.like(root.get(type.getDeclaredSingularAttribute("firstname", String.class)), "%" + keyword + "%"),
        builder.like(root.get(type.getDeclaredSingularAttribute("lastname", String.class)), "%" + keyword + "%")
        ));

// Order By
query.orderBy(builder.asc(root.get("lastname")),
            builder.asc(root.get("firstname")));

// Execute
return em.createQuery(query).
            setMaxResults(PAGE_SIZE + 1).
            setFirstResult((page - 1) * PAGE_SIZE).
            getResultList();

Questions:

Is there any function like in Hibernate driver?

Am I using the JPA 2.0 criteria correctly? This is awkward and uncomfortable solution in compare to Hibernate Restrictions.

Or can anybody help me how to change my solution to be case-insensitive, please?

Thanks a lot.

like image 413
Gaim Avatar asked Jan 02 '11 20:01

Gaim


4 Answers

It may seem a little awkward at first, but it is type-safe. Building queries from strings isn't, so you notice errors at runtime instead of at compile time. You can make the queries more readable by using indentations or taking each step separately, instead of writing an entire WHERE clause in a single line.

To make your query case-insensitive, convert both your keyword and the compared field to lower case:

query.where(
    builder.or(
        builder.like(
            builder.lower(
                root.get(
                    type.getDeclaredSingularAttribute("username", String.class)
                )
            ), "%" + keyword.toLowerCase() + "%"
        ), 
        builder.like(
            builder.lower(
                root.get(
                    type.getDeclaredSingularAttribute("firstname", String.class)
                )
            ), "%" + keyword.toLowerCase() + "%"
        ), 
        builder.like(
            builder.lower(
                root.get(
                    type.getDeclaredSingularAttribute("lastname", String.class)
                )
            ), "%" + keyword.toLowerCase() + "%"
        )
    )
);
like image 87
weltraumpirat Avatar answered Nov 18 '22 05:11

weltraumpirat


As I commented in the (currently) accepted answer, there is a pitfall using on one hand DBMS' lower() function and on the other hand java's String.toLowerCase() as both method are not warrantied to provide the same output for the same input string.

I finally found a much safer (yet not bullet-proof) solution which is to let the DBMS do all the lowering using a literal expression:

builder.lower(builder.literal("%" + keyword + "%")

So the complete solution would look like :

query.where(
    builder.or(
        builder.like(
            builder.lower(
                root.get(
                    type.getDeclaredSingularAttribute("username", String.class)
                )
            ), builder.lower(builder.literal("%" + keyword + "%")
        ), 
        builder.like(
            builder.lower(
                root.get(
                    type.getDeclaredSingularAttribute("firstname", String.class)
                )
            ), builder.lower(builder.literal("%" + keyword + "%")
        ), 
        builder.like(
            builder.lower(
                root.get(
                    type.getDeclaredSingularAttribute("lastname", String.class)
                )
            ), builder.lower(builder.literal("%" + keyword + "%")
        )
    )
);

Edit:
As @cavpollo requested me to give example, I had to think twice about my solution and realized it's not that much safer than the accepted answer:

DB value* | keyword | accepted answer | my answer
------------------------------------------------
elie     | ELIE    | match           | match
Élie     | Élie    | no match        | match
Élie     | élie    | no match        | no match
élie     | Élie    | match           | no match

Still, I prefer my solution as it does not compare the outcome out two different functions that are supposed to work alike. I apply the very same function to all character arrays so that comparing the output become more "stable".

A bullet-proof solution would involve locale so that SQL's lower() become able to correctly lower accented characters. (But this goes beyond my humble knowledge)

*Db value with PostgreSQL 9.5.1 with 'C' locale

like image 45
Ghurdyl Avatar answered Nov 18 '22 06:11

Ghurdyl


This work for me :

CriteriaBuilder critBuilder = em.getCriteriaBuilder();

CriteriaQuery<CtfLibrary> critQ = critBuilder.createQuery(Users.class);
Root<CtfLibrary> root = critQ.from(Users.class);

Expression<String> path = root.get("lastName");
Expression<String> upper =critBuilder.upper(path);
Predicate ctfPredicate = critBuilder.like(upper,"%stringToFind%");
critQ.where(critBuilder.and(ctfPredicate));
em.createQuery(critQ.select(root)).getResultList();
like image 8
alpanko Avatar answered Nov 18 '22 05:11

alpanko


If you are using a database like Postgres which supports ilike which provides a much better performance as using the lower() function none of the provided solution solves the issue properly.

A solution can be a custom function.

The HQL query you are writing is:

SELECT * FROM User WHERE (function('caseInSensitiveMatching', name, '%test%')) = true

Where the caseInSensitiveMatching is the function name of our custom function. The name is the path to the property which you want to compare with and the %test% is the pattern which you want to match it against.

The goal is to convert the HQL query into the following SQL query:

SELECT * FROM User WHERE (name ilike '%test%') = true

To achieve this we have to implement our own dialect with our custom function registered:

    public class CustomPostgreSQL9Dialect extends PostgreSQL9Dialect {
        /**
         * Default constructor.
         */
        public CustomPostgreSQL9Dialect() {
            super();
            registerFunction("caseInSensitiveMatching", new CaseInSensitiveMatchingSqlFunction());
        }

        private class CaseInSensitiveMatchingSqlFunction implements SQLFunction {

            @Override
            public boolean hasArguments() {
                return true;
            }

            @Override
            public boolean hasParenthesesIfNoArguments() {
                return true;
            }

            @Override
            public Type getReturnType(Type firstArgumentType, Mapping mapping) throws QueryException {
                return StandardBasicTypes.BOOLEAN;
            }

            @Override
            public String render(Type firstArgumentType, @SuppressWarnings("rawtypes") List arguments,
                    SessionFactoryImplementor factory) throws QueryException {

                if (arguments.size() != 2) {
                    throw new IllegalStateException(
                            "The 'caseInSensitiveMatching' function requires exactly two arguments.");
                }

                StringBuilder buffer = new StringBuilder();

                buffer.append("(").append(arguments.get(0)).append(" ilike ").append(arguments.get(1)).append(")");

                return buffer.toString();
            }

        }

    }

The above optimization produced in our situation a performance improvement of a factor of 40 compared to the version with the lower function as Postgres could leverage the index on the corresponding column. In our situation the query execution time could be reduced from 4.5 seconds to 100 ms.

The lower prevents an efficient usage of the index and as such it is much slower.

like image 5
Thomas Hunziker Avatar answered Nov 18 '22 05:11

Thomas Hunziker