Usando critérios de hibernação, há uma maneira de escapar caracteres especiais?
Pergunta
Para esta pergunta, queremos evitar ter que escrever uma consulta especial, pois a consulta teria de ser diferente em vários bancos de dados. Usando apenas hibernate critérios, nós queremos ser capazes de escapar caracteres especiais.
Esta situação é a razão para a necessidade da capacidade de escapar caracteres especiais:
Suponha que tenhamos tabela 'foo' no banco de dados. Tabela 'foo' contém apenas 1 campo, chamado de 'nome'. O campo 'nome' pode conter caracteres que podem ser considerados especial em um banco de dados. Dois exemplos de tal nome um são 'nome_1' e 'nome% 1'. Tanto o '_' e '%' são caracteres especiais, pelo menos em Oracle. Se um usuário quiser procurar um desses exemplos depois que eles são inseridos no banco de dados, podem ocorrer problemas.
criterion = Restrictions.ilike("name", searchValue, MatchMode.ANYWHERE);
return findByCriteria(null, criterion);
Neste código, 'searchValue' é o valor que o utilizador tenha dado o aplicativo para usar para a sua pesquisa. Se o usuário quiser procurar '%', o usuário vai ser devolvido com cada entrada 'foo' no banco de dados. Isso ocorre porque o caractere '%' representa a "qualquer número de caracteres" wildcard para correspondência de corda e o código SQL que o Hibernate produz será algo como:
select * from foo where name like '%'
Existe uma maneira de dizer hibernate para escapar certos caracteres, ou para criar uma solução que não é o tipo de banco de dados específico?
Solução
construtores de LikeExpression são todos protegidos, por isso não é uma opção viável. Além disso, tem seus próprios problemas .
Um colega e eu criado um patch que funciona muito bem. A essência do patch é que, para o construtor LikeExpression que consome MatchMode, nós escapar os caracteres especiais. Para o construtor que consome um caractere (o caractere de escape), assumimos que o usuário escapa os caracteres especiais por conta própria.
Nós também parametrizado o caractere de escape para garantir que ele não pode corromper a consulta SQL se usarem algo como \ ou um carácter de citação.
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;
}
}
Se você está se perguntando o que os lhs e métodos typedValue são para, a nova IlikeExpression deve responder a essas perguntas.
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();
}
}
Depois disso, a única coisa que resta é fazer com que as restrições de uso destas novas 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: Ah, sim. Isso funciona para Oracle. Não temos certeza sobre outras bases de dados embora.
Outras dicas
Não é uma forma muito limpa para fazer isso, mas uma sqlRestrinction deve ser mais fácil:
criterions.add(Restrictions.sqlRestriction(columnName+ " ilike '!%' escape '!'"));
Você pode até fazer um começo com a pesquisa usando o mesmo princípio:
criterions.add(Restrictions.sqlRestriction(columnName+ " ilike '!%%' escape '!'"));
Se você usar LikeExpression diretamente, ele permite que você especifique o caractere de escape. Presumo que deve ser tudo que você precisa.
Se você usar Hibernate 3.2+, você pode subclasse LikeExpression
e crie like
métodos de fábrica / ilike
:
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.LikeExpression;
import org.hibernate.criterion.MatchMode;
public class EscapedLikeRestrictions {
private EscapedLikeRestrictions() {}
public static Criterion likeEscaped(String propertyName, String value, MatchMode matchMode) {
return likeEscaped(propertyName, value, matchMode, false);
}
public static Criterion ilikeEscaped(String propertyName, String value, MatchMode matchMode) {
return likeEscaped(propertyName, value, matchMode, true);
}
private static Criterion likeEscaped(String propertyName, String value, MatchMode matchMode, boolean ignoreCase) {
return new LikeExpression(propertyName, escape(value), matchMode, '!', ignoreCase) {/*a trick to call protected constructor*/};
}
private static String escape(String value) {
return value
.replace("!", "!!")
.replace("%", "!%")
.replace("_", "!_");
}
}
Se você usar sqlRectrictions, a maneira correta de fazê-lo é o seguinte:
criterions.add(Restrictions.sqlRestriction(columnName+" LIKE '!%' ESCAPE '!'"));
É como uma consulta SQL, não trabalho ilike => utilização como usar o Oracle 12i.