在本文中,我不会淹没JOOQ。我更喜欢Hibernate及其背后的所有Spring Data JPA功能。但是本文将不涉及它们。
当我们使用Hibernate和Spring Data JPA时,我们不需要考虑内部流程-了解注释并在存储库中编写正确的方法名称-这两个怪物将为您完成其余工作。对于JOOQ而言,不幸的是,对于许多人来说,这将花费一些精力,并且要比findAllByUserId(Long userId)编写更多内容。什么是JOOQ?
让我们编写一个简单的SQL查询:select * from countries c where c.population > 10000000;
我们可以在控制台中执行此请求。好的我们感觉好像不在控制台中。我们希望我们的应用程序发送此请求。它不是一次性脚本,并且至少已针对语法进行了验证。我们通过Spring Data JPA做到这一点:List<Country> findAllByPopulationAfter(Long amount);
与上面相同的请求被执行,但是由Spring执行。这种方法的明显优势是什么?它由功能强大的框架执行;它还验证对错误的请求。但是查询管理负责框架。我们希望同时完全管理该请求,以便完全验证该请求。采用询问:@Query("select c from Country c where c.population > :amount")
List<Country> findAllPopulationGreaterThan(@Param("amount") Long amount);
在SQL和DSL之间进行这种折衷也很好。但是,如果我们不想弄乱SQL,我们将很高兴看到以下内容:return dsl.selectFrom(COUNTRIES)
.where(COUNTRIES.POPULATION.greaterThan(amount))
.fetch();
一些库都适合这样的:QueryDSL
JOOQ
Speedment关于QueryDSL我一个写了几年前。我没有研究Speedment,但它似乎不仅仅是一个简单的DSL生成器,而且我还必须学习他们加密SQL查询命令的方法。一般来说,今天我们将讨论JOOQ。JOOQ查询
是的,我们已经在上面看到了以下查询之一:return dsl.selectFrom(COUNTRIES)
.where(COUNTRIES.POPULATION.greaterThan(amount))
.fetch();
还有什么?例如,一个简单的get请求:return dsl.selectFrom(Countries.COUNTRIES)
.where(Countries.COUNTRIES.ID.eq(id))
.fetchAny();
或插入请求:return dsl.insertInto(Countries.COUNTRIES)
.set(Countries.COUNTRIES.NAME, country.getName())
.set(Countries.COUNTRIES.POPULATION, country.getPopulation())
.set(Countries.COUNTRIES.GOVERNMENT_FORM, nameOrNull(country.getGovernmentForm()))
.returning()
.fetchOne();
如您所见,一切都清楚了,仅此而已。但这只是乍看之下。找出这个兔子洞有多深可能需要几个星期。但是,让我们从头开始。是的,它将成为专家
我更喜欢Gradle。但是出于某种原因,JOOQ开发人员对Gradle的关注度降低了,他们选择了第三方插件。好的,培训项目将在Maven上进行。但是,亲爱的读者,如果您在github的深处翻阅,并且准备在Gradle上展示一个完全自定义的项目,请写信给我,您将成为本文的合著者。H2同样适用(培训项目将在PostgreSQL上)。为什么我在Gradle和H2上没有成功, Gradle. jooq-generator . H2, « » H2 , , , .
必要的Maven依赖项:<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jooq</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons.lang3.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.jooq</groupId>
<artifactId>jooq</artifactId>
<version>${jooq.version}</version>
</dependency>
<dependency>
<groupId>org.jooq</groupId>
<artifactId>jooq-meta</artifactId>
<version>${jooq.version}</version>
</dependency>
<dependency>
<groupId>org.jooq</groupId>
<artifactId>jooq-codegen</artifactId>
<version>${jooq.version}</version>
</dependency>
</dependencies>
Spring已经有一个JOOQ启动器,它将根据application.yml中的指定设置自配置DataSource。但是对于JOOQ的全部工作来说,这还不够。必须产生实体。是的,是的,我们不编写实体,而是生成它们。所谓表优先的方法。专注于使用数据库的常规应用程序结构如下:
在服务层之前,数据以平面模型(DTO)的形式在应用程序中传播。此外,当服务需要使用数据库时,它将DTO转换为实体,将其发送到存储库,并且该DTO已经将其保存到数据库。JOOQ开发人员对事物的看法有所不同:
正如我们所看到的,在JOOQ中,他们认真修改了标准模式并决定走自己的路。差异非常明显:- DTO转换为实体已经在存储库中进行。
- 我们不会这样写实体。它是由JOOQ生成器为我们编写的。
已经足以得出结论,JOOQ并不是那么简单。高度可定制的生成实体,DTO中难以理解的映射以及其他困难将吓跑许多人。但是,在无处可去的情况下,对JOOQ的这些方面进行定向研究可能会严重带走并花费数天甚至数周。我相信我的帖子将大大节省您的时间。我们将介绍与JOOQ合作的以下方面:- 实体生成。
- 编写CRUD查询。
- 优化CRUD请求。
- 还有另一个CRUD请求优化。
- 使用标准的JOOQ库映射实体。
- 并讨论为什么所有这些都是必要的。
我们走吧。我们写什么?
我们的项目将有两个实体:国家/地区:@Data
public class Country {
private Long id;
private String name;
private GovernmentForm governmentForm;
private Integer population;
private List<City> cities;
}
市:@Data
public class City {
private Long id;
private Long countryId;
private String name;
}
一对多关系。国家包含许多相关的城市。城市包含countryId。Flyway迁移看起来像这样:create table countries
(
id bigserial primary key,
name varchar(255),
government_form varchar(255),
population int
);
create table cities
(
id bigserial primary key,
country_id bigint,
name varchar(255)
);
让我们从实体生成开始。
正如我所说,实体是分别生成的。有两种方法可以做到这一点。在Maven中生成实体
当项目生成时,Maven启动生成器并生成实体。您可以在任何方便的时间调用生成器并生成实体。例如,当基座的结构发生变化时,这是必需的。Maven中的插件如下所示:
<plugin>
<groupId>org.jooq</groupId>
<artifactId>jooq-codegen-maven</artifactId>
<version>${jooq.version}</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<configuration>
<jdbc>
<driver>${db.driver}</driver>
<url>${db.url}</url>
<user>${db.username}</user>
<password>${db.password}</password>
</jdbc>
<generator>
<database>
<includes>.*</includes>
<excludes>
flyway_schema_history
</excludes>
<inputSchema>public</inputSchema>
</database>
<generate>
<records>true</records>
</generate>
<target>
<packageName>ru.xpendence.jooqexample.domain</packageName>
<directory>target/generated-sources/jooq</directory>
</target>
</generator>
</configuration>
</plugin>
如果您正确执行了所有操作,请更新Maven,然后在插件中看到jooq-codegen。
运行。它将为您生成实体。
这些是用于访问数据库的实体。第一个麻烦:它们是一成不变的。也就是说,它们是可变的,但是类型更改以一种人为设计的方式发生,因此该过程描述将被拉到单独的文章中。因此,尝试在表生成阶段考虑数据类型。实体看起来很奇怪。例如,这是CountrysRecord类的签名:public class CountriesRecord extends UpdatableRecordImpl<CountriesRecord> implements Record4<Long, String, String, Integer> {
如我们所见,CountryRecord实现了Record4,它由4种类型来键入。国家表有4列,因此为Record4。城市中有3列,因此为Record3。我不知道为什么这样发明。JOOQ库中有22个这样的记录,即Record1 ... Record22。从中可以得出结论,JOOQ的功能仅限于处理最多22列的表,但事实并非如此。我有包含数十列的表,JOOQ只是立即实现Record。好吧,...用Java代码生成JOOQ实体看起来像这样:@Component
public class AfterStartupApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
@Value("${spring.datasource.driver-class-name}")
private String driver;
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
@Value("${jooq.generator.database.name}")
private String databaseName;
@Value("${jooq.generator.database.with-includes}")
private String databaseWithIncludes;
@Value("${jooq.generator.database.with-input-schema}")
private String databaseWithInputSchema;
@Value("${jooq.generator.target.package-name}")
private String targetPackageName;
@Value("${jooq.generator.target.directory}")
private String targetDirectory;
@SneakyThrows
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
new GenerationTool().run(configureGenerator());
}
private Configuration configureGenerator() {
return new Configuration()
.withJdbc(new Jdbc()
.withDriver(driver)
.withUrl(url)
.withUser(username)
.withPassword(password))
.withGenerator(new Generator()
.withDatabase(new Database()
.withName(databaseName)
.withIncludes(databaseWithIncludes)
.withExcludes("")
.withInputSchema(databaseWithInputSchema))
.withTarget(new Target()
.withPackageName(targetPackageName)
.withDirectory(targetDirectory)));
}
}
就我而言,当所有新的迁移都汇总并且数据库是最新的时,生成器将在应用程序完全启动后启动。因此,我们有模型,有实体,有基础。现在该创建存储库并编写查询了。创建存储库
所有请求都通过DslContext。重复DslContext。@Repository
@RequiredArgsConstructor
public class CountryRepository implements CrudRepository<Country> {
private final DSLContext dsl;
}
存储库已准备就绪。编写CRUD查询
插
如果使用SQL,则添加另一个国家/地区的请求将是:insert into countries(name, government_form, population)
values (' -', 'UNITARY', 100500) returning *;
在JOOQ中,查询尽可能接近SQL语法: public Country insertValues(Country country) {
return dsl.insertInto(Countries.COUNTRIES)
.values(country.getId(), country.getName(), country.getPopulation(), nameOrNull(country.getGovernmentForm()))
.returning()
.fetchOne()
.into(Country.class);
}
如我们所见,没有什么复杂的。但是,这样的请求不适合我们,因为在我们的情况下,ID是在数据库中生成的,因此我们必须考虑到这一点。如果我们这样写:.values(null, country.getName(), country.getPopulation(), nameOrNull(country.getGovernmentForm()))
我们会得到org.postgresql.util.PSQLException: : "id" NOT NULL
但是,如果我们在应用程序端生成了ID,那么这样的请求将是一个旅程。我们将不得不重写请求,为每个字段指定一个特定值。所以你可以: public Country insert(Country country) {
return dsl.insertInto(Countries.COUNTRIES)
.set(Countries.COUNTRIES.NAME, country.getName())
.set(Countries.COUNTRIES.POPULATION, country.getPopulation())
.set(Countries.COUNTRIES.GOVERNMENT_FORM, nameOrNull(country.getGovernmentForm()))
.returning()
.fetchOne()
.into(Country.class);
}
在这种情况下,我们在字段中手动设置对象。这个选项很有效,但是更好。像Spring Data JPA一样,发送整个对象进行保存是理想的:repository.save(country);
可能是这样。为此,我们需要将我们的模型映射到extended Record实体并将其发送以进行保存: public Country insert(Country country) {
return dsl.insertInto(Countries.COUNTRIES)
.set(dsl.newRecord(Countries.COUNTRIES, country))
.returning()
.fetchOptional()
.orElseThrow(() -> new DataAccessException("Error inserting entity: " + country.getId()))
.into(Country.class);
}
对于不需要配置映射的情况,此方法是最简单且最容易理解的。但是映射设置会更低。正如我们注意到的,此查询返回整个实体。您可以在fetch()方法中确定确切需要返回的内容。看起来像这样: public Long insertAndReturnId(Country country) {
return dsl.insertInto(Countries.COUNTRIES)
.set(dsl.newRecord(Countries.COUNTRIES, country))
.returning(Countries.COUNTRIES.ID)
.fetchOptional()
.orElseThrow(() -> new DataAccessException("Error inserting entity: " + country.getId()))
.get(Countries.COUNTRIES.ID);
}
有点麻烦,但是又要比较一下。我们将编写其余的CRUD方法。更新资料
SQL查询:update countries
set name = '',
government_form = 'CONFEDERATE',
population = 100500
where id = 1
returning *;
JOOQ请求: public Country update(Country country) {
return dsl.update(Countries.COUNTRIES)
.set(dsl.newRecord(Countries.COUNTRIES, country))
.where(Countries.COUNTRIES.ID.eq(country.getId()))
.returning()
.fetchOptional()
.orElseThrow(() -> new DataAccessException("Error updating entity: " + country.getId()))
.into(Country.class);
}
选择
SQL中的查询为:select *
from countries c
where id = ?;
在JOOQ中,查询将相应地显示: public Country find(Long id) {
return dsl.selectFrom(Countries.COUNTRIES)
.where(Countries.COUNTRIES.ID.eq(id))
.fetchAny()
.into(Country.class);
}
删除
在有些请求中,我们没有任何退货。此类查询返回受影响的行数。在删除中,我们什么也没退,但是我们收到的信息仍然对我们有用。 public Boolean delete(Long id) {
return dsl.deleteFrom(Countries.COUNTRIES)
.where(Countries.COUNTRIES.ID.eq(id))
.execute() == 1;
}
我们删除一行。因此,SQL查询将返回类似以下内容的内容:1 row affected in 5 ms
收到这样的答案后,我们肯定知道该行已被删除。这足以宣布操作成功。这不是童话。那只是润滑剂。使用常规映射器在Record中对实体进行瘦映射,反之亦然
“好吧,”读者对读者说,“一对多在哪里?我没有看到乡村随众多城市一起成长的那一刻。”你会是对的。这不是休眠;在这里您必须手动进行。您所需要做的就是获取ID = country.getId()的城市列表。我已经展示了into()方法,该方法使用常规映射器将Record映射到实体。但是JOOQ还有一个map()方法,使我们可以对Record进行任何操作。让我们看一下它的功能并添加城市: public Country find(Long id) {
return dsl.selectFrom(Countries.COUNTRIES)
.where(Countries.COUNTRIES.ID.eq(id))
.fetchAny()
.map(r -> {
Country country = r.into(Country.class);
country.setCities(cityRepository.findAll(Cities.CITIES.COUNTRY_ID.eq(country.getId())));
return country;
});
}
如我们所见,现在我们首先映射“国家/地区中的记录”,然后再在城市中提出另一个请求,我们获得了该国家/地区的所有城市并将其设置为实质。但是可以有数十个这样的集合,这意味着可以有数十个请求。并直接在方法中描述这些请求就是这样。正确的解决方案是编写一个单独的映射器,并将所有这些请求放在此处。@RequiredArgsConstructor
@Component
public class CountryRecordMapper implements RecordMapper<CountriesRecord, Country> {
private final CityRepository cityRepository;
@Override
public Country map(CountriesRecord record) {
Country country = record.into(Country.class);
country.setCities(cityRepository.findAll(Cities.CITIES.COUNTRY_ID.eq(country.getId())));
return country;
}
}
现在查询将如下所示: public Country findWithCustomMapper(Long id) {
return dsl.selectFrom(Countries.COUNTRIES)
.where(Countries.COUNTRIES.ID.eq(id))
.fetchAny()
.map(r -> countryRecordMapper.map((CountriesRecord) r));
}
它更加简洁,并且不包含其他逻辑。好的,我们学习了如何将Record映射到实体,但是如何微调Record中实体的映射呢?
到目前为止,我们已经有了以下设计:.set(dsl.newRecord(Countries.COUNTRIES, country))
已经很好了,但是如果我们需要专门绘制一个字段怎么办?例如,我们在那里有LocalDateTime,而诸如Timestamp之类的PostgreSQL生成器生成了OffsetDateTime。在这种情况下,该字段将不会被映射,也不会记录在数据库中。在这种情况下,我们将需要另一个映射器执行相同的操作,但方向相反。是的,对于每个映射器,我们都没有映射器
他被称为。让我们写他的继承人。@Component
@RequiredArgsConstructor
public class CountryRecordUnmapper implements RecordUnmapper<Country, CountriesRecord> {
private final DSLContext dsl;
@Override
public CountriesRecord unmap(Country country) throws MappingException {
CountriesRecord record = dsl.newRecord(Countries.COUNTRIES, country);
record.setPopulation(-1);
return record;
}
}
插入及其应用程序将如下所示: public Country insertWithUnmapper(Country country) {
return dsl.insertInto(Countries.COUNTRIES)
.set(countryRecordUnmapper.unmap(country))
.returning()
.fetchOptional()
.orElseThrow(() -> new DataAccessException("Error inserting entity: " + country.getId()))
.into(Country.class);
}
如我们所见,也相当。结论与结论
就个人而言,我更喜欢休眠。对于90%以上的应用程序,可能更合理。但是,如果您(读者)想要控制每个请求,则JOOQ或其他类似的库更适合您。一如既往,我发布了培训项目。他躺在这里。