Статьи

Факты гибернации: как «утверждать» количество операторов SQL

Вступление

Hibernate упрощает операции CRUD, особенно при работе с графами сущностей. Но любая абстракция имеет свою цену, и Hibernate ничем не отличается. Я уже говорил о важности выбора стратегии и знания ваших SQL-запросов Criteria , но есть и другие способы управления JPA. Этот пост посвящен контролю количества операторов SQL, которые Hibernate вызывает от вашего имени.

До того, как инструменты ORM стали настолько популярными, все взаимодействия с базой данных осуществлялись с помощью явных операторов SQL, а оптимизация была в основном направлена ​​на медленные запросы.

Hibernate может создать ложное впечатление, что вам не нужно беспокоиться о SQL-выражениях. Это неверное и опасное предположение. Предполагается, что Hibernate облегчает постоянство модели предметной области, а не освобождает вас от взаимодействия с SQL.

С Hibernate вы управляете переходами состояния объекта, которые затем переводятся в операторы SQL. Количество сгенерированных операторов SQL зависит от текущей стратегии выборки, запросов Criteria или отображений Collection, и вы не всегда можете получить то, что ожидали. Игнорирование операторов SQL сопряжено с риском и может в конечном итоге значительно снизить общую производительность приложения.

Я решительный сторонник рецензирования, но это не «обязательное условие» для обнаружения неправильного использования Hibernate. Незначительные изменения могут повлиять на количество операторов SQL и пройти незамеченными в процессе рецензирования. Не в последнюю очередь, когда дело доходит до «угадывания» операторов JPA SQL, я чувствую, что могу использовать любую дополнительную помощь. Я за максимально возможную автоматизацию, и именно поэтому я придумал механизм для обеспечения ожидания подсчета операторов SQL.

Во-первых, нам нужен способ перехвата всех выполненных операторов SQL. Я исследовал эту тему, и мне посчастливилось найти эту замечательную библиотеку прокси-источника данных .

Добавление автоматического валидатора

Эта защита предназначена для запуска только на этапе тестирования, поэтому я добавлю ее исключительно в контекст пружины Integration Testing. Я уже говорил о псевдонимах Spring bean, и сейчас самое время его использовать.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<bean id="testDataSource" class="bitronix.tm.resource.jdbc.PoolingDataSource" init-method="init"
      destroy-method="close">
    <property name="className" value="bitronix.tm.resource.jdbc.lrc.LrcXADataSource"/>
    <property name="uniqueName" value="testDataSource"/>
    <property name="minPoolSize" value="0"/>
    <property name="maxPoolSize" value="5"/>
    <property name="allowLocalTransactions" value="false" />
    <property name="driverProperties">
        <props>
            <prop key="user">${jdbc.username}</prop>
            <prop key="password">${jdbc.password}</prop>
            <prop key="url">${jdbc.url}</prop>
            <prop key="driverClassName">${jdbc.driverClassName}</prop>
        </props>
    </property>
</bean>
 
<bean id="proxyDataSource" class="net.ttddyy.dsproxy.support.ProxyDataSource">
    <property name="dataSource" ref="testDataSource"/>
    <property name="listener">
        <bean class="net.ttddyy.dsproxy.listener.ChainListener">
            <property name="listeners">
                <list>
                    <bean class="net.ttddyy.dsproxy.listener.CommonsQueryLoggingListener">
                        <property name="logLevel" value="INFO"/>
                    </bean>
                    <bean class="net.ttddyy.dsproxy.listener.DataSourceQueryCountListener"/>
                </list>
            </property>
        </bean>
    </property>
</bean>
 
<alias name="proxyDataSource" alias="dataSource"/>

Новый прокси-источник данных украшает уже существующий источник данных, перехватывая все выполняемые операторы SQL. Эта библиотека может регистрировать все операторы SQL вместе с фактическими значениями параметров, в отличие от ведения журнала Hibernate по умолчанию, который вместо этого печатает только заполнитель.

Вот как выглядит валидатор:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class SQLStatementCountValidator {
 
    private SQLStatementCountValidator() {
    }
 
    /**
     * Reset the statement recorder
     */
    public static void reset() {
        QueryCountHolder.clear();
    }
 
    /**
     * Assert select statement count
     * @param expectedSelectCount expected select statement count
     */
    public static void assertSelectCount(int expectedSelectCount) {
        QueryCount queryCount = QueryCountHolder.getGrandTotal();
        int recordedSelectCount = queryCount.getSelect();
        if(expectedSelectCount != recordedSelectCount) {
            throw new SQLSelectCountMismatchException(expectedSelectCount, recordedSelectCount);
        }
    }
 
    /**
     * Assert insert statement count
     * @param expectedInsertCount expected insert statement count
     */
    public static void assertInsertCount(int expectedInsertCount) {
        QueryCount queryCount = QueryCountHolder.getGrandTotal();
        int recordedInsertCount = queryCount.getInsert();
        if(expectedInsertCount != recordedInsertCount) {
            throw new SQLInsertCountMismatchException(expectedInsertCount, recordedInsertCount);
        }
    }
 
    /**
     * Assert update statement count
     * @param expectedUpdateCount expected update statement count
     */
    public static void assertUpdateCount(int expectedUpdateCount) {
        QueryCount queryCount = QueryCountHolder.getGrandTotal();
        int recordedUpdateCount = queryCount.getUpdate();
        if(expectedUpdateCount != recordedUpdateCount) {
            throw new SQLUpdateCountMismatchException(expectedUpdateCount, recordedUpdateCount);
        }
    }
 
    /**
     * Assert delete statement count
     * @param expectedDeleteCount expected delete statement count
     */
    public static void assertDeleteCount(int expectedDeleteCount) {
        QueryCount queryCount = QueryCountHolder.getGrandTotal();
        int recordedDeleteCount = queryCount.getDelete();
        if(expectedDeleteCount != recordedDeleteCount) {
            throw new SQLDeleteCountMismatchException(expectedDeleteCount, recordedDeleteCount);
        }
    }
}

Эта утилита является частью моего проекта db-util вместе с механизмом повторных попыток управления оптимистичным параллелизмом JPA и MongoDB .

Поскольку он уже доступен в Центральном репозитории Maven, вы можете легко использовать его, просто добавив эту зависимость в ваш pom.xml:

1
2
3
4
5
<dependency>
    <groupId>com.vladmihalcea</groupId>
    <artifactId>db-util</artifactId>
    <version>0.0.1</version>
</dependency>

Давайте напишем тест для обнаружения печально известной проблемы выбора N + 1 .

Для этого мы напишем два метода обслуживания, один из которых затронут вышеупомянутой проблемой:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Override
@Transactional
public List<WarehouseProductInfo> findAllWithNPlusOne() {
    List<WarehouseProductInfo> warehouseProductInfos = entityManager.createQuery(
            "from WarehouseProductInfo", WarehouseProductInfo.class).getResultList();
    navigateWarehouseProductInfos(warehouseProductInfos);
    return warehouseProductInfos;
}
 
@Override
@Transactional
public List<WarehouseProductInfo> findAllWithFetch() {
    List<WarehouseProductInfo> warehouseProductInfos = entityManager.createQuery(
            "from WarehouseProductInfo wpi " +
            "join fetch wpi.product p " +
            "join fetch p.company", WarehouseProductInfo.class).getResultList();
    navigateWarehouseProductInfos(warehouseProductInfos);
    return warehouseProductInfos;
}
 
private void navigateWarehouseProductInfos(List<WarehouseProductInfo> warehouseProductInfos) {
    for(WarehouseProductInfo warehouseProductInfo : warehouseProductInfos) {
        warehouseProductInfo.getProduct();
    }
}

Модульный тест довольно прост в написании, поскольку он следует тому же стилю кодирования, что и любой другой механизм утверждения JUnit.

01
02
03
04
05
06
07
08
09
10
11
try {
    SQLStatementCountValidator.reset();
    warehouseProductInfoService.findAllWithNPlusOne();
    assertSelectCount(1);
} catch (SQLSelectCountMismatchException e) {
    assertEquals(3, e.getRecorded());
}
 
SQLStatementCountValidator.reset();
warehouseProductInfoService.findAllWithFetch();
assertSelectCount(1);

Наш валидатор работает для всех типов операторов SQL, поэтому давайте проверим, сколько SQL INSERT выполняется следующим методом сервиса:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
@Override
@Transactional
public WarehouseProductInfo newWarehouseProductInfo() {
 
    LOGGER.info("newWarehouseProductInfo");
 
    Company company = entityManager.createQuery("from Company", Company.class).getResultList().get(0);
 
    Product product3 = new Product("phoneCode");
    product3.setName("Phone");
    product3.setCompany(company);
 
    WarehouseProductInfo warehouseProductInfo3 = new WarehouseProductInfo();
    warehouseProductInfo3.setQuantity(19);
    product3.addWarehouse(warehouseProductInfo3);
 
    entityManager.persist(product3);
    return warehouseProductInfo3;
}

И валидатор выглядит так:

1
2
3
4
SQLStatementCountValidator.reset();
warehouseProductInfoService.newWarehouseProductInfo();
assertSelectCount(1);
assertInsertCount(2);

Давайте проверим протоколы испытаний, чтобы убедиться в их эффективности:

1
2
3
4
5
6
7
INFO  [main]: o.v.s.i.WarehouseProductInfoServiceImpl - newWarehouseProductInfo
Hibernate: select company0_.id as id1_6_, company0_.name as name2_6_ from Company company0_
INFO  [main]: n.t.d.l.CommonsQueryLoggingListener - Name:, Time:1, Num:1, Query:{[select company0_.id as id1_6_, company0_.name as name2_6_ from Company company0_][]}
Hibernate: insert into WarehouseProductInfo (id, quantity) values (default, ?)
INFO  [main]: n.t.d.l.CommonsQueryLoggingListener - Name:, Time:0, Num:1, Query:{[insert into WarehouseProductInfo (id, quantity) values (default, ?)][19]}
Hibernate: insert into Product (id, code, company_id, importer_id, name, version) values (default, ?, ?, ?, ?, ?)
INFO  [main]: n.t.d.l.CommonsQueryLoggingListener - Name:, Time:0, Num:1, Query:{[insert into Product (id, code, company_id, importer_id, name, version) values (default, ?, ?, ?, ?, ?)][phoneCode,1,-5,Phone,0]}

Вывод

Проверка кода — это хорошая техника, но ее недостаточно для крупномасштабных проектов разработки. Вот почему автоматическая проверка имеет первостепенное значение. После того, как тест написан, вы уверены, что никакие будущие изменения не нарушат ваши предположения.

  • Код доступен на GitHub .