Автор оригинала: Vlad Mihalcea.
Вступление
В этой статье вы узнаете, как автоматически обнаруживать проблему с запросом N+1 при использовании JPA и переходить в спящий режим с помощью db-util
проект с открытым исходным кодом .
С помощью Hibernate вы управляете переходами состояний сущностей, которые затем преобразуются в инструкции SQL. На количество сгенерированных инструкций SQL влияет текущая стратегия выборки, запросы критериев или сопоставления коллекций, и вы не всегда можете получить то, что ожидали. Игнорирование инструкций SQL является рискованным, и это может в конечном итоге сильно повлиять на общую производительность приложения.
Я убежденный сторонник экспертной оценки, но это не “обязательное условие” для обнаружения неправильного использования спящего режима. Незначительные изменения могут повлиять на количество операторов SQL и пройти незамеченными в процессе проверки. Ни в коей мере, когда дело доходит до “угадывания” операторов JPA SQL, я чувствую, что могу использовать любую дополнительную помощь. Я за максимально возможную автоматизацию, и именно поэтому я придумал механизм для обеспечения соблюдения ожиданий количества операторов SQL.
Во-первых, нам нужен способ перехвата всех выполняемых операторов SQL. Я исследовал эту тему, и мне повезло найти эту замечательную источник данных-прокси библиотеку.
Добавление автоматического валидатора
Эта защита предназначена для запуска только на этапе тестирования, поэтому я добавлю ее исключительно в контекст весеннего тестирования интеграции. Я уже говорил о сглаживании Spring bean, и сейчас самое подходящее время его использовать.
@Bean public DataSource dataSource(DataSource originalDataSource) { ChainListener listener = new ChainListener(); SLF4JQueryLoggingListener loggingListener = new SLF4JQueryLoggingListener(); loggingListener.setQueryLogEntryCreator(new InlineQueryLogEntryCreator()); listener.addListener(loggingListener); listener.addListener(new DataSourceQueryCountListener()); return ProxyDataSourceBuilder .create(originalDataSource) .name("DS-Proxy") .listener(listener) .build(); }
Новый прокси-источник данных украшает уже существующий источник данных, перехватывая все выполняемые инструкции SQL. Эта библиотека может регистрировать все инструкции SQL вместе с фактическими значениями параметров, в отличие от журнала гибернации по умолчанию, который вместо этого печатает только заполнитель.
Вот как выглядит валидатор:
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:
com.vladmihalcea db-util ${db-util.version}
Давайте напишем тест для обнаружения печально известной проблемы N+1 с выбором запроса .
Для этого мы напишем два метода обслуживания, на один из которых повлияет вышеупомянутая проблема:
@Override @Transactional public ListfindAllWithNPlusOne() { List warehouseProductInfos = entityManager .createQuery( "from WarehouseProductInfo", WarehouseProductInfo.class) .getResultList(); navigateWarehouseProductInfos(warehouseProductInfos); return warehouseProductInfos; } @Override @Transactional public List findAllWithFetch() { List 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 warehouseProductInfos) { for(WarehouseProductInfo warehouseProductInfo : warehouseProductInfos) { warehouseProductInfo.getProduct(); } }
Модульный тест довольно прост в написании, так как он следует тому же стилю кодирования, что и любой другой механизм утверждения JUnit.
try { SQLStatementCountValidator.reset(); warehouseProductInfoService.findAllWithNPlusOne(); assertSelectCount(1); } catch (SQLSelectCountMismatchException e) { assertEquals(3, e.getRecorded()); } SQLStatementCountValidator.reset(); warehouseProductInfoService.findAllWithFetch(); assertSelectCount(1);
Наш валидатор работает для всех типов операторов SQL, поэтому давайте проверим, сколько вставок SQL выполняется следующим методом службы:
@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; }
И валидатор выглядит так:
SQLStatementCountValidator.reset(); warehouseProductInfoService.newWarehouseProductInfo(); assertSelectCount(1); assertInsertCount(2);
Давайте проверим журналы тестов, чтобы убедиться в их эффективности:
-- newWarehouseProductInfo SELECT c.id as id1_6_, c.name as name2_6_ FROM Company c INSERT INTO WarehouseProductInfo (id, quantity) VALUES (default, 19) INSERT INTO Product (id, code, company_id, importer_id, name, version) VALUES (default, 'phoneCode', 1, -5, 'Phone', 0)
Вывод
Проверка кода – это прекрасный метод, но его недостаточно для крупномасштабного проекта разработки. Вот почему автоматическая проверка имеет первостепенное значение. Как только тест был написан, вы уверены, что никакие будущие изменения не смогут разрушить ваши предположения.
Код доступен на GitHub .