60

我一直在 JPA 1.0(休眠驱动程序)中使用休眠限制。定义Restrictions.ilike("column","keyword", MatchMode.ANYWHERE)了哪些测试关键字是否与任何地方的列匹配并且不区分大小写。

现在,我使用 JPA 2.0 和 EclipseLink 作为驱动程序,所以我必须使用“限制”内置 JPA 2.0。我找到了CriteriaBuilder方法like,我也找到了如何让它在任何地方匹配(虽然它很可怕而且手动),但我仍然没有弄清楚如何做到不区分大小写。

我目前有一个很棒的解决方案:

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();

问题:

有没有像 Hibernate 驱动程序中的任何功能?

我是否正确使用了 JPA 2.0 标准?与 Hibernate Restrictions 相比,这是一种尴尬且不舒服的解决方案。

或者任何人都可以帮助我如何将我的解决方案更改为不区分大小写,好吗?

非常感谢。

4

9 回答 9

108

一开始可能看起来有点尴尬,但它是类型安全的。不是从字符串构建查询,因此您会在运行时而不是在编译时注意到错误。您可以通过使用缩进或单独执行每个步骤来使查询更具可读性,而不是在一行中编写整个 WHERE 子句。

要使您的查询不区分大小写,请将关键字和比较字段都转换为小写:

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() + "%"
        )
    )
);
于 2011-01-04T08:02:34.490 回答
13

正如我在(当前)接受的答案中评论的那样,一方面使用 DBMS 的lower()函数,另一方面使用 java 的函数存在一个陷阱,String.toLowerCase()因为这两种方法都不能保证为相同的输入字符串提供相同的输出。

我终于找到了一个更安全(但不是防弹)的解决方案,即让 DBMS 使用文字表达式进行所有降低:

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

所以完整的解决方案看起来像:

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 + "%")
        )
    )
);

编辑:
正如@cavpollo 要求我举个例子,我不得不对我的解决方案三思而后行,并意识到它并不比公认的答案安全得多:

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

尽管如此,我还是更喜欢我的解决方案,因为它不会将结果与应该工作相同的两个不同功能进行比较。我将相同的函数应用于所有字符数组,以便比较输出变得更加“稳定”。

防弹解决方案将涉及语言环境,以便 SQLlower()能够正确降低重音字符。(但这超出了我的卑微知识)

*带有 'C' 语言环境的 PostgreSQL 9.5.1 的 Db 值

于 2017-11-24T16:58:36.500 回答
8

这对我有用:

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();
于 2014-05-07T13:22:03.517 回答
3

在数据库中强制执行不区分大小写比 JPA 更容易、更有效。

  1. 在 SQL 2003、2006、2008 标准下,可以通过向以下内容添加COLLATE SQL_Latin1_General_CP1_CI_ASORCOLLATE latin1_general_cs来做到这一点:

    • 列定义

      CREATE TABLE <table name> (
        <column name> <type name> [DEFAULT...] 
                                  [NOT NULL|UNIQUE|PRIMARY KEY|REFERENCES...]
                                  [COLLATE <collation name>], 
        ...
      )
      
    • 域定义

      CREATE DOMAIN <domain name> [ AS ] <data type>
        [ DEFAULT ... ] [ CHECK ... ] [ COLLATE <collation name> ]
      
    • 字符集定义

      CREATE CHARACTER SET <character set name>
      [ AS ] GET <character set name> [ COLLATE <collation name> ]
      

    有关上述完整说明,请参阅: http ://savage.net.au/SQL/sql-2003-2.bnf.html#column%20definition http://dev.mysql.com/doc/refman/5.1/en/ charset-table.html http://msdn.microsoft.com/en-us/library/ms184391.aspx

  2. 在 Oracle 中,可以设置 NLS Session/Configuration 参数

     SQL> ALTER SESSION SET NLS_COMP=LINGUISTIC;
     SQL> ALTER SESSION SET NLS_SORT=BINARY_CI;
     SQL> SELECT ename FROM emp1 WHERE ename LIKE 'McC%e';
    
     ENAME
     ----------------------
     McCoye
     Mccathye
    

    或者,在init.ora(或初始化参数文件的操作系统特定名称):

    NLS_COMP=LINGUISTIC
    NLS_SORT=BINARY_CI
    

    二进制排序可以不区分大小写或不区分重音。当您将 BINARY_CI 指定为 NLS_SORT 的值时,它指定了一种区分重音和不区分大小写的排序。BINARY_AI 指定不区分重音和不区分大小写的二进制排序。如果字符集的二进制排序顺序适合您正在使用的字符集,您可能希望使用二进制排序。使用 NLS_SORT 会话参数指定不区分大小写或不区分重音的排序:

    Append _CI to a sort name for a case-insensitive sort.
    Append _AI to a sort name for an accent-insensitive and case-insensitive sort. 
    

    例如,您可以将 NLS_SORT 设置为以下类型的值:

    FRENCH_M_AI
    XGERMAN_CI
    

    将 NLS_SORT 设置为 BINARY [可选 _CI 或 _AI] 以外的任何值都会导致排序使用全表扫描,而不管优化器选择的路径如何。BINARY 是一个例外,因为索引是根据键的二进制顺序构建的。因此,当 NLS_SORT 设置为 BINARY 时,优化器可以使用索引来满足 ORDER BY 子句。如果 NLS_SORT 设置为任何语言排序,优化器必须在执行计划中包含全表扫描和全排序。

    或者,如果 NLS_COMP 设置为 LINGUISTIC,如上所述,那么排序设置可以在本地应用到索引列,而不是在整个数据库中全局应用:

    CREATE INDEX emp_ci_index ON emp (NLSSORT(emp_name, 'NLS_SORT=BINARY_CI'));
    

    参考:ORA 11g 语言排序和字符串搜索 ORA 11g 设置全球化支持环境

于 2013-05-30T07:34:50.040 回答
2

OpenJPA 2.3.0 和 Postgresql 绝望的解决方法

public class OpenJPAPostgresqlDictionaryPatch extends PostgresDictionary {

  @Override
  public SQLBuffer toOperation(String op, SQLBuffer selects, SQLBuffer from, SQLBuffer where, SQLBuffer group, SQLBuffer having, SQLBuffer order, boolean distinct, long start, long end, String forUpdateClause, boolean subselect) {
    String whereSQL = where.getSQL();
    int p = whereSQL.indexOf("LIKE");
    int offset = 0;
    while (p != -1) {
      where.replaceSqlString(p + offset, p + offset + 4, "ILIKE");
      p = whereSQL.indexOf("LIKE", p + 1);
      offset++;
    }
    return super.toOperation(op, selects, from, where, group, having, order, distinct, start, end, forUpdateClause, subselect);
  }

}

对于使用 OpenJPA 和 Postgresql 数据库执行不区分大小写的 LIKE 操作,这是一个脆弱且丑陋的解决方法。它将生成的 SQL 中的 LIKE 运算符替换为 ILIKE 运算符。

OpenJPA DBDictionary 不允许更改运算符名称太糟糕了。

于 2014-05-28T14:09:34.623 回答
2

如果您使用的是像 Postgres 这样的数据库,它支持ilike提供更好的性能,因为使用该lower()功能提供的解决方案都不能正确解决问题。

解决方案可以是自定义函数。

您正在编写的 HQL 查询是:

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

其中caseInSensitiveMatching是我们自定义函数的函数名。name是要比较的属性的路径,是%test%要匹配的模式。

目标是将 HQL 查询转换为以下 SQL 查询:

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

为此,我们必须使用注册的自定义函数来实现我们自己的方言:

    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();
            }

        }

    }

上述优化在我们的情况下产生了 40 倍的性能提升,与具有lower功能的版本相比,Postgres 可以利用相应列上的索引。在我们的情况下,查询执行时间可以从 4.5 秒减少到 100 毫秒。

lower会阻止索引的有效使用,因此速度要慢得多。

于 2020-06-16T06:34:01.110 回答
1

要将Thomas Hunziker 的方法与 hibernate 的标准构建器一起使用,您可以提供特定的谓词实现,如下所示

public class ILikePredicate extends AbstractSimplePredicate implements Serializable {

    private final Expression<String> matchExpression;

    private final Expression<String> pattern;

    public ILikePredicate(
        CriteriaBuilderImpl criteriaBuilder,
        Expression<String> matchExpression,
        Expression<String> pattern) {
        super(criteriaBuilder);
        this.matchExpression = matchExpression;
        this.pattern = pattern;
    }

    public ILikePredicate(
        CriteriaBuilderImpl criteriaBuilder,
        Expression<String> matchExpression,
        String pattern) {
        this(criteriaBuilder, matchExpression, new LiteralExpression<>(criteriaBuilder, pattern));
    }

    public Expression<String> getMatchExpression() {
        return matchExpression;
    }

    public Expression<String> getPattern() {
        return pattern;
    }

    @Override
    public void registerParameters(ParameterRegistry registry) {
        Helper.possibleParameter(getMatchExpression(), registry);
        Helper.possibleParameter(getPattern(), registry);
    }

    @Override
    public String render(boolean isNegated, RenderingContext renderingContext) {
        String match = ((Renderable) getMatchExpression()).render(renderingContext);
        String pattern = ((Renderable) getPattern()).render(renderingContext);
        return String.format("function('caseInSensitiveMatching', %s, %s) = %s", match, pattern, !isNegated);
    }
}
于 2021-04-28T11:05:27.540 回答
0

作为weltraumpirat的回答,简而言之,对于您的Root上的每个所需字段,将以下谓词添加到您的谓词列表中

criteriaBuilder.like(criteriaBuilder.lower(root.get(<desired field on your root>)), "%" + text.toLowerCase(Locale.ROOT) + "%")

然后使用所需的 OR-AND 获得TypedQuery,如下所示

entityManager.createQuery(criteriaQuery.where(criteriaBuilder.and(predicateList.toArray(new Predicate[]{}))));
于 2022-03-01T13:14:37.993 回答
-2

请考虑使用

CriteriaBuilder.like(Expression<String> x, Expression<String> pattern, char escapeChar);

匹配任何地方。

于 2016-08-31T15:03:34.277 回答