Introdução ao Jooq com Spring
*1. Visão geral *
Este artigo apresentará a Jooq Object Oriented Querying - Jooq - e uma maneira simples de configurá-lo em colaboração com o Spring Framework.
A maioria dos aplicativos Java tem algum tipo de persistência SQL e acessa essa camada com a ajuda de ferramentas de nível superior, como JPA. E, embora isso seja útil, em alguns casos, você realmente precisa de uma ferramenta mais refinada e com mais nuances para acessar seus dados ou realmente tirar proveito de tudo o que o banco de dados subjacente tem a oferecer.
O Jooq evita alguns padrões ORM típicos e gera código que nos permite criar consultas seguras e obter controle completo do SQL gerado por meio de uma API fluente limpa e poderosa.
===* 2. Dependências do Maven *
As seguintes dependências são necessárias para executar o código neste tutorial.
====* 2.1 jOOQ *
<dependency>
<groupId>org.Jooq</groupId>
<artifactId>jooq</artifactId>
<version>3.7.3</version>
</dependency>
====* 2.2 Primavera *
Existem várias dependências do Spring necessárias para o nosso exemplo; no entanto, para simplificar, basta incluir explicitamente dois deles no arquivo POM:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.2.5.RELEASE</version>
</dependency>
====* 2.3 Base de dados *
Para facilitar as coisas para o nosso exemplo, usaremos o banco de dados incorporado H2:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.191</version>
</dependency>
===* 3. Geração de código *
====* 3.1 Estrutura do banco de dados *
Vamos apresentar a estrutura do banco de dados com a qual trabalharemos ao longo deste artigo. Suponha que precisamos criar um banco de dados para um editor armazenar informações dos livros e autores que gerenciam, onde um autor pode escrever muitos livros e um livro pode ser co-escrito por muitos autores.
Para simplificar, geraremos apenas três tabelas: o book para livros, author para autores e outra tabela chamada author_book para representar a relação muitos-para-muitos entre autores e livros. A tabela author possui três colunas: id, first_name e last_name. A tabela book contém apenas uma coluna title e a chave primária id.
As seguintes consultas SQL, armazenadas no arquivo de recursos intro_schema.sql, serão executadas no banco de dados que já configuramos anteriormente para criar as tabelas necessárias e preenchê-las com dados de amostra:
DROP TABLE IF EXISTS author_book, author, book;
CREATE TABLE author (
id INT NOT NULL PRIMARY KEY,
first_name VARCHAR(50),
last_name VARCHAR(50) NOT NULL
);
CREATE TABLE book (
id INT NOT NULL PRIMARY KEY,
title VARCHAR(100) NOT NULL
);
CREATE TABLE author_book (
author_id INT NOT NULL,
book_id INT NOT NULL,
PRIMARY KEY (author_id, book_id),
CONSTRAINT fk_ab_author FOREIGN KEY (author_id) REFERENCES author (id)
ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT fk_ab_book FOREIGN KEY (book_id) REFERENCES book (id)
);
INSERT INTO author VALUES
(1, 'Kathy', 'Sierra'),
(2, 'Bert', 'Bates'),
(3, 'Bryan', 'Basham');
INSERT INTO book VALUES
(1, 'Head First Java'),
(2, 'Head First Servlets and JSP'),
(3, 'OCA/OCP Java SE 7 Programmer');
INSERT INTO author_book VALUES (1, 1), (1, 3), (2, 1);
====* 3.2 Propriedades Maven Plugin *
Usaremos três plugins Maven diferentes para gerar o código Jooq. O primeiro deles é o plugin Properties Maven.
Este plug-in é usado para ler dados de configuração de um arquivo de recurso. Não é necessário, pois os dados podem ser adicionados diretamente ao POM, mas é uma boa idéia gerenciar as propriedades externamente.
Nesta seção, definiremos propriedades para conexões com o banco de dados, incluindo a classe do driver JDBC, a URL do banco de dados, o nome de usuário e a senha, em um arquivo chamado intro_config.properties. A externalização dessas propriedades facilita a alternância do banco de dados ou apenas a alteração dos dados de configuração.
O objetivo read-project-properties deste plug-in deve ser vinculado a uma fase inicial para que os dados de configuração possam ser preparados para uso por outros plug-ins. Nesse caso, ele está vinculado à fase initialize:
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>properties-maven-plugin</artifactId>
<version>1.0.0</version>
<executions>
<execution>
<phase>initialize</phase>
<goals>
<goal>read-project-properties</goal>
</goals>
<configuration>
<files>
<file>src/main/resources/intro_config.properties</file>
</files>
</configuration>
</execution>
</executions>
</plugin>
====* 3.3 Plug-in do SQL Maven *
O plug-in SQL Maven é usado para executar instruções SQL para criar e preencher tabelas de banco de dados. Ele utilizará as propriedades que foram extraídas do arquivo intro_config.properties pelo plug-in Properties Maven e obterá as instruções SQL do recurso intro_schema.sql.
O plug-in do SQL Maven está configurado da seguinte forma:
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>sql-maven-plugin</artifactId>
<version>1.5</version>
<executions>
<execution>
<phase>initialize</phase>
<goals>
<goal>execute</goal>
</goals>
<configuration>
<driver>${db.driver}</driver>
<url>${db.url}</url>
<username>${db.username}</username>
<password>${db.password}</password>
<srcFiles>
<srcFile>src/main/resources/intro_schema.sql</srcFile>
</srcFiles>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.191</version>
</dependency>
</dependencies>
</plugin>
Observe que esse plug-in deve ser colocado depois do plug-in Properties Maven no arquivo POM, pois seus objetivos de execução estão vinculados à mesma fase e o Maven os executará na ordem em que estão listados.
====* 3.4 Plugin jOOQ Codegen *
O Jooq Codegen Plugin gera código Java a partir de uma estrutura de tabela de banco de dados. Seu objetivo generate deve ser associado à fase generate-sources para garantir a ordem correta de execução. Os metadados do plug-in são parecidos com o seguinte:
<plugin>
<groupId>org.Jooq</groupId>
<artifactId>jooq-codegen-maven</artifactId>
<version>${org.jooq.version}</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<jdbc>
<driver>${db.driver}</driver>
<url>${db.url}</url>
<user>${db.username}</user>
<password>${db.password}</password>
</jdbc>
<generator>
<target>
<packageName>com..jooq.introduction.db</packageName>
<directory>src/main/java</directory>
</target>
</generator>
</configuration>
</execution>
</executions>
</plugin>
====* 3.5 Gerando código *
Para finalizar o processo de geração do código fonte, precisamos executar a fase Maven generate-sources. No Eclipse, podemos fazer isso clicando com o botão direito do mouse no projeto e escolhendo Run As → Maven generate-sources. Após a conclusão do comando, os arquivos de origem correspondentes às tabelas author, book, author_book (e vários outros para classes de suporte) são gerados.
Vamos nos aprofundar nas classes da tabela para ver o que o Jooq produziu. Cada classe tem um campo estático com o mesmo nome que a classe, exceto que todas as letras no nome estão em maiúsculas. A seguir, trechos de código extraídos das definições das classes geradas:
A classe Author:
public class Author extends TableImpl<AuthorRecord> {
public static final Author AUTHOR = new Author();
//other class members
}
A classe Book:
public class Book extends TableImpl<BookRecord> {
public static final Book BOOK = new Book();
//other class members
}
A classe AuthorBook:
public class AuthorBook extends TableImpl<AuthorBookRecord> {
public static final AuthorBook AUTHOR_BOOK = new AuthorBook();
//other class members
}
As instâncias referenciadas por esses campos estáticos servirão como objetos de acesso a dados para representar as tabelas correspondentes ao trabalhar com outras camadas em um projeto.
===* 4. Configuração de molas *
====* 4.1 Traduzindo exceções do jOOQ para o Spring *
Para tornar as exceções geradas pela execução do Jooq consistentes com o suporte do Spring para acesso ao banco de dados, precisamos convertê-las em subtipos da classe DataAccessException.
Vamos definir uma implementação da interface ExecuteListener para converter exceções:
public class ExceptionTranslator extends DefaultExecuteListener {
public void exception(ExecuteContext context) {
SQLDialect dialect = context.configuration().dialect();
SQLExceptionTranslator translator
= new SQLErrorCodeSQLExceptionTranslator(dialect.name());
context.exception(translator
.translate("Access database using Jooq", context.sql(), context.sqlException()));
}
}
Esta classe será usada pelo contexto do aplicativo Spring.
====* 4.2 Configurando o Spring *
Esta seção seguirá as etapas para definir um PersistenceContext que contém metadados e beans a serem usados no contexto do aplicativo Spring.
Vamos começar aplicando as anotações necessárias à classe:
-
_ @ Configuração_: faça com que a classe seja reconhecida como um contêiner para beans
-
_ @ ComponentScan_: configure as diretivas de varredura, incluindo a opção value para declarar uma matriz de nomes de pacotes para procurar componentes. Neste tutorial, o pacote a ser pesquisado é o gerado pelo plugin Jooq Codegen Maven
-
_ @ EnableTransactionManagement_: ativa as transações a serem gerenciadas pelo Spring *_ @ PropertySource_: indica os locais dos arquivos de propriedades a serem carregados. O valor neste artigo aponta para o arquivo que contém dados de configuração e dialeto do banco de dados, que passa a ser o mesmo arquivo mencionado na subseção 4.1.
@Configuration
@ComponentScan({"com..Jooq.introduction.db.public_.tables"})
@EnableTransactionManagement
@PropertySource("classpath:intro_config.properties")
public class PersistenceContext {
//Other declarations
}
Em seguida, use um objeto Environment para obter os dados de configuração, que são usados para configurar o bean DataSource:
@Autowired
private Environment environment;
@Bean
public DataSource dataSource() {
JdbcDataSource dataSource = new JdbcDataSource();
dataSource.setUrl(environment.getRequiredProperty("db.url"));
dataSource.setUser(environment.getRequiredProperty("db.username"));
dataSource.setPassword(environment.getRequiredProperty("db.password"));
return dataSource;
}
Agora, definimos vários beans para trabalhar com operações de acesso ao banco de dados:
@Bean
public TransactionAwareDataSourceProxy transactionAwareDataSource() {
return new TransactionAwareDataSourceProxy(dataSource());
}
@Bean
public DataSourceTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
@Bean
public DataSourceConnectionProvider connectionProvider() {
return new DataSourceConnectionProvider(transactionAwareDataSource());
}
@Bean
public ExceptionTranslator exceptionTransformer() {
return new ExceptionTranslator();
}
@Bean
public DefaultDSLContext dsl() {
return new DefaultDSLContext(configuration());
}
Por fim, fornecemos uma implementação Jooq Configuration e a declaramos como um bean Spring a ser usado pela classe DSLContext:
@Bean
public DefaultConfiguration configuration() {
DefaultConfiguration JooqConfiguration = new DefaultConfiguration();
jooqConfiguration.set(connectionProvider());
jooqConfiguration.set(new DefaultExecuteListenerProvider(exceptionTransformer()));
String sqlDialectName = environment.getRequiredProperty("jooq.sql.dialect");
SQLDialect dialect = SQLDialect.valueOf(sqlDialectName);
jooqConfiguration.set(dialect);
return jooqConfiguration;
}
===* 5. Usando jOOQ com Spring *
Esta seção demonstra o uso do Jooq em consultas comuns de acesso ao banco de dados. Existem dois testes, um para confirmação e outro para reversão, para cada tipo de operação de "gravação", incluindo inserção, atualização e exclusão de dados. O uso da operação de “leitura” é ilustrado ao selecionar dados para verificar as consultas de “gravação”.
Começaremos declarando um objeto DSLContext com fio automático e instâncias de classes geradas pelo Jooq a serem usadas por todos os métodos de teste:
@Autowired
private DSLContext dsl;
Author author = Author.AUTHOR;
Book book = Book.BOOK;
AuthorBook authorBook = AuthorBook.AUTHOR_BOOK;
====* 5.1. Inserindo dados *
O primeiro passo é inserir dados nas tabelas:
dsl.insertInto(author)
.set(author.ID, 4)
.set(author.FIRST_NAME, "Herbert")
.set(author.LAST_NAME, "Schildt")
.execute();
dsl.insertInto(book)
.set(book.ID, 4)
.set(book.TITLE, "A Beginner's Guide")
.execute();
dsl.insertInto(authorBook)
.set(authorBook.AUTHOR_ID, 4)
.set(authorBook.BOOK_ID, 4)
.execute();
Uma consulta SELECT para extrair dados:
Result<Record3<Integer, String, Integer>> result = dsl
.select(author.ID, author.LAST_NAME, DSL.count())
.from(author)
.join(authorBook)
.on(author.ID.equal(authorBook.AUTHOR_ID))
.join(book)
.on(authorBook.BOOK_ID.equal(book.ID))
.groupBy(author.LAST_NAME)
.fetch();
A consulta acima produz a seguinte saída:
+----+---------+-----+
| ID|LAST_NAME|count|
+----+---------+-----+
| 1|Sierra | 2|
| 2|Bates | 1|
| 4|Schildt | 1|
+----+---------+-----+
O resultado é confirmado pela API Assert:
assertEquals(3, result.size());
assertEquals("Sierra", result.getValue(0, author.LAST_NAME));
assertEquals(Integer.valueOf(2), result.getValue(0, DSL.count()));
assertEquals("Schildt", result.getValue(2, author.LAST_NAME));
assertEquals(Integer.valueOf(1), result.getValue(2, DSL.count()));
Quando ocorre uma falha devido a uma consulta inválida, uma exceção é lançada e a transação é revertida. No exemplo a seguir, a consulta INSERT viola uma restrição de chave estrangeira, resultando em uma exceção:
@Test(expected = DataAccessException.class)
public void givenInvalidData_whenInserting_thenFail() {
dsl.insertInto(authorBook)
.set(authorBook.AUTHOR_ID, 4)
.set(authorBook.BOOK_ID, 5)
.execute();
}
====* 5.2 Atualizando dados *
Agora vamos atualizar os dados existentes:
dsl.update(author)
.set(author.LAST_NAME, "Baeldung")
.where(author.ID.equal(3))
.execute();
dsl.update(book)
.set(book.TITLE, "Building your REST API with Spring")
.where(book.ID.equal(3))
.execute();
dsl.insertInto(authorBook)
.set(authorBook.AUTHOR_ID, 3)
.set(authorBook.BOOK_ID, 3)
.execute();
Obtenha os dados necessários:
Result<Record3<Integer, String, String>> result = dsl
.select(author.ID, author.LAST_NAME, book.TITLE)
.from(author)
.join(authorBook)
.on(author.ID.equal(authorBook.AUTHOR_ID))
.join(book)
.on(authorBook.BOOK_ID.equal(book.ID))
.where(author.ID.equal(3))
.fetch();
A saída deve ser:
+----+---------+----------------------------------+
| ID|LAST_NAME|TITLE |
+----+---------+----------------------------------+
| 3|Baeldung |Building your REST API with Spring|
+----+---------+----------------------------------+
O teste a seguir verificará se o Jooq funcionou conforme o esperado:
assertEquals(1, result.size());
assertEquals(Integer.valueOf(3), result.getValue(0, author.ID));
assertEquals("Baeldung", result.getValue(0, author.LAST_NAME));
assertEquals("Building your REST API with Spring", result.getValue(0, book.TITLE));
Em caso de falha, uma exceção é lançada e a transação é revertida, o que confirmamos com um teste:
@Test(expected = DataAccessException.class)
public void givenInvalidData_whenUpdating_thenFail() {
dsl.update(authorBook)
.set(authorBook.AUTHOR_ID, 4)
.set(authorBook.BOOK_ID, 5)
.execute();
}
====* 5.3. Exclusão de dados *
O método a seguir exclui alguns dados:
dsl.delete(author)
.where(author.ID.lt(3))
.execute();
Aqui está a consulta para ler a tabela afetada:
Result<Record3<Integer, String, String>> result = dsl
.select(author.ID, author.FIRST_NAME, author.LAST_NAME)
.from(author)
.fetch();
A saída da consulta:
+----+----------+---------+
| ID|FIRST_NAME|LAST_NAME|
+----+----------+---------+
| 3|Bryan |Basham |
+----+----------+---------+
O teste a seguir verifica a exclusão:
assertEquals(1, result.size());
assertEquals("Bryan", result.getValue(0, author.FIRST_NAME));
assertEquals("Basham", result.getValue(0, author.LAST_NAME));
Por outro lado, se uma consulta for inválida, lançará uma exceção e a transação será revertida. O seguinte teste provará que:
@Test(expected = DataAccessException.class)
public void givenInvalidData_whenDeleting_thenFail() {
dsl.delete(book)
.where(book.ID.equal(1))
.execute();
}
===* 6. Conclusão*
Este tutorial apresentou os conceitos básicos do Jooq, uma biblioteca Java para trabalhar com bancos de dados. Ele abordou as etapas para gerar código-fonte a partir de uma estrutura de banco de dados e como interagir com esse banco de dados usando as classes recém-criadas.
A implementação de todos esses exemplos e trechos de código pode ser encontrada em um projeto GitHub.