Criteria API очень полезен для динамического построения запросов, но это единственный вариант использования, в котором я бы его использовал. Всякий раз, когда у вас есть пользовательский интерфейс с N фильтрами, которые могут появиться в любых M комбинациях, имеет смысл иметь API для динамического построения запросов, поскольку объединение строк — это всегда путь, от которого я убегаю.
Вопрос в том, знаете ли вы о SQL-запросах, которые ваш Criteria API генерирует за кулисами? В последнее время я проверял много таких запросов, и меня поразило, как легко это сделать неправильно.
Давайте начнем со следующей диаграммы сущностей:
Таким образом, у нас есть Продукт с ассоциацией ToOne для WareHouseProductInfo и ассоциацией ToMany с сущностью Image.
Теперь давайте начнем с этого запроса Criteria API:
1
2
3
4
5
6
7
8
9
|
CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<Product> query = cb.createQuery(Product. class ); Root<Product> productRoot = query.from(Product. class ); query.select(productRoot) .where(cb.and(cb.equal(productRoot.get(Product_.code), "tvCode" ), cb.gt(productRoot.get(Product_.warehouseProductInfo) .get(WarehouseProductInfo_.quantity), 50 ))); Product product = entityManager.createQuery(query).getSingleResult(); |
Можете ли вы обнаружить какие-либо проблемы с этим предыдущим запросом? Давайте проверим сгенерированный SQL:
01
02
03
04
05
06
07
08
09
10
11
|
SELECT product0_.id AS id1_14_, product0_.code AS code2_14_, product0_.company_id AS company_5_14_, product0_.importer_id AS importer6_14_, product0_.name AS name3_14_, product0_.version AS version4_14_ FROM product product0_ CROSS JOIN warehouseproductinfo warehousep1_ WHERE product0_.id = warehousep1_.id AND product0_.code = ? AND warehousep1_.quantity > 50 |
Я ожидал ВНУТРЕННЕГО СОЕДИНЕНИЯ, и вместо этого я получил КРОСС-СОЕДИНЕНИЕ. Декартово произведение очень неэффективно, и это то, что вы получите, если не забудете правильно присоединиться к ассоциациям, с которыми вы заинтересованы в фильтрации по предложениям where. Итак, написание Criteria API — это не прогулка по парку.
К счастью, этот пример можно исправить следующим образом:
1
2
3
4
5
6
7
8
9
|
CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<Product> query = cb.createQuery(Product. class ); Root<Product> productRoot = query.from(Product. class ); Join<Product, WarehouseProductInfo> warehouseProductInfoJoin = productRoot.join(Product_.warehouseProductInfo); query.select(productRoot) .where(cb.and(cb.equal(productRoot.get(Product_.code), "tvCode" ), cb.gt(warehouseProductInfoJoin.get(WarehouseProductInfo_.quantity), 50 ))); Product product = entityManager.createQuery(query).getSingleResult(); |
который дает ожидаемый запрос SQL:
01
02
03
04
05
06
07
08
09
10
11
|
SELECT product0_.id AS id1_14_, product0_.code AS code2_14_, product0_.company_id AS company_5_14_, product0_.importer_id AS importer6_14_, product0_.name AS name3_14_, product0_.version AS version4_14_ FROM product product0_ INNER JOIN warehouseproductinfo warehousep1_ ON product0_.id = warehousep1_.id WHERE product0_.code = ? AND warehousep1_.quantity > 50 |
Так что будьте осторожны с тем, как вы определяете свои объединения в Criteria API. Теперь давайте сравним предыдущий запрос Criteria API с его аналогом JPAQL:
01
02
03
04
05
06
07
08
09
10
|
Product product = entityManager.createQuery( "select p " + "from Product p " + "inner join p.warehouseProductInfo w " + "where " + " p.code = :code and " + " w.quantity > :quantity " , Product. class ) .setParameter( "code" , "tvCode" ) .setParameter( "quantity" , 50 ) .getSingleResult(); |
Я всегда считал JPAQL более описательным, чем Criteria API, но есть проекты, в которых Criteria API является механизмом запросов JPA по умолчанию, поэтому он используется не только для запросов динамических фильтров, но даже для тех, у которых фиксированные предложения where.
Что ж, в конечном итоге вы можете достичь тех же результатов, но, хотя я могу предсказать SQL-запрос из JPAQL-запроса, но когда дело доходит до Criteria API, я совершенно не в курсе. Всякий раз, когда я рассматриваю запрос Criteria, мне всегда нужно запускать интеграционный тест, чтобы проверить выводимый SQL, поскольку небольшие изменения могут действительно иметь большие различия.
Даже если использование Criteria API навязано, вы все равно можете обойти его, считая, что вы очень осторожны и просматриваете все свои запросы.
Теперь давайте вернемся к одному из самых экзотических (но все же неоптимальных) критериев присоединения, который я недавно наткнулся на. Если вы работаете над большим проектом со многими разработчиками, вы неизбежно столкнетесь с этим типом конструкций. Это еще одна причина, по которой я предпочитаю JPAQL над Criteria API. С JPAQL вы не можете получить его так, как в следующем примере:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<Product> query = cb.createQuery(Product. class ); Root<Product> product = query.from(Product. class ); query.select(product); query.distinct( true ); List<Predicate> criteria = new ArrayList<Predicate>(); criteria.add(cb.like(cb.lower(product.get(Product_.name)), "%tv%" )); Subquery<Long> subQuery = query.subquery(Long. class ); Root<Image> infoRoot = subQuery.from(Image. class ); Join<Image, Product> productJoin = infoRoot.join( "product" ); subQuery.select(productJoin.<Long>get(Product_.id)); subQuery.where(cb.gt(infoRoot.get(Image_.index), 0 )); criteria.add(cb.in(product.get(Product_.id)).value(subQuery)); query.where(cb.and(criteria.toArray( new Predicate[criteria.size()]))); return entityManager.createQuery(query).getResultList(); |
Я нахожу эти типы запросов слишком сложными для анализа, только просматривая их, но есть подвыбор, пахнущий как проблема, поэтому давайте посмотрим на сгенерированный запрос SQL:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
SELECT DISTINCT product0_.id AS id1_14_, product0_.code AS code2_14_, product0_.company_id AS company_5_14_, product0_.importer_id AS importer6_14_, product0_.name AS name3_14_, product0_.version AS version4_14_ FROM product product0_ WHERE ( Lower(product0_.name) LIKE ? ) AND ( product0_.id IN (SELECT product2_.id FROM image image1_ INNER JOIN product product2_ ON image1_.product_id = product2_.id WHERE image1_.index > 0 ) ) |
Хотя некоторые сценарии использования требуют подзапроса SQL, здесь он просто совершенно не нужен и только замедляет ваш запрос. Но на этот раз нам фактически требовался запрос динамической фильтрации, поэтому JPAQL не обсуждался. Единственный способ исправить это — написать правильный запрос Criteria.
Так вот, после рефакторинга:
01
02
03
04
05
06
07
08
09
10
11
|
CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<Product> query = cb.createQuery(Product. class ); Root<Image> imageRoot = query.from(Image. class ); Join<Image, Product> productJoin = imageRoot.join( "product" ); query.select(productJoin); query.distinct( true ); List<Predicate> criteria = new ArrayList<Predicate>(); criteria.add(cb.like(cb.lower(productJoin.get(Product_.name)), "%tv%" )); criteria.add(cb.gt(imageRoot.get(Image_.index), 0 )); query.where(cb.and(criteria.toArray( new Predicate[criteria.size()]))); return entityManager.createQuery(query).getResultList(); |
И теперь наш SQL-запрос выглядит намного лучше:
01
02
03
04
05
06
07
08
09
10
11
|
SELECT DISTINCT product1_.id AS id1_14_, product1_.code AS code2_14_, product1_.company_id AS company_5_14_, product1_.importer_id AS importer6_14_, product1_.name AS name3_14_, product1_.version AS version4_14_ FROM image image0_ INNER JOIN product product1_ ON image0_.product_id = product1_.id WHERE ( Lower(product1_.name) LIKE ? ) AND image0_.index > 0 |
Я рассуждал о том, почему разработчик выбрал подзапрос в этом конкретном контексте, и я полагал, что это потому, что он не знал, что может спроектировать объект, отличный от корневого, аналогично JPAQL. запрос.
Теперь давайте сделаем прогноз DTO, поскольку бывают случаи, когда нам не нужно извлекать целые сущности, а достаточно информации для удовлетворения потребностей нашего бизнеса. На этот раз мы создадим следующий запрос:
01
02
03
04
05
06
07
08
09
10
11
12
|
CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<ImageProductDTO> query = cb.createQuery(ImageProductDTO. class ); Root<Image> imageRoot = query.from(Image. class ); Join<Image, Product> productJoin = imageRoot.join(Image_.product); query.distinct( true ); List<Predicate> criteria = new ArrayList<Predicate>(); criteria.add(cb.like(cb.lower(productJoin.get(Product_.name)), "%tv%" )); criteria.add(cb.gt(imageRoot.get(Image_.index), 0 )); query.where(cb.and(criteria.toArray( new Predicate[criteria.size()]))); query.select(cb.construct(ImageProductDTO. class , imageRoot.get(Image_.name), productJoin.get(Product_.name))) .orderBy(cb.asc(imageRoot.get(Image_.name))); return entityManager.createQuery(query).getResultList(); |
Генерация чистого SQL:
1
2
3
4
5
6
7
8
|
SELECT DISTINCT image0_.name AS col_0_0_, product1_.name AS col_1_0_ FROM image image0_ INNER JOIN product product1_ ON image0_.product_id = product1_.id WHERE ( Lower(product1_.name) LIKE ? ) AND image0_.index > 0 ORDER BY image0_.name ASC |
Но посмотрите предыдущий Criteria Query, чтобы узнать, как JOOQ создает такой запрос:
1
2
3
4
5
6
7
8
|
jooqContext .select(IMAGE.NAME, PRODUCT.NAME) .from(IMAGE) .join(PRODUCT).on(IMAGE.PRODUCT_ID.equal(PRODUCT.ID)) .where(PRODUCT.NAME.likeIgnoreCase( "%tv%" )) .and(IMAGE.INDEX.greaterThan( 0 )) .orderBy(IMAGE.NAME.asc()) .fetch().into(ImageProductDTO. class ); |
Это намного удобнее для чтения, вам не нужно угадывать, каков выходной SQL-запрос, и он даже генерирует параметры привязки, которые я считаю чрезвычайно ценными:
1
2
3
4
5
6
7
8
|
SELECT "PUBLIC" . "image" . "name" , "PUBLIC" . "product" . "name" FROM "PUBLIC" . "image" JOIN "PUBLIC" . "product" ON "PUBLIC" . "image" . "product_id" = "PUBLIC" . "product" . "id" WHERE ( Lower( "PUBLIC" . "product" . "name" ) LIKE Lower( '%tv%' ) AND "PUBLIC" . "image" . "index" > 0 ) ORDER BY "PUBLIC" . "image" . "name" ASC |
Вывод
Первый случай, который я вам показал, — одна из самых первых ошибок, которые я совершил, пытаясь изучить Criteria API. Я обнаружил, что при написании таких запросов я должен быть особенно осторожен, поскольку вы можете легко получить неожиданные запросы SQL.
Если вы решили использовать Criteria API для всех своих запросов, то вам может быть интересно проверить JOOQ . Даже если вы выберете JPAQL, всякий раз, когда вы захотите создавать расширенные динамически фильтрованные запросы, JOOQ может помочь вам в этом.
Вы по-прежнему будете использовать свободный API, вы не будете писать какие-либо строки и получите больше возможностей SQL, чем то, что в настоящее время предлагает Hibernate. Поэтому, когда ваши варианты использования не требуют запросов к управляемым объектам, вы можете использовать вместо них JOOQ. Мне это нравится, потому что я могу предсказать сгенерированный SQL намного лучше, чем с Criteria API, и когда API легче использовать, меньше «сюрпризов» ждут, чтобы «удивить» вас.
Код доступен на GitHub .