1. 概要

この記事では、Jooqオブジェクト指向クエリ(Jooq)と、SpringFrameworkと連携してセットアップする簡単な方法を紹介します。

ほとんどのJavaアプリケーションには、ある種のSQL永続性があり、JPAなどの高レベルのツールを使用してそのレイヤーにアクセスします。 これは便利ですが、場合によっては、データにアクセスしたり、基盤となるDBが提供するすべてのものを実際に利用したりするために、より細かく、より微妙なツールが必要になります。

Jooqは、いくつかの典型的なORMパターンを回避し、タイプセーフクエリを構築し、クリーンで強力な流暢なAPIを介して生成されたSQLを完全に制御できるコードを生成します。

この記事では、SpringMVCに焦点を当てています。 記事jOOQのSpringBootサポートでは、SpringBootでjOOQを使用する方法について説明しています。

2. Mavenの依存関係

このチュートリアルのコードを実行するには、次の依存関係が必要です。

2.1. jOOQ

<dependency>
    <groupId>org.jooq</groupId>
    <artifactId>jooq</artifactId>
    <version>3.14.15</version>
</dependency>

2.2. 春

この例では、いくつかのSpring依存関係が必要です。 ただし、簡単にするために、POMファイルにそれらのうちの2つを明示的に含める必要があります。

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.2.2.RELEASE</version>
</dependency>

2.3. データベース

この例を簡単にするために、H2組み込みデータベースを使用します。

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.191</version>
</dependency>

3. コード生成

3.1. データベース構造

この記事全体で使用するデータベース構造を紹介しましょう。 出版社が管理する本と著者の情報を保存するためのデータベースを作成する必要があるとします。ここで、著者は多くの本を書き、本は多くの著者によって共同執筆される可能性があります。

簡単にするために、本の book 、著者の author 、および多対多を表すauthor_bookという別のテーブルの3つのテーブルのみを生成します。 -著者と本の間の多くの関係。 author テーブルには、 id first_name 、およびlast_nameの3つの列があります。bookテーブルにはtitle列とid主キー。

intro_schema.sql リソースファイルに保存されている次のSQLクエリは、必要なテーブルを作成してサンプルデータを入力するために、前に設定したデータベースに対して実行されます。

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. プロパティMavenプラグイン

3つの異なるMavenプラグインを使用してJooqコードを生成します。 これらの最初のものは、PropertiesMavenプラグインです。

このプラグインは、リソースファイルから構成データを読み取るために使用されます。 データはPOMに直接追加される可能性があるため必須ではありませんが、プロパティを外部で管理することをお勧めします。

このセクションでは、 intro_config.properties という名前のファイルで、JDBCドライバークラス、データベースURL、ユーザー名、パスワードなどのデータベース接続のプロパティを定義します。 これらのプロパティを外部化すると、データベースの切り替えや構成データの変更が簡単になります。

このプラグインのread-project-propertiesの目標は、構成データを他のプラグインで使用できるように準備できるように、初期段階にバインドする必要があります。 この場合、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. SQLMavenプラグイン

SQL Mavenプラグインは、SQLステートメントを実行してデータベーステーブルを作成および設定するために使用されます。 これは、PropertiesMavenプラグインによってintro_config.properties ファイルから抽出されたプロパティを利用し、intro_schema.sqlリソースからSQLステートメントを取得します。

SQLMavenプラグインは次のように構成されています。

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

このプラグインは、実行目標が両方とも同じフェーズにバインドされており、Mavenがリストされている順序で実行するため、POMファイルのPropertiesMavenプラグインよりも後に配置する必要があることに注意してください。

3.4. jOOQCodegenプラグイン

Jooq Codegenプラグインは、データベーステーブル構造からJavaコードを生成します。 そのgenerateゴールは、 generate-sources フェーズにバインドして、正しい実行順序を確保する必要があります。 プラグインのメタデータは次のようになります。

<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.baeldung.jooq.introduction.db</packageName>
                        <directory>src/main/java</directory>
                    </target>
                </generator>
            </configuration>
        </execution>
    </executions>
</plugin>

3.5. コードの生成

ソースコード生成のプロセスを完了するには、Maven generate-sourcesフェーズを実行する必要があります。 Eclipseでは、プロジェクトを右クリックして実行 –> Mavengenerate-sourcesを選択することでこれを行うことができます。 コマンドが完了すると、 author book author_book テーブル(およびサポートクラス用の他のいくつか)に対応するソースファイルが生成されます。

テーブルクラスを掘り下げて、Jooqが何を生成したかを見てみましょう。 各クラスには、名前のすべての文字が大文字になっていることを除いて、クラスと同じ名前の静的フィールドがあります。 以下は、生成されたクラスの定義から取得したコードスニペットです。

Author クラス:

public class Author extends TableImpl<AuthorRecord> {
    public static final Author AUTHOR = new Author();

    // other class members
}

Book クラス:

public class Book extends TableImpl<BookRecord> {
    public static final Book BOOK = new Book();

    // other class members
}

AuthorBook クラス:

public class AuthorBook extends TableImpl<AuthorBookRecord> {
    public static final AuthorBook AUTHOR_BOOK = new AuthorBook();

    // other class members
}

これらの静的フィールドによって参照されるインスタンスは、プロジェクト内の他のレイヤーを操作するときに、対応するテーブルを表すデータアクセスオブジェクトとして機能します。

4. スプリング構成

4.1. jOOQ例外をSpringに変換する

Jooqの実行からスローされた例外をデータベースアクセスのSpringサポートと一致させるには、それらをDataAccessExceptionクラスのサブタイプに変換する必要があります。

例外を変換するためのExecuteListenerインターフェースの実装を定義しましょう。

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()));
    }
}

このクラスは、Springアプリケーションコンテキストによって使用されます。

4.2. Springの構成

このセクションでは、Springアプリケーションコンテキストで使用されるメタデータとBeanを含むPersistenceContextを定義する手順を説明します。

クラスに必要なアノテーションを適用することから始めましょう:

  • @Configuration :クラスをBeanのコンテナーとして認識されるようにします
  • @ComponentScan :コンポーネントを検索するためのパッケージ名の配列を宣言する value オプションを含む、スキャンディレクティブを構成します。 このチュートリアルでは、検索されるパッケージはJooqCodegenMavenプラグインによって生成されたものです。
  • @EnableTransactionManagement :トランザクションをSpringで管理できるようにします
  • @PropertySource :ロードするプロパティファイルの場所を示します。 この記事の値は、データベースの構成データと方言を含むファイルを指しています。これは、サブセクション4.1で説明したのと同じファイルです。
@Configuration
@ComponentScan({"com.baeldung.Jooq.introduction.db.public_.tables"})
@EnableTransactionManagement
@PropertySource("classpath:intro_config.properties")
public class PersistenceContext {
    // Other declarations
}

次に、 Environment オブジェクトを使用して構成データを取得します。このデータは、DataSourceBeanの構成に使用されます。

@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; 
}

次に、データベースアクセス操作で機能するいくつかのBeanを定義します。

@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());
}

最後に、Jooq Configuration 実装を提供し、DSLContextクラスで使用されるSpringBeanとして宣言します。

@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. SpringでjOOQを使用する

このセクションでは、一般的なデータベースアクセスクエリでのJooqの使用方法について説明します。 データの挿入、更新、削除など、「書き込み」操作のタイプごとに、コミット用とロールバック用の2つのテストがあります。 「書き込み」クエリを検証するためにデータを選択するときの「読み取り」操作の使用法が示されています。

まず、自動配線された DSLContext オブジェクトと、すべてのテストメソッドで使用されるJooqで生成されたクラスのインスタンスを宣言します。

@Autowired
private DSLContext dsl;

Author author = Author.AUTHOR;
Book book = Book.BOOK;
AuthorBook authorBook = AuthorBook.AUTHOR_BOOK;

5.1. データの挿入

最初のステップは、データをテーブルに挿入することです。

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();

データを抽出するためのSELECTクエリ:

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();

上記のクエリは、次の出力を生成します。

+----+---------+-----+
|  ID|LAST_NAME|count|
+----+---------+-----+
|   1|Sierra   |    2|
|   2|Bates    |    1|
|   4|Schildt  |    1|
+----+---------+-----+

結果はAssertAPIによって確認されます。

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()));

無効なクエリが原因で失敗が発生すると、例外がスローされ、トランザクションがロールバックされます。 次の例では、 INSERT クエリが外部キー制約に違反しているため、例外が発生します。

@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. データの更新

次に、既存のデータを更新しましょう。

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();

必要なデータを取得します。

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();

出力は次のようになります。

+----+---------+----------------------------------+
|  ID|LAST_NAME|TITLE                             |
+----+---------+----------------------------------+
|   3|Baeldung |Building your REST API with Spring|
+----+---------+----------------------------------+

次のテストでは、Jooqが期待どおりに機能したことを確認します。

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));

失敗した場合、例外がスローされ、トランザクションがロールバックされます。これはテストで確認されます。

@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. データの削除

次のメソッドは、いくつかのデータを削除します。

dsl.delete(author)
  .where(author.ID.lt(3))
  .execute();

影響を受けるテーブルを読み取るためのクエリは次のとおりです。

Result<Record3<Integer, String, String>> result = dsl
  .select(author.ID, author.FIRST_NAME, author.LAST_NAME)
  .from(author)
  .fetch();

クエリ出力:

+----+----------+---------+
|  ID|FIRST_NAME|LAST_NAME|
+----+----------+---------+
|   3|Bryan     |Basham   |
+----+----------+---------+

次のテストは、削除を確認します。

assertEquals(1, result.size());
assertEquals("Bryan", result.getValue(0, author.FIRST_NAME));
assertEquals("Basham", result.getValue(0, author.LAST_NAME));

一方、クエリが無効な場合、例外がスローされ、トランザクションがロールバックされます。 次のテストはそれを証明します:

@Test(expected = DataAccessException.class)
public void givenInvalidData_whenDeleting_thenFail() {
    dsl.delete(book)
      .where(book.ID.equal(1))
      .execute();
}

6. 結論

このチュートリアルでは、データベースを操作するためのJavaライブラリであるJooqの基本を紹介しました。 データベース構造からソースコードを生成する手順と、新しく作成されたクラスを使用してそのデータベースと対話する方法について説明しました。

これらすべての例とコードスニペットの実装は、GitHubプロジェクトにあります。