JPA2:不区分大小写,例如在任何地方匹配

Posted

技术标签:

【中文标题】JPA2:不区分大小写,例如在任何地方匹配【英文标题】:JPA2: Case-insensitive like matching anywhere 【发布时间】:2011-06-02 14:11:01 【问题描述】:

我一直在 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 相比,这是一种尴尬且不舒服的解决方案。

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

非常感谢。

【问题讨论】:

【参考方案1】:

要将approach of 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);
    

【讨论】:

【参考方案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();
            

        

    

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

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

【讨论】:

【参考方案3】:

这对我有用:

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

【讨论】:

您在代码中的哪个位置确保stringToFind 是大写的?【参考方案4】:

正如我在(当前)接受的答案中评论的那样,一方面使用 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

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

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

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

【讨论】:

您能否添加一个示例,其中接受的答案会失败但这种方法运行良好?谢谢=)【参考方案5】:

请考虑使用

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

用于匹配任何地方。

【讨论】:

可以加个例子详细说明一下?【参考方案6】:

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 不允许更改运算符名称,这太糟糕了。

【讨论】:

【参考方案7】:

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

    在 SQL 2003、2006、2008 标准下,可以通过在以下内容中添加 COLLATE SQL_Latin1_General_CP1_CI_ASCOLLATE 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

    在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 Linguistic Sorting and String Searching ORA 11g Setting Up a Globalization Support Environment

【讨论】:

您的链接再次指向这个问题。我认为递归不是你想要的;)【参考方案8】:

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

【讨论】:

我也在从 Hibernate 迁移到 JPA,我发现 JPA 的 API 有点……有时不清楚。似乎有很多不同的方法可以完成同样的事情——有些方法比其他方法更冗长。我相信这将是形成类似语句的一种“更友好”的方式:builder.like(builder.lower(root.get("username")), "%"+keyword.toLowerCase()+"%" ) 我认为最好使用JPA Static Metamodel Generator。它将确保属性在编译时存在并且知道类型;-) 注意,将 SGBD 降低的字符串与 java 降低的字符串进行比较可能会导致重音字符的差异。示例:Java : "EÉÊÈ".toLower() = "eÉÊÈ" 而 postgresql lower('EÉÊÈ') = 'eéêè'。我正在寻找一种方法将 de 'lower' 全部献给 SGBD @Ghurdyl 我刚刚使用这段代码测试了Java的toLowerCase,它成功了。 assertThat("EÉÊÈ".toLowerCase(Locale.ROOT), is("eéêè"));,并且至少在 1.8.0_144 版本中有效。 @RolandIllig 确实,我写错了,Java toLower 正确处理重音字符,PostgreSQL(使用 'c' 语言环境没有)选择 lower('EÉÊÈ') -> eÉÊÈ。我的观点仍然有效,将 java toLower() 与 DB lower() 进行比较可能会导致意外行为。 (见我的回答)

以上是关于JPA2:不区分大小写,例如在任何地方匹配的主要内容,如果未能解决你的问题,请参考以下文章

正则匹配修饰符

正则匹配修饰符

Java - MongoDB不区分大小写不检查精确匹配

如何使 yarp 匹配路径不区分大小写?

Javascript匹配-不区分大小写[重复]

怎么设置正则表达式不区分大小写