【问题标题】:Hibernate Query with String parameters doesn't use apostrophes带有字符串参数的休眠查询不使用撇号
【发布时间】:2021-08-27 14:10:57
【问题描述】:

我们使用 SQL Server 数据库。 当尝试使用 27GB 内存对具有 100M 条记录的表运行查询时,Hibernate 会将查询(无论是 findById 还是 findByFieldName)转换为如下内容:

select sbtinvento0_.cust_nbr as cust_nbr1_10_0_, sbtinvento0_.ean_upc as ean_upc2_10_0_, sbtinvento0_.rep_bal_qty as rep_bal_3_10_0_ from odh_inv.sbt_inven sbtinvento0_ where sbtinvento0_.cust_nbr=? and sbtinvento0_.ean_upc=?

现在发生的是这个查询持续了大约 7 秒。数据库中的两个字段都是字符串类型。

我们能够在 DBeaver 中重现此行为:

SELECT CUST_NBR, EAN_UPC, REP_BAL_QTY
FROM ODH_INV.SBT_INVEN
WHERE CUST_NBR = 0000100974 AND EAN_UPC = '0042823091698';

此查询也将持续 7 秒。但是,如果按照以下方式进行查询,只需要 0.7 秒:

SELECT CUST_NBR, EAN_UPC, REP_BAL_QTY
FROM CommonSQLDevPrj.ODH_INV.SBT_INVEN
WHERE CUST_NBR = '0000100974' AND EAN_UPC = '0042823091698';

这种行为在多个应用程序/环境/数据库、具有数百万行的表上、在使用带有字符串参数的 findByIdfindByFieldName 时由 Hibernate 自动创建的查询上重复。

我尝试在谷歌上搜索此问题,但找不到任何有用的信息。是否可以在 application.yml 文件中设置任何属性以强制在字符串参数上使用撇号?

我尝试了以下方法:

@Query("SELECT si FROM SbtInventory si WHERE sbtInventoryId.customerNumber = ':customerNumber' and sbtInventoryId.eanUpc = ':eanUpc'")
Optional<SbtInventory> findByCustomerNumberAndEanUpc(String customerNumber, String eanUpc);

但是查询按原样运行,它不会用函数参数中的值替换查询参数。我尝试使用 ?1 和 ?2,或者转义撇号,但它们都不起作用。

我还尝试了一些使用实体管理器和会话的解决方法。

使用 Entity Manager 的第一个解决方法确实可以在 1 秒内完成。问题是我必须在查询中手动添加参数。在这种情况下,这不是问题,因为这是一项工作,我们保证这里不会有任何 SQL 注入,但由于这个原因,不建议使用此方法,我们将无法使用它在 API 上。

    private final EntityManager em;

    public Optional<SbtInventory> get(String customerNumber, String eanUpc) {
        @SuppressWarnings("unchecked")
        List<SbtInventory> sbtInventory = em
                .createQuery("SELECT si FROM SbtInventory si WHERE sbtInventoryId.customerNumber = '" + customerNumber
                        + "' and sbtInventoryId.eanUpc = '" + eanUpc + "'")
                .getResultList();
        
        if (sbtInventory.size() == 0) {
            return Optional.empty();
        } else {
            return Optional.of(sbtInventory.get(0));
        }
    }

推荐的第二种方法,使用 Session,也可以,但查询再次需要 7 秒才能运行:

    private final EntityManager em;

    @Transactional
    public Optional<SbtInventory> get(String customerNumber, String eanUpc) {
        Session session = null;
        if (em == null || (session = em.unwrap(Session.class)) == null) {
            throw new NullPointerException();
        }
        @SuppressWarnings("unchecked")
        List<SbtInventory> sbtInventory = session.createQuery(
                "SELECT si FROM SbtInventory si WHERE sbtInventoryId.customerNumber = :customerNumber and sbtInventoryId.eanUpc = :eanUpc")
                .setParameter("customerNumber", customerNumber, StandardBasicTypes.STRING)
                .setParameter("eanUpc", eanUpc, StandardBasicTypes.STRING).getResultList();
        if (sbtInventory.size() == 0) {
            return Optional.empty();
        } else {
            return Optional.of(sbtInventory.get(0));
        }
    }

但所有这些都是不受欢迎的,因为它为我们的查询添加了许多额外的代码和额外的处理,所以我仍然希望我们可以设置一些休眠属性,以便它自己添加撇号。

编辑: 我尝试了 Jens 的演员建议:

SELECT CUST_NBR, EAN_UPC, REP_BAL_QTY
FROM ODH_INV.SBT_INVEN
WHERE CUST_NBR = CAST(0000100974, varchar) AND EAN_UPC = '0042823091698';

此查询运行速度很快,但没有返回任何结果。我认为这可能是因为删除了前导零,所以我尝试了:

SELECT CUST_NBR, EAN_UPC, REP_BAL_QTY
FROM ODH_INV.SBT_INVEN
WHERE CUST_NBR = RIGHT(CONCAT('0000000000', CAST(0000100974 AS varchar)), 10) AND EAN_UPC = '0042823091698';

这终于可以工作了,但它意味着手动编写查询,这让我可以在存储库中使用 @Query 而不是使用实体管理器,这是一种升级(如果它实际上可以在周一工作,则无法测试它) Hibernate ),但仍然意味着像这样编写每个敏感查询,而不是简单地使用由函数名形成的查询:(

编辑 2: 我将在这里回答一些小工具的问题

您的 SbtInventory 实体中 customerNumber 的休眠映射是否有任何自定义?能否请您发布映射?

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Entity
@Builder
@Table(name = "SBT_INVEN", schema = "ODH_INV")
public class SbtInventory {

    @EmbeddedId
    private SbtInventoryIdentity sbtInventoryId;

    @Column(name = "REP_BAL_QTY")
    private Integer reportedBalanceQuantity;

}

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
@EqualsAndHashCode(callSuper = false)
@Embeddable
public class SbtInventoryIdentity implements Serializable {

    /**
     * 
     */
    private static final long serialVersionUID = -892835625356278964L;

    @Column(name = "CUST_NBR")
    private String customerNumber;

    @Column(name = "EAN_UPC")
    private String eanUpc;
}

还有你用什么方言来休眠?

数据库连接的完整配置属性:

  datasource:
    url: jdbc:sqlserver://db-url:db-port;databaseName=db-name
    username: user
    password: pass
    driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
    hikari:
      data-source-properties:
        cachePrepStmts: true
        prepStmtCacheSize: 250
        prepStmtCacheSqlLimit: 2048
        useServerPrepStmts: false
        maximum-pool-size: 50
  jpa:
    database-platform: org.hibernate.dialect.SQLServer2008Dialect
    database: SQL_SERVER
    show-sql: true
    hibernate:
      ddl-auto: none
    properties:
      hibernate.dialect: org.hibernate.dialect.SQLServer2008Dialect
      hibernate.jdbc.batch_size: 20
      hibernate.cache.use_second_level_cache: false
      hibernate.enable_lazy_load_no_trans: true

在 db 中使用 char 而不是 varchar 的任何特殊原因?

这是一个我没有确切答案的问题。这就是数据库的设计方式。客户编号是另一个表的外键,其中主键被创建为 Char。这些是一些旧表,为了保持该标准,它们继续使用 char 作为遗留字段。

编辑 3:

    @Query(value = "SELECT REP_BAL_QTY\r\n" + "FROM ODH_INV.SBT_INVEN\r\n"
            + "WHERE CUST_NBR = RIGHT(CONCAT('0000000000', CAST(:customerNumber AS varchar)), 10) "
            + "AND EAN_UPC = RIGHT(CONCAT('0000000000000', CAST(:eanUpc AS varchar)), 13)", nativeQuery = true)
    Optional<Integer> findReportedBalanceQuantityByCustomerNumberAndEanUpc(String customerNumber, String eanUpc);

所以这在存储库中工作正常,但在这种情况下,它是我要公开的 API,我只需要结果中的 REP_BAL_QTY 列,所以我必须为页面的每一行运行此查询我'我回来了。在我将其映射到实体之前:

    @MapsId
    @OneToOne
    @JoinColumns({
        @JoinColumn(name = "EAN_UPC", referencedColumnName = "EAN_UPC"),
        @JoinColumn(name = "CUST_NBR", referencedColumnName = "CUST_NBR")
    })
    @NotFound(action = NotFoundAction.IGNORE)
    private SbtInventory sbtInventory;

我认为这是在加载实体时调用的,并且过程有点快(除了显而易见的,避免样板代码) 使用实体中的join,带10行页面的过程持续:
SbtInventoryController.getData 耗时 84113 毫秒
这是连接的执行查询之一:

select sbtinvento0_.CUST_NBR as CUST_NBR1_19_0_, sbtinvento0_.EAN_UPC as EAN_UPC2_19_0_, sbtinvento0_.REP_BAL_QTY as REP_BAL_3_19_0_ from ODH_INV.SBT_INVEN sbtinvento0_ where sbtinvento0_.CUST_NBR=? and sbtinvento0_.EAN_UPC=?

使用查询,过程持续:
SbtInventoryController.getData 耗时 14063 毫秒
所以这是一个巨大的性能飞跃。缺点是通过使用查询,我必须放弃对包含 REP_BAL_QTY 的列进行排序:(

【问题讨论】:

  • SbtInventory 实体中 customerNumber 的 java 类型是什么?
  • CUST_NBR 的类型是什么?如果是VARCHAR 或类似的,那么您在开头提到的两个语句是非常不同的。您可以尝试强制转换绑定参数w3schools.com/sql/func_sqlserver_cast.asp
  • @gadget 对于 customerNumber 和 eanUpc 的类型都是 String
  • @JensSchauder CUST_NBR 是 char(10) 而 EAN_UPC 是 char(13)
  • 用@JensSchauder 的建议更新了问题

标签: java sql-server spring hibernate spring-data-jpa


【解决方案1】:

一个月后,我们终于确定了导致问​​题的原因。

在与我们的 DBA 交谈后,我们获得了查询的评估计划,我们注意到其中一些在谓词中调用了 CONVERT_IMPLICIT 函数:

CONVERT_IMPLICIT(nchar(18),[dbo].[PRICING].[MATL_NBR] as [pricing0_].[MATL_NBR],0)=[@P1]

在此查询中,CONVERT_IMPLICIT 调用导致它在 150 毫秒内运行,并且没有利用应用于表的 INDEX。

搜索这个 CONVERT_IMPLICIT 函数,我发现这篇文章提到了我扣除的内容,查询将使用更多资源并且不会选择正确的索引:

https://blog.sqlauthority.com/2018/06/11/sql-server-how-to-fix-convert_implicit-warnings/

在搜索如何禁用此行为时,我发现了 Vlad Mihalcea 几个月前撰写的以下文章,我想将解决方案归功于该文章:

https://vladmihalcea.com/sql-server-jdbc-sendstringparametersasunicode/

本文指定在 JDBC 驱动程序上设置默认属性 sendStringParametersAsUnicode,应始终禁用该属性,因为它会极大地影响查询的性能。这篇文章还提到微软提出了同样的建议:

https://docs.microsoft.com/en-us/sql/connect/jdbc/improving-performance-and-reliability-with-the-jdbc-driver?view=sql-server-ver15

When working with CHAR, VARCHAR, and LONGVARCHAR data, users can set the connection property sendStringParametersAsUnicode to false for optimal performance gain.

因此,解决方案非常简单。只需附加 sendStringParametersAsUnicode=false;到您的数据库连接字符串。您应该始终将此属性设置为 false 以防止查询出现性能问题。

dbc:sqlserver://localhost;instance=SQLEXPRESS;databaseName=high_performance_sql;sendStringParametersAsUnicode=false;

【讨论】:

    猜你喜欢
    • 2011-04-23
    • 2014-12-29
    • 1970-01-01
    • 2010-10-01
    • 1970-01-01
    • 2012-11-23
    • 2019-03-13
    • 1970-01-01
    • 2022-11-15
    相关资源
    最近更新 更多