JOOQ — это аккуратный фреймворк, и он решает длинную проблему, с которой я столкнулся при использовании расширенных запросов с динамической фильтрацией. В то время как Hibernate и JPA поставляются с полезным Criteria API, который я использовал в течение достаточно долгого времени, существуют понятные ограничения на то, что вы можете делать с ними. Например, вы не можете выходить за рамки простых операций SQL (например, JOINS, NESTED SLECTS, AGGREGATION) и делать что-то вроде: оконных функций , пользовательских функций или простого упорядочения, чтобы назвать несколько.
JOOQ не хочет конкурировать с Hibernate, но вместо этого я чувствую, что это завершает его. Я использовал Hibernate для части WRITE моего уровня данных, отсюда его название или часть «Persisting» в JPA. Для простых и средних сложных запросов Hibernate делает все возможное, но я не должен полностью полагаться на него во всех моих запросах, не так ли? Есть также недостаток в запросе свойств, и это потому, что вам иногда нужно добавить ассоциацию к вашей доменной модели только для того, чтобы запросить ее для небольшого числа вариантов использования.
Так что, поскольку я не боюсь писать нативные запросы, я мог бы делать это в стиле DSL и независимо от поставщика.
В то время как вы можете использовать строковое именование столбцов, JOOQ предлагает лучший подход за счет использования безопасных с точки зрения типов метаданных, поэтому первое, что нам нужно сделать, — это сгенерировать отображение таблиц для нашей схемы базы данных.
Поскольку у меня уже есть модель JPA, я могу сгенерировать из нее DDL схемы базы данных, и для этого мы можем использовать задачу ant hibernatetool.
|
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
61
62
63
64
65
66
67
68
69
70
|
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-antrun-plugin</artifactId> <executions> <execution> <id>generate-test-sql-scripts</id> <phase>generate-test-resources</phase> <goals> <goal>run</goal> </goals> <configuration> <tasks> <property name="maven_test_classpath" refid="maven.test.classpath"/> <path id="hibernate_tools_path"> <pathelement path="${maven_test_classpath}"/> </path> <property name="hibernate_tools_classpath" refid="hibernate_tools_path"/> <taskdef name="hibernatetool" classname="org.hibernate.tool.ant.HibernateToolTask"/> <mkdir dir="${project.build.directory}/test-classes/hsqldb"/> <hibernatetool destdir="${project.build.directory}/test-classes/hsqldb"> <classpath refid="hibernate_tools_path"/> <jpaconfiguration persistenceunit="testPersistenceUnit" propertyfile="src/test/resources/META-INF/spring/jdbc.properties"/> <hbm2ddl drop="false" create="true" export="false" outputfilename="create_db.sql" delimiter=";" format="true"/> <hbm2ddl drop="true" create="false" export="false" outputfilename="drop_db.sql" delimiter=";" format="true"/> </hibernatetool> </tasks> </configuration> </execution> </executions> <dependencies> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>${hibernate.version}</version> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-tools</artifactId> <version>${hibernate.tools.version}</version> <exclusions> <exclusion> <groupId>org.hibernate</groupId> <artifactId>hibernate-commons-annotations</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>${slf4j.version}</version> </dependency> </dependencies></plugin> |
Это создаст сценарий DDL базы данных «create_db.sql», который мы будем использовать для заполнения временного файлового HSQLDB, используя «maven.sql.plugin». Я бы предпочел встроенный HSQLDB в памяти, но, к сожалению, он не сохранял состояние между выполнением плагинов.
|
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
|
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>sql-maven-plugin</artifactId> <dependencies> <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> <version>${hsqldb.version}</version> </dependency> </dependencies> <configuration> <driver>org.hsqldb.jdbc.JDBCDriver</driver> <url>jdbc:hsqldb:file:${project.build.directory}/hsqldb/db;shutdown=true</url> <username>sa</username> <password></password> <autocommit>true</autocommit> <settingsKey>hsql-db-test</settingsKey> </configuration> <executions> <execution> <id>create-test-compile-data</id> <phase>process-test-resources</phase> <inherited>true</inherited> <goals> <goal>execute</goal> </goals> <configuration> <orderFile>ascending</orderFile> <fileset> <basedir>${project.build.directory}/test-classes/hsqldb/</basedir> <includes> <include>create_db.sql</include> </includes> </fileset> <autocommit>true</autocommit> </configuration> </execution> </executions></plugin> |
Таким образом, HSQLDB теперь заполнен нашей сгенерированной схемой JPA, и мы наконец можем вызвать генерацию кода JOOQ для построения отображения таблицы.
|
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
|
<plugin> <groupId>org.jooq</groupId> <artifactId>jooq-codegen-maven</artifactId> <executions> <execution> <phase>process-test-classes</phase> <goals> <goal>generate</goal> </goals> </execution> </executions> <dependencies> <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> <version>${hsqldb.version}</version> </dependency> </dependencies> <configuration> <jdbc> <driver>org.hsqldb.jdbc.JDBCDriver</driver> <url>jdbc:hsqldb:file:${project.build.directory}/hsqldb/db</url> <user>sa</user> <password></password> </jdbc> <generator> <name>org.jooq.util.JavaGenerator</name> <database> <name>org.jooq.util.hsqldb.HSQLDBDatabase</name> <includes>.*</includes> <excludes></excludes> <inputSchema>PUBLIC</inputSchema> </database> <generate></generate> <target> <packageName>vladmihalcea.jooq.schema</packageName> <directory>target/generated-sources/jooq</directory> </target> </generator> </configuration></plugin> |
Проходя через maven, мы генерируем отображение таблицы, поэтому давайте сравним метамодель JPA для класса Image с сопоставлением отображения таблицы JOOQ:
Метамодель JPA выглядит так:
|
01
02
03
04
05
06
07
08
09
10
|
@StaticMetamodel(Image.class)public abstract class Image_ { public static volatile SingularAttribute<Image, Product> product; public static volatile SingularAttribute<Image, Long> id; public static volatile SetAttribute<Image, Version> versions; public static volatile SingularAttribute<Image, Integer> index; public static volatile SingularAttribute<Image, String> name;} |
и отображение таблицы JOOQ
|
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
|
comments = "This class is generated by jOOQ")@java.lang.SuppressWarnings({ "all", "unchecked", "rawtypes" })public class Image extends org.jooq.impl.TableImpl<vladmihalcea.jooq.schema.tables.records.ImageRecord> { private static final long serialVersionUID = 1596930978; /** * The singleton instance of <code>PUBLIC.IMAGE</code> */ public static final vladmihalcea.jooq.schema.tables.Image IMAGE = new vladmihalcea.jooq.schema.tables.Image(); /** * The class holding records for this type */ @Override public java.lang.Class<vladmihalcea.jooq.schema.tables.records.ImageRecord> getRecordType() { return vladmihalcea.jooq.schema.tables.records.ImageRecord.class; } /** * The column <code>PUBLIC.IMAGE.ID</code>. */ public final org.jooq.TableField<vladmihalcea.jooq.schema.tables.records.ImageRecord, java.lang.Long> ID = createField("ID", org.jooq.impl.SQLDataType.BIGINT.nullable(false), this); /** * The column <code>PUBLIC.IMAGE.INDEX</code>. */ public final org.jooq.TableField<vladmihalcea.jooq.schema.tables.records.ImageRecord, java.lang.Integer> INDEX = createField("INDEX", org.jooq.impl.SQLDataType.INTEGER, this); /** * The column <code>PUBLIC.IMAGE.NAME</code>. */ public final org.jooq.TableField<vladmihalcea.jooq.schema.tables.records.ImageRecord, java.lang.String> NAME = createField("NAME", org.jooq.impl.SQLDataType.VARCHAR.length(255), this); /** * The column <code>PUBLIC.IMAGE.PRODUCT_ID</code>. */ public final org.jooq.TableField<vladmihalcea.jooq.schema.tables.records.ImageRecord, java.lang.Long> PRODUCT_ID = createField("PRODUCT_ID", org.jooq.impl.SQLDataType.BIGINT, this); /** * Create a <code>PUBLIC.IMAGE</code> table reference */ public Image() { super("IMAGE", vladmihalcea.jooq.schema.Public.PUBLIC); } /** * Create an aliased <code>PUBLIC.IMAGE</code> table reference */ public Image(java.lang.String alias) { super(alias, vladmihalcea.jooq.schema.Public.PUBLIC, vladmihalcea.jooq.schema.tables.Image.IMAGE); } /** * {@inheritDoc} */ @Override public org.jooq.Identity<vladmihalcea.jooq.schema.tables.records.ImageRecord, java.lang.Long> getIdentity() { return vladmihalcea.jooq.schema.Keys.IDENTITY_IMAGE; } /** * {@inheritDoc} */ @Override public org.jooq.UniqueKey<vladmihalcea.jooq.schema.tables.records.ImageRecord> getPrimaryKey() { return vladmihalcea.jooq.schema.Keys.SYS_PK_10059; } /** * {@inheritDoc} */ @Override public java.util.List<org.jooq.UniqueKey<vladmihalcea.jooq.schema.tables.records.ImageRecord>> getKeys() { return java.util.Arrays.<org.jooq.UniqueKey<vladmihalcea.jooq.schema.tables.records.ImageRecord>>asList(vladmihalcea.jooq.schema.Keys.SYS_PK_10059, vladmihalcea.jooq.schema.Keys.UK_OQBG3YIU5I1E17SL0FEAWT8PE); } /** * {@inheritDoc} */ @Override public java.util.List<org.jooq.ForeignKey<vladmihalcea.jooq.schema.tables.records.ImageRecord, ?>> getReferences() { return java.util.Arrays.<org.jooq.ForeignKey<vladmihalcea.jooq.schema.tables.records.ImageRecord, ?>>asList(vladmihalcea.jooq.schema.Keys.FK_9W522RC4D0KFDKQ390IHV92GB); } /** * {@inheritDoc} */ @Override public vladmihalcea.jooq.schema.tables.Image as(java.lang.String alias) { return new vladmihalcea.jooq.schema.tables.Image(alias); }} |
Теперь нам также нужно проинформировать Maven о наших недавно сгенерированных классах метаданных JOOQ, чтобы он мог скомпилировать их на следующем этапе тестовой компиляции.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> <executions> <execution> <id>add-source</id> <phase>process-test-sources</phase> <goals> <goal>add-test-source</goal> </goals> <configuration> <sources> <source>${project.build.directory}/generated-sources/java</source> </sources> </configuration> </execution> </executions></plugin> |
Теперь я могу начать играть с JOOQ. Давайте добавим DSLContext в наш контекст приложения Spring:
|
1
2
3
4
|
<bean id="jooqContext" class="org.jooq.impl.DSL" factory-method="using"> <constructor-arg ref="dataSource"/> <constructor-arg value="#{T(org.jooq.SQLDialect).HSQLDB}"/></bean |
И мы напишем тест, чтобы проверить, все ли работает правильно:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
private List<ImageProductDTO> getImageProductDTOs_JOOQ() { return transactionTemplate.execute(new TransactionCallback<List<ImageProductDTO>>() { @Override public List<ImageProductDTO> doInTransaction(TransactionStatus transactionStatus) { return 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 |
Я впервые использую JOOQ, и мне не потребовалось много времени, чтобы просмотреть документацию и настроить все в моем примере кодирования Hibernate Facts. Сборка запросов JOOQ кажется естественной, это похоже на написание собственного кода SQL, поэтому мне не нужно изучать API, чтобы знать, как его использовать. Я с гордостью добавлю его в свой Java Data Toolbox.
В этом примере кодирования генерируются отображения JOOQ в папку test-classes, поэтому вы не можете использовать их из исходных файлов main / java. Это можно решить, но это требует рефакторинга существующего решения путем перемещения классов модели в отдельный модуль Maven. Вы можете сгенерировать схему JOOQ в этом отдельном модуле, где перед упаковкой вы должны переместить классы схемы из тестовых классов в папку классов. Затем вам нужно будет включить этот новый модуль, где вы обычно используете схему JOOQ.