Eclipse + Java + Gradle の環境で Doma を動かす

## はじめに

この記事では、データベースアクセスフレームワークである [Doma](https://github.com/domaframework/doma) を利用する際の開発環境構築についてノウハウを紹介します。
簡単なサンプルアプリケーションを作りながら解説します。

## 前提

### バージョンなど

開発マシンは macOS を想定しますが、Windows と異なるところはないと思います。

IDE、ツール、ライブラリ等のバージョンは以下のとおりです。

- Eclipse IDE for Java Developers 2024-03 (4.31.0) 
- Java 17.0.10（Eclipse のデフォルト）
- Gradle 8.1.1（Eclipse で Gradle Project 作成時に利用されるバージョン） 
- Doma 2.58.0 

### Eclipse の仕組みを踏まえた戦略

Eclipse は以下のようなさまざまな設定ファイルを参照して動きます。

- .classpath : どのライブラリのどのバージョンを使うのかといったクラスパス情報
- .factorypath : アノテーションプロセッサーのライブラリのパス情報
- .project : プロジェクト名などのメタ情報
- .settings : Eclipseの画面からの設定項目など複数ファイルを管理するフォルダ

これらのファイルは Eclipse の 画面上で設定を行うことで出力できますが、その場合の問題点はビルドファイルと Eclipse の設定ファイルで二重管理が必要となってしまうことです。 二重管理を避けるために、ビルドファイルを一次情報としてビルドファイルから Eclipse の設定ファイルを自動生成するのが基本的な戦略となります。

## Eclipse から Gradle Project の作成

Eclipse 上部のメニューから File > New > Project... > Gradle > Gradle Project を選択しダイアログを進めます。プロジェクト名には example を入力して Finish ボタンを押します。

そうすると、example と lib の2つのプロジェクトが作成されます。

## 生成された build.gradle の確認

lib プロジェクトの build.gradle を確認しましょう。

```groovy:build.gradle
/*
 * This file was generated by the Gradle 'init' task.
 *
 * This generated file contains a sample Java library project to get you started.
 * For more details take a look at the 'Building Java & JVM projects' chapter in the Gradle
 * User Manual available at https://docs.gradle.org/8.1.1/userguide/building_java_projects.html
 */

plugins {
    // Apply the java-library plugin for API and implementation separation.
    id 'java-library'
}

repositories {
    // Use Maven Central for resolving dependencies.
    mavenCentral()
}

dependencies {
    // Use JUnit Jupiter for testing.
    testImplementation 'org.junit.jupiter:junit-jupiter:5.9.1'

    // This dependency is exported to consumers, that is to say found on their compile classpath.
    api 'org.apache.commons:commons-math3:3.6.1'

    // This dependency is used internally, and not exposed to consumers on their own compile classpath.
    implementation 'com.google.guava:guava:31.1-jre'
}

// Apply a specific Java toolchain to ease working on different environments.
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

tasks.named('test') {
    // Use JUnit Platform for unit tests.
    useJUnitPlatform()
}
```

上部のコメントにあるように、これはサンプル用のビルドファイルです。

:::note info
まずはこの状態で動作確認したいところですが、私の環境では Eclipse 上から JUnit が動きませんでした。Eclipse の JUnit と build.gradle に記載の JUnit のバージョンに互換性がないからだと思われます。
:::

## build.gradle の修正

オリジナルの build.gradle を以下の内容で置換します。

```groovy:build.gradle
plugins {
    id 'java-library'
    // Eclipse のアノテーションプロセッシング関連の設定ファイルを生成するプラグイン
    id 'com.diffplug.eclipse.apt' version '3.44.0'
    // Doma を使ったビルドをサポートするプラグイン
    id 'org.domaframework.doma.compile' version '2.0.0'
}

repositories {
    mavenCentral()
}

dependencies {
    // 最新の JUnit を利用。
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.2'

    // Doma の核となるライブラリ。ビルド時も実行時も必要。
    implementation 'org.seasar.doma:doma-core:2.58.0'

    // Doma のログをSLF4J経由で出力するライブラリ。
    implementation("org.seasar.doma:doma-slf4j:2.58.0")

    // SLF4J の実装ライブラリ。
    runtimeOnly("ch.qos.logback:logback-classic:1.2.11")

    // Doma のアノテーションプロセッサー。ビルド時のみ必要。
    annotationProcessor 'org.seasar.doma:doma-processor:2.58.0'

    // 本サンプルで利用する H2 Database Engine。データベースでありJDBCドライバである。
    runtimeOnly 'com.h2database:h2:2.2.224'
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

tasks.named('test') {
    useJUnitPlatform()
}

// Eclipse 関連の設定
eclipse {
    // .classpath ファイルに関する設定
    classpath {
        file {
            whenMerged { classpath ->
                // アノテーションプロセッサーで生成したソースコードを保存するフォルダ
                def folder = new org.gradle.plugins.ide.eclipse.model.SourceFolder(".apt_generated", "bin/main")
                // クラスパスに追加
                classpath.entries.add(folder)
                // 存在しなければ作成
                def dir = file(folder.path)
                if (!dir.exists()) {
                    dir.mkdir()
                }
            }
        }
    }
    // .project ファイルに関する設定
    project {
        buildCommand 'org.eclipse.buildship.core.gradleprojectbuilder'
        natures 'org.eclipse.buildship.core.gradleprojectnature'
    }
    // Eclipse から "Refresh Gradle Project" を呼び出した時に実行されるタスク
    synchronizationTasks 'cleanEclipse', 'eclipse'
}
```

ポイントは以下のとおりです。

- plugins ブロックに Eclipse や Doma に関するプラグインを指定します。
- dependencies ブロックに Doma 関連のライブラリを記載します。JUnit のライブラリも変更します。
- eclipse ブロックに Eclipse 関連の設定ファイルを生成するための記述をします。

特にコメントを入れていないところはオリジナルの build.gradle にも記載があったものです。詳細はオリジナルの方のコメントを参照ください。

## 動作確認（build.gradle の記述に問題ないか？）

では、置換した build.gradle が正しく動くか確認しましょう。

### Refresh Gradle Project の実行

example か lib プロジェクトを右クリックして Gradle > Refresh Gradle Project を実行してください。Refresh Gradle Project を行うと build.gradle に記載した synchronizationTasks に指定したタスクが動きます。synchronizationTasks には 以下の2つの Gradle タスクを設定していました。

- cleanEclipse: Eclipse の設定ファイルを削除するタスク
- eclipse: Eclipse 設定ファイルを生成するタスク

Refresh Gradle Project の実行結果は Eclipse のコンソール上に以下のように出力されます。BUILD SUCCESSFUL の文字列が出力されていれば成功です。

```sh
> Task :lib:cleanEclipseClasspath
> Task :lib:cleanEclipseFactorypath
> Task :lib:cleanEclipseJdt
> Task :lib:cleanEclipseJdtApt
> Task :lib:cleanEclipseProject
> Task :lib:cleanEclipse UP-TO-DATE
> Task :lib:eclipseClasspath
> Task :lib:eclipseFactorypath
> Task :lib:eclipseJdt
> Task :lib:eclipseJdtApt
> Task :lib:eclipseProject
> Task :lib:eclipse

Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.

You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.

See https://docs.gradle.org/8.1.1/userguide/command_line_interface.html#sec:command_line_warnings

BUILD SUCCESSFUL in 712ms
11 actionable tasks: 10 executed, 1 up-to-date
```

:::note info
コンソールへの出力を確認するには、コンソールビューの右上の Display Selected Console アイコンから [Gradle Operations] を選んでください。
:::

### JUnit の実行

次に lib プロジェクトを右クリックして Run As > JUnit Test を選択します。
JUnit ビューにグリーンのバーが出力されれば成功です。

## Doma を利用するコードの追加

lib プロジェクトの src/main/java フォルダの example パッケージに次のクラスを追加します。

```java:DbConfig.java
package example;

import java.util.Objects;

import javax.sql.DataSource;

import org.seasar.doma.jdbc.Config;
import org.seasar.doma.jdbc.JdbcLogger;
import org.seasar.doma.jdbc.dialect.Dialect;
import org.seasar.doma.jdbc.dialect.H2Dialect;
import org.seasar.doma.jdbc.tx.LocalTransactionDataSource;
import org.seasar.doma.jdbc.tx.LocalTransactionManager;
import org.seasar.doma.jdbc.tx.TransactionManager;
import org.seasar.doma.slf4j.Slf4jJdbcLogger;

public class DbConfig implements Config {

    private final Dialect dialect;
    private final DataSource dataSource;
    private final JdbcLogger jdbcLogger;
    private final TransactionManager transactionManager;

    public DbConfig(Dialect dialect, DataSource dataSource, JdbcLogger jdbcLogger,
            TransactionManager transactionManager) {
        this.dialect = Objects.requireNonNull(dialect);
        this.dataSource = Objects.requireNonNull(dataSource);
        this.jdbcLogger = Objects.requireNonNull(jdbcLogger);
        this.transactionManager = Objects.requireNonNull(transactionManager);
    }

    @Override
    public JdbcLogger getJdbcLogger() {
        return jdbcLogger;
    }

    @Override
    public Dialect getDialect() {
        return dialect;
    }

    @Override
    public DataSource getDataSource() {
        return dataSource;
    }

    @Override
    public TransactionManager getTransactionManager() {
        return transactionManager;
    }

    public static DbConfig of() {
        var dialect = new H2Dialect();
        var dataSource = new LocalTransactionDataSource("jdbc:h2:mem:tutorial;DB_CLOSE_DELAY=-1", "sa", null);
        var jdbcLogger = new Slf4jJdbcLogger();
        var transactionManager = new LocalTransactionManager(dataSource, jdbcLogger);
        return new DbConfig(dialect, dataSource, jdbcLogger, transactionManager);
    }
}
```

```java:Person.java
package example;

import org.seasar.doma.Entity;
import org.seasar.doma.Id;

@Entity
public class Person {

    @Id
    Integer id;

    String name;
}
```

```java:PersonDao.java
package example;

import java.util.List;

import org.seasar.doma.Dao;
import org.seasar.doma.Insert;
import org.seasar.doma.Script;
import org.seasar.doma.Select;
import org.seasar.doma.Sql;

@Dao
public interface PersonDao {

    @Script
    @Sql("""
            create table if not exists person(
              id integer not null primary key,
              name varchar(20)
            )
            """)
    void createTable();

    @Select
    List<Person> selectAll();

    @Insert
    int insert(Person person);
}
```

lib プロジェクトの src/main/resources/META-INF/example/PersonDao フォルダに次の SQL ファイルを追加します。

```sql:selectAll.sql
select /*%expand*/* from person
```

lib プロジェクトの src/test/java フォルダの example パッケージに次のクラスを追加します。

```java:PersonDaoTest
package example;

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;

public class PersonDaoTest {

    // データベースの設定
    DbConfig dbConfig = DbConfig.of();
    // DAO
    PersonDao dao = new PersonDaoImpl(dbConfig);

    @Test
    public void test() {
        // トランザクション内で実行
        dbConfig.getTransactionManager().required(() -> {
            dao.createTable(); // テーブルの作成

            var person1 = new Person();
            person1.id = 1;
            person1.name = "abc";
            dao.insert(person1); // 追加

            var person2 = new Person();
            person2.id = 2;
            person2.name = "def";
            dao.insert(person2); // 追加

            var people = dao.selectAll(); // 全件取得
            assertEquals(2, people.size());

            for (var p : people) {
                System.out.printf(p.name);
            }
        });

    }
}
```

## 動作確認（Doma 関連の設定が正しいか？）

追加した Doma　のコードのビルドや実行を確認します。

### クリーンビルドの実行

Eclipse の上部メニューから Project > Clean を実行して問題が発生しないことを確認します。

### JUnit の実行

lib プロジェクトを右クリックして Run As > JUnit Test を選択します。
JUnit ビューから PersonDaoTest　が実行されていること、グリーンのバーが表示されていることを確認します。

### アノテーションプロセッサーによる検証

PersonDao.java の `@Insert` をコメントアウトし、ファイルを保存します。
エディタ上に下記のエラーメッセージが表示されたらアノテーションプロセッサーが適切に動いていると言えます。

```
[DOMA4005] The query annotation such as @Select and @Update is required.
```

確認を終えたらコメントアウトを元に戻してください。

:::note info
ファイル保存時にアノテーションプロセッサーを起動するには、 Eclipse 上部の Project メニューで Build Automatically が有効になっていなければいけません（デフォルトで有効です）。無効にしている場合は、代わりにクリーンビルドを実行してください。
:::

## Q & A
### Refresh Gradle Project をいつ実行すればいいのですか？

build.gradle を変更したら実行しましょう。例えば、依存ライブラリを新しく追加したときやライブラリのバージョン番号を変更したときなどです。

### ビルドファイルを Groovy ではなく Kotlin DSL で記述する場合の注意点はありますか？

Eclipse や Doma に特化したような注意点は特にないと思います。
Kotlin DSL を使った例はこちらを参照ください。

- https://github.com/domaframework/getting-started
- https://github.com/domaframework/simple-examples

### アノテーションプロセッサーのオプションを指定するにはどうすればいいですか？

次のような記述が必要です。[Domaのドキュメント](https://doma.readthedocs.io/ja/latest/annotation-processing/#setting-options-in-gradle)も参照ください。

```groovy: build.gradle
compileJava {
    aptOptions {
        processorArgs = [
            'doma.debug' : 'true'
        ]
    }
}
```

:::note warn
アノテーションプロセッサーのオプションは、 Refresh Gradle Project を実施することで Eclipse の設定ファイル（.settings/org.eclipse.jdt.apt.core.prefs）に反映されますが、この設定ファイルは直ちに Eclipse にロードされません。Eclipse 上で有効にするには以下のいずれかの方法を実施してください（もっと良い方法があるかもしれません）。

- Eclipse を再起動する
- .settings/org.eclipse.jdt.apt.core.prefs を Eclipse 上で開き修正して保存する（コメント行に空白を1文字追加するなど）

バグレポートを作ってみました。
https://github.com/eclipse/buildship/issues/1305
:::

### Eclipse の設定ファイルを確認するにはどうするのが良いですか？

Eclipse の Package Explorer の右上メニューから Filters... を選択し、 .*resources のチェックを外せば Package Explorer 上で表示できます。 

Visual Studio Code などで表示するのはオススメしません。Visual Studio Code に Java の拡張機能が入っていると勝手に Eclipse の設定を書き換えたりすることがあるので注意が必要です。

## おわりに

もっと良い方法をご存知の方は共有をよろしくお願いします。

Gradle ではなく Maven を利用する場合は、[Eclipse + Java + Maven の環境で Doma を動かす](https://qiita.com/nakamura-to/items/f98a05b926363016070b) を参照ください。