For this question, we want to avoid having to write a special query since the query would have to be different across multiple databases. Using only hibernate criteria, we want to be able to escape special characters.
This situation is the reason for needing the ability to escape special characters:
Assume that we have table 'foo' in the database. Table 'foo' contains only 1 field, called 'name'. The 'name' field can contain characters that may be considered special in a database. Two examples of such a name are 'name_1' and 'name%1'. Both the '_' and '%' are special characters, at least in Oracle. If a user wants to search for one of these examples after they are entered in the database, problems may occur.
criterion = Restrictions.ilike("name", searchValue, MatchMode.ANYWHERE);
return findByCriteria(null, criterion);
In this code, 'searchValue' is the value that the user has given the application to use for its search. If the user wants to search for '%', the user is going to be returned with every 'foo' entry in the database. This is because the '%' character represents the "any number of characters" wildcard for string matching and the SQL code that hibernate produces will look like:
select * from foo where name like '%'
Is there a way to tell hibernate to escape certain characters, or to create a workaround that is not database type specific?
To search for a special character that has a special function in the query syntax, you must escape the special character by adding a backslash before it, for example: To search for the string "where?", escape the question mark as follows: "where\?"
Hibernate provides alternate ways of manipulating objects and in turn data available in RDBMS tables. One of the methods is Criteria API, which allows you to build up a criteria query object programmatically where you can apply filtration rules and logical conditions.
Since Hibernate 5.2, the Hibernate Criteria API is deprecated, and new development is focused on the JPA Criteria API. We'll explore how to use Hibernate and JPA to build Criteria Queries.
Using Escape in JPA Before writing the JPQL query, we have to add an escape character to the beginning of the input text character, which needs to be escaped. The code snippet shown below adds the escape character '\' to the beginning of the character, which should be escaped. String name = name.
LikeExpression's constructors are all protected, so it's not a viable option. Also, it has problems of its own.
A colleague and I created a patch which works pretty well. The gist of the patch is that for the LikeExpression constructor which consumes a MatchMode, we escape the special characters. For the constructor which consumes a Character (the escape character), we assume the user escapes the special characters on their own.
We also parameterized the escape character to ensure that it can't corrupt the SQL query if they use something like \ or a quote character.
package org.hibernate.criterion;
import org.hibernate.Criteria;
import org.hibernate.HibernateException;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.TypedValue;
public class LikeExpression implements Criterion {
private final String propertyName;
private final String value;
private final Character escapeChar;
protected LikeExpression(
String propertyName,
Object value) {
this(propertyName, value.toString(), (Character) null);
}
protected LikeExpression(
String propertyName,
String value,
MatchMode matchMode) {
this( propertyName, matchMode.toMatchString( value
.toString()
.replaceAll("!", "!!")
.replaceAll("%", "!%")
.replaceAll("_", "!_")), '!' );
}
protected LikeExpression(
String propertyName,
String value,
Character escapeChar) {
this.propertyName = propertyName;
this.value = value;
this.escapeChar = escapeChar;
}
public String toSqlString(
Criteria criteria,
CriteriaQuery criteriaQuery) throws HibernateException {
Dialect dialect = criteriaQuery.getFactory().getDialect();
String[] columns = criteriaQuery.getColumnsUsingProjection( criteria, propertyName );
if ( columns.length != 1 ) {
throw new HibernateException( "Like may only be used with single-column properties" );
}
String lhs = lhs(dialect, columns[0]);
return lhs + " like ?" + ( escapeChar == null ? "" : " escape ?" );
}
public TypedValue[] getTypedValues(
Criteria criteria,
CriteriaQuery criteriaQuery) throws HibernateException {
return new TypedValue[] {
criteriaQuery.getTypedValue( criteria, propertyName, typedValue(value) ),
criteriaQuery.getTypedValue( criteria, propertyName, escapeChar.toString() )
};
}
protected String lhs(Dialect dialect, String column) {
return column;
}
protected String typedValue(String value) {
return value;
}
}
If you're wondering what the lhs and typedValue methods are for, the new IlikeExpression should answer those questions.
package org.hibernate.criterion;
import org.hibernate.dialect.Dialect;
public class IlikeExpression extends LikeExpression {
protected IlikeExpression(
String propertyName,
Object value) {
super(propertyName, value);
}
protected IlikeExpression(
String propertyName,
String value,
MatchMode matchMode) {
super(propertyName, value, matchMode);
}
protected IlikeExpression(
String propertyName,
String value,
Character escapeChar) {
super(propertyName, value, escapeChar);
}
@Override
protected String lhs(Dialect dialect, String column) {
return dialect.getLowercaseFunction() + '(' + column + ')';
}
@Override
protected String typedValue(String value) {
return super.typedValue(value).toLowerCase();
}
}
After this, the only thing left is to make Restrictions use these new classes:
public static Criterion like(String propertyName, Object value) {
return new LikeExpression(propertyName, value);
}
public static Criterion like(String propertyName, String value, MatchMode matchMode) {
return new LikeExpression(propertyName, value, matchMode);
}
public static Criterion like(String propertyName, String value, Character escapeChar) {
return new LikeExpression(propertyName, value, escapeChar);
}
public static Criterion ilike(String propertyName, Object value) {
return new IlikeExpression(propertyName, value);
}
public static Criterion ilike(String propertyName, String value, MatchMode matchMode) {
return new IlikeExpression(propertyName, value, matchMode);
}
public static Criterion ilike(String propertyName, String value, Character escapeChar) {
return new IlikeExpression(propertyName, value, escapeChar);
}
Edit: Oh yeah. This works for Oracle. We're not sure about other databases though.
It's not a very clean way to do it but a sqlRestrinction should be easier:
criterions.add(Restrictions.sqlRestriction(columnName+ " ilike '!%' escape '!'"));
You can even do a start with search using the same principle:
criterions.add(Restrictions.sqlRestriction(columnName+ " ilike '!%%' escape '!'"));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With