Introdução ao Jooq com Spring

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 AsMaven 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.