かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Gradle で Multi-project を作成する ( その12 )( doma2lib+cmdapp+webapp編、sample-webapp プロジェクトを作成する )

概要

記事一覧はこちらです。

Gradle で Multi-project を作成する ( その11 )( doma2lib+cmdapp+webapp編、log4jdbc-log4j2 を導入してトランザクションが有効なことを確認する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Doma 2 の Entity、Dao を提供するライブラリ+Spring Boot ベースのコマンドラインアプリケーション+Spring Boot ベースの Web アプリケーションの Multi-project を作成します。
    • 今回は sample-webapp プロジェクトを作成します。

参照したサイト・書籍

目次

  1. Spring Initializr で sample-webapp プロジェクトを作成する
  2. sample-webapp の build.gradle を変更する
  3. gradle-multiprj-doma2lib-cmdwebapp の settings.gradle に include 'sample-webapp' を追加する
  4. gradle-multiprj-doma2lib-cmdwebapp の build.gradle の configure の適用対象に sample-webapp を追加する
  5. DB のテーブルのデータを読み込んで画面に表示する機能を実装する
  6. テストを作成する
  7. 動作確認
  8. 次回は。。。

手順

Spring Initializr で sample-webapp プロジェクトを作成する

IntelliJ IDEA から Spring Initializr を利用して sample-webapp プロジェクトを作成します。

f:id:ksby:20190501125136p:plainf:id:ksby:20190501125253p:plain
f:id:ksby:20190501125406p:plainf:id:ksby:20190501125441p:plain
f:id:ksby:20190501125530p:plainf:id:ksby:20190501125556p:plain

f:id:ksby:20190501125736p:plain

作成後 IntelliJ IDEA のウィンドウが開きますが、何もせずに閉じます。

src ディレクトリと build.gradle だけ残して、それ以外のディレクトリファイルは全て削除します。

また sample-webapp/src/test/java/ksbysample/app/samplewebapp/SampleWebappApplicationTests.java も削除し、sample-cmdapp/src/test/java/ の下はクリアします。

sample-webapp の build.gradle を変更する

sample-webapp の build.gradle を以下のように変更します。

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    runtimeOnly("org.springframework.boot:spring-boot-devtools")

    implementation project(":doma2-lib")
}
  • doma2-lib プロジェクトへの依存関係を追加します。

gradle-multiprj-doma2lib-cmdwebapp の settings.gradle に include 'sample-webapp' を追加する

settings.gradle に include 'sample-webapp' を追加します。

rootProject.name = 'gradle-multiprj-doma2lib-cmdwebapp'
include 'doma2-lib'
include 'sample-cmdapp'
include 'sample-webapp'

gradle-multiprj-doma2lib-cmdwebapp の build.gradle の configure の適用対象に sample-webapp を追加する

gradle-multiprj-doma2lib-cmdwebapp の build.gradle の configure の適用対象に sample-webapp を追加します。

..........

configure(subprojects.findAll { it.name ==~ /^(doma2-lib|sample-cmdapp|sample-webapp)$/ }) {
    apply plugin: "org.springframework.boot"

    dependencyManagement {
        imports {
            mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
        }
    }

    ext {
        jdbcDriver = "mysql:mysql-connector-java:8.0.15"
        domaVersion = "2.24.0"
    }
    
    dependencies {
        def lombokVersion = "1.18.6"

        testImplementation("org.springframework.boot:spring-boot-starter-test")

        runtimeOnly(jdbcDriver)
        implementation("org.seasar.doma.boot:doma-spring-boot-starter:1.1.1")
        implementation("org.seasar.doma:doma:${domaVersion}")
        implementation("org.flywaydb:flyway-core:5.2.4")
        implementation("com.integralblue:log4jdbc-spring-boot-starter:1.0.2")

        // for lombok
        // testAnnotationProcessor、testCompileOnly を併記しなくてよいよう configurations で設定している
        annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
        compileOnly("org.projectlombok:lombok:${lombokVersion}")
    }

}
  • /^(doma2-lib|sample-cmdapp)$//^(doma2-lib|sample-cmdapp|sample-webapp)$/ に変更します。

変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。Gradle Tool Window に sample-webapp が表示されます。

f:id:ksby:20190501131037p:plain

Run Dashboard Tool Window にも SampleWebappApplication が追加されています。

f:id:ksby:20190501131248p:plain

clean タスク実行 → Rebuild Project 実行 → build タスク実行を行い、警告・エラーが出ずに BUILD SUCCESSFUL が出力されることを確認します。

f:id:ksby:20190501131516p:plain f:id:ksby:20190501131632p:plain

DB のテーブルのデータを読み込んで画面に表示する機能を実装する

以下の仕様で実装します。

  • employee テーブルからデータを読み込んで name, age, sex のデータを画面上に表示します。
  • URL は http://localhost:8080/sample にします。
  • Thymeleaf は使用しません。@ResponseBody アノテーションを付与してテキストデータを返します。

sample-webapp/src/main/java/ksbysample/app/samplewebapp/SampleWebappApplication.java に @ComponentScan アノテーションを追加し、ksbysample.libksbysample.app の2つの package を指定します

package ksbysample.app.samplewebapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.AutoConfigurationExcludeFilter;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.TypeExcludeFilter;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;

@SpringBootApplication
@ComponentScan(
        basePackages = {"ksbysample.lib", "ksbysample.app"},
        excludeFilters = {
                @ComponentScan.Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
                @ComponentScan.Filter(type = FilterType.CUSTOM,
                        classes = AutoConfigurationExcludeFilter.class)})
public class SampleWebappApplication {

    public static void main(String[] args) {
        SpringApplication.run(SampleWebappApplication.class, args);
    }

}

employee テーブルからデータ一覧を取得するメソッドがないので、doma2-lib/src/main/java/ksbysample/lib/doma2lib/dao/EmployeeDao.java に selectAll メソッドを追加します。

■doma2-lib/src/main/java/ksbysample/lib/doma2lib/dao/EmployeeDao.java

package ksbysample.lib.doma2lib.dao;

import ksbysample.lib.doma2lib.entity.Employee;
import org.seasar.doma.boot.ConfigAutowireable;
import org.seasar.doma.Dao;
import org.seasar.doma.Delete;
import org.seasar.doma.Insert;
import org.seasar.doma.Select;
import org.seasar.doma.Update;

import java.util.List;

/**
 */
@Dao
@ConfigAutowireable
public interface EmployeeDao {

    /**
     * @param id
     * @return the Employee entity
     */
    @Select
    Employee selectById(Integer id);

    @Select
    List<Employee> selectAll();
    
    /**
     * @param entity
     * @return affected rows
     */
    @Insert(excludeNull = true)
    int insert(Employee entity);

    /**
     * @param entity
     * @return affected rows
     */
    @Update
    int update(Employee entity);

    /**
     * @param entity
     * @return affected rows
     */
    @Delete
    int delete(Employee entity);
}

■doma2-lib/src/main/resources/META-INF/ksbysample/lib/doma2lib/dao/EmployeeDao/selectAll.sql

select
  /*%expand*/*
from
  employee
order by id

DB のテーブルのデータを読み込んで画面に表示するクラスを実装します。sample-webapp/src/main/java/ksbysample/app/samplewebapp/SampleController.java を新規作成し、以下の内容を記述します。

package ksbysample.app.samplewebapp;

import ksbysample.lib.doma2lib.dao.EmployeeDao;
import ksbysample.lib.doma2lib.entity.Employee;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.List;
import java.util.stream.Collectors;

@Controller
@RequestMapping("/sample")
public class SampleController {

    private final EmployeeDao employeeDao;

    public SampleController(EmployeeDao employeeDao) {
        this.employeeDao = employeeDao;
    }

    @RequestMapping(produces = MediaType.TEXT_PLAIN_VALUE)
    @ResponseBody
    public String index() {
        List<Employee> employeeList = employeeDao.selectAll();
        return employeeList.stream()
                .map(employee ->
                        String.format("name = %s, age = %d, sex = %s"
                                , employee.getName()
                                , employee.getAge()
                                , employee.getSex()))
                .collect(Collectors.joining("\n"));
    }

}

clean タスク実行 → Rebuild Project 実行 → build タスク実行を行い、警告・エラーが出ずに BUILD SUCCESSFUL が出力されることを確認します(画面キャプチャは省略します)。

テストを作成する

sample-webapp/src/main/java/ksbysample/app/samplewebapp/SampleController.java のテストを作成します。

f:id:ksby:20190501141041p:plainf:id:ksby:20190501141111p:plain

sample-webapp/src/test/groovy/ksbysample/app/samplewebapp/SampleControllerTest.groovy が新規作成されますので、以下の内容を記述します。

package ksbysample.app.samplewebapp

import groovy.sql.Sql
import ksbysample.lib.doma2lib.entity.Employee
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.web.servlet.MockMvc
import spock.lang.Specification

import javax.sql.DataSource

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status

// 通常は spring.profiles.active は IntelliJ IDEA の JUnit の Run/Debug Configuration と build.gradle に定義するが、
// 今回はテストが1つしかないので @SpringBootTest の properties 属性で指定する
@SpringBootTest(properties = ["spring.profiles.active=develop"])
@AutoConfigureMockMvc
class SampleControllerTest extends Specification {

    static final def TESTDATA = [
            [id: 1, name: "鈴木 太郎", age: 42, sex: "男", update_time: "2019/05/01 00:00:00"],
            [id: 2, name: "渡辺 香", age: 36, sex: "女", update_time: "2019/04/30 16:51:47"],
            [id: 3, name: "木村 結衣", age: 23, sex: "女", update_time: "2019/04/01 09:15:02"]
    ]

    @Autowired
    private MockMvc mvc

    @Autowired
    private DataSource dataSource

    def sql
    List<Employee> backupData

    void setup() {
        sql = new Sql(dataSource)

        // employee テーブルのバックアップを取得後クリアする
        backupData = sql.rows("select * from employee")
        sql.execute("truncate table employee")
    }

    void cleanup() {
        // バックアップからemployee テーブルのデータをリカバリする
        sql.execute("truncate table employee")
        backupData.each {
            sql.execute("insert into employee values (:id, :name, :age, :sex, :update_time)", it)
        }

        sql.close()
    }

    def "employeeテーブルにデータがない場合にはcontentも空になる"() {
        expect:
        mvc.perform(get("/sample"))
                .andExpect(status().isOk())
                .andExpect(content().string(""))
    }

    def "employeeテーブルにデータがある場合には全てのデータが出力される"() {
        setup:
        TESTDATA.each {
            sql.execute("insert into employee values (:id, :name, :age, :sex, :update_time)", it)
        }
        def expectedContent =
                TESTDATA.collect { String.format("name = ${it.name}, age = ${it.age}, sex = ${it.sex}") }
                        .join("\n")

        expect:
        mvc.perform(get("/sample"))
                .andExpect(status().isOk())
                .andExpect(content().string(expectedContent))
    }

}

テストを実行して成功することを確認します。

f:id:ksby:20190501151541p:plain

今回 employee テーブルをバックアップ・リカバリする仕組みを考えたので、doma2-lib/src/test/groovy/ksbysample/lib/doma2lib/dao/EmployeeDaoTest にも反映します。以下のように変更します。

package ksbysample.lib.doma2lib.dao

import groovy.sql.Sql
import ksbysample.lib.doma2lib.entity.Employee
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import spock.lang.Specification
import spock.lang.Unroll

import javax.sql.DataSource

// 通常は spring.profiles.active は IntelliJ IDEA の JUnit の Run/Debug Configuration と build.gradle に定義するが、
// 今回はテストが1つしかないので @SpringBootTest の properties 属性で指定する
@SpringBootTest(properties = ["spring.profiles.active=develop"])
class EmployeeDaoTest extends Specification {

    static final def TESTDATA = [
            [id: 1, name: "鈴木 太郎", age: 42, sex: "男", update_time: "2019/05/01 00:00:00"],
            [id: 2, name: "渡辺 香", age: 36, sex: "女", update_time: "2019/04/30 16:51:47"],
            [id: 3, name: "木村 結衣", age: 23, sex: "女", update_time: "2019/04/01 09:15:02"]
    ]

    static final def TESTDATA2 =
            new Employee(id: null, name: "木村 太郎", age: 35, sex: "男", updateTime: null)

    @Autowired
    EmployeeDao employeeDao

    @Autowired
    private DataSource dataSource

    def sql

    List<Employee> backupData

    void setup() {
        sql = new Sql(dataSource)

        // employee テーブルのバックアップを取得後クリアする
        backupData = sql.rows("select * from employee")
        sql.execute("truncate table employee")
    }

    void cleanup() {
        // バックアップからemployee テーブルのデータをリカバリする
        sql.execute("truncate table employee")
        backupData.each {
            sql.execute("insert into employee values (:id, :name, :age, :sex, :update_time)", it)
        }

        sql.close()
    }

    @Unroll
    def "selectById メソッドのテスト(#id --> #name, #age, #sex)"() {
        setup:
        TESTDATA.each {
            sql.execute("insert into employee values (:id, :name, :age, :sex, :update_time)", it)
        }
        def row = employeeDao.selectById(id)

        expect:
        row.name == name
        row.age == age
        row.sex == sex

        where:
        id || name    | age | sex
        1  || "鈴木 太郎" | 42  | "男"
        2  || "渡辺 香"  | 36  | "女"
    }

    def "insert メソッドのテスト"() {
        setup:
        employeeDao.insert(TESTDATA2)

        expect:
        def row = sql.firstRow("select * from employee where name = ${TESTDATA2.name}")
        row.name == TESTDATA2.name
        row.age == TESTDATA2.age
        row.sex == TESTDATA2.sex
    }

}

テストを実行して成功することを確認します。

f:id:ksby:20190501153435p:plain

動作確認

clean タスク実行 → Rebuild Project 実行 → build タスク実行を行い、警告・エラーが出ずに BUILD SUCCESSFUL が出力されることを確認します。

f:id:ksby:20190501153905p:plain f:id:ksby:20190501154018p:plain

Run Dashboard から sample-webapp を起動して動作確認します。

f:id:ksby:20190501154217p:plain

employee テーブルに以下のデータが登録されている場合、

f:id:ksby:20190501154531p:plain

http://localhost:8080/sample にアクセスすると employee テーブルのデータが出力されていることが確認できます。

f:id:ksby:20190501154706p:plain

コマンドラインから java -Dspring.profiles.active=develop -Dbatch.execute=EmployeeDataCsvToDbLoader -jar sample-cmdapp-1.0.0-RELEASE.jar -csvfile=D:\project-springboot\ksbysample-boot-miscellaneous\gradle-multiprj-doma2lib-cmdwebapp\sample-cmdapp\src\test\resources\employee.csv を実行してデータを追加すると、

f:id:ksby:20190501154853p:plain

http://localhost:8080/sample の画面を更新すると追加されたデータが反映されました。

f:id:ksby:20190501154932p:plain

コマンドラインから sample-webapp を起動して動作確認します。java -Dspring.profiles.active=develop -jar sample-webapp-1.0.0-RELEASE.jar コマンドを実行して起動します。

f:id:ksby:20190501155403p:plain (.....途中省略.....) f:id:ksby:20190501155524p:plain

http://localhost:8080/sample にアクセスすると先程と同じデータが表示されました。

f:id:ksby:20190501155628p:plain

最後に sample-webapp プロジェクトのディレクトリ構成を記載します。

f:id:ksby:20190501161752p:plain

sample-webapp-1.0.0-RELEASE.jar の中身を見てみると以下のようになっており doma2-lib-1.0.0-RELEASE.jar が lib ディレクトリの下に入っています。

f:id:ksby:20190501161925p:plain

次回は。。。

ここまで doma2-lib-1.0.0-RELEASE.jar を各サブプロジェクトの jar の中に入れる方式(JarLauncher による起動)で書きましたので、次は doma2-lib-1.0.0-RELEASE.jar を外に出す方式(PropertiesLauncher による起動)を書く予定です。

履歴

2019/05/01
初版発行。

Gradle で Multi-project を作成する ( その11 )( doma2lib+cmdapp+webapp編、log4jdbc-log4j2 を導入してトランザクションが有効なことを確認する )

概要

記事一覧はこちらです。

Gradle で Multi-project を作成する ( その10 )( doma2lib+cmdapp+webapp編、sample-cmdapp プロジェクトを作成する2 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Doma 2 の Entity、Dao を提供するライブラリ+Spring Boot ベースのコマンドラインアプリケーション+Spring Boot ベースの Web アプリケーションの Multi-project を作成します。
    • ksbysample.app.samplecmdapp.EmployeeDataCsvToDbLoader#run メソッドに @Transactional アノテーションを付与してトランザクションが有効になるように設定したつもりでいましたが、log4jdbc-log4j2 を導入して本当に有効になっているのかを確認します。

参照したサイト・書籍

目次

  1. gradle-multiprj-doma2lib-cmdwebapp の build.gradle を変更する
  2. doma2-lib の src/main/resources の下に application-develop.properties、application-product.properties を作成する
  3. doma2-lib の db-product.properties と同じ内容を db-develop.properties に記述する
  4. EmployeeDataCsvToDbLoaderTest クラスでトランザクションが有効か確認する
  5. java.sql.SQLSyntaxErrorException: SELECT command denied to user 'sampledb_user'@'172.18.0.1' for table 'user_variables_by_thread' を解消する
  6. sample-cmdapp をコマンドラインから実行した時にトランザクションが有効か確認する

手順

gradle-multiprj-doma2lib-cmdwebapp の build.gradle を変更する

gradle-multiprj-doma2lib-cmdwebapp の build.gradle の configure に log4jdbc-log4j2 を利用するための com.integralblue:log4jdbc-spring-boot-starter を依存関係に追加します。

..........

configure(subprojects.findAll { it.name ==~ /^(doma2-lib|sample-cmdapp)$/ }) {
    apply plugin: "org.springframework.boot"

    dependencyManagement {
        imports {
            mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
        }
    }

    ext {
        jdbcDriver = "mysql:mysql-connector-java:8.0.15"
        domaVersion = "2.24.0"
    }
    
    dependencies {
        def lombokVersion = "1.18.6"

        testImplementation("org.springframework.boot:spring-boot-starter-test")

        runtimeOnly(jdbcDriver)
        implementation("org.seasar.doma.boot:doma-spring-boot-starter:1.1.1")
        implementation("org.seasar.doma:doma:${domaVersion}")
        implementation("org.flywaydb:flyway-core:5.2.4")
        implementation("com.integralblue:log4jdbc-spring-boot-starter:1.0.2")

        // for lombok
        // testAnnotationProcessor、testCompileOnly を併記しなくてよいよう configurations で設定している
        annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
        compileOnly("org.projectlombok:lombok:${lombokVersion}")
    }

}
  • implementation("com.integralblue:log4jdbc-spring-boot-starter:1.0.2") を追加します。

doma2-lib の src/main/resources の下に application-develop.properties、application-product.properties を作成する

develop profile の時には log4jdbc-log4j2 でログが出力されるようにし、product profile の時にはログが出力されないようにします。設定ファイルは doma2-lib プロジェクトの下に作成します(ここに作成しても sample-cmdapp に適用されます)。

doma2-lib/src/main/resources の下に application-develop.properties、application-product.properties を新規作成し、以下の内容を記述します。

■application-develop.properties

# log4jdbc-log4j2
logging.level.jdbc.sqlonly=DEBUG
logging.level.jdbc.sqltiming=INFO
logging.level.jdbc.audit=INFO
logging.level.jdbc.resultset=ERROR
logging.level.jdbc.resultsettable=ERROR
logging.level.jdbc.connection=DEBUG

■application-product.properties

spring.autoconfigure.exclude=com.integralblue.log4jdbc.spring.Log4jdbcAutoConfiguration

doma2-lib の db-product.properties と同じ内容を db-develop.properties に記述する

develop profile ではログが出力され product profile ではログが出力されないことを確認できるようにするために doma2-lib/src/main/resources/db-product.properties の内容を db-develop.properties と同じにします。

■db-product.properties

doma.dialect=mysql

spring.datasource.hikari.jdbc-url=jdbc:mysql://localhost/sampledb?sslMode=DISABLED&characterEncoding=utf8
spring.datasource.hikari.username=sampledb_user
spring.datasource.hikari.password=xxxxxxxx
spring.datasource.hikari.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.leak-detection-threshold=60000
spring.datasource.hikari.register-mbeans=true

EmployeeDataCsvToDbLoaderTest クラスでトランザクションが有効か確認する

sample-cmdapp/src/test/java/ksbysample/app/samplecmdapp/EmployeeDataCsvToDbLoaderTest.groovy のテストを実行して、employeeDataCsvToDbLoader = (EmployeeDataCsvToDbLoader) context.getAutowireCapableBeanFactory().applyBeanPostProcessorsAfterInitialization(employeeDataCsvToDbLoader, "employeeDataCsvToDbLoader") で手動で Bean に登録した場合にトランザクションが有効か確認します。

テストを実行すると insert 前に Connection.setAutoCommit(false) が呼び出されて、employeeDataCsvToDbLoader.run(...) で2件 insert した後に Connection.commit() が呼び出されていることが確認できました。

f:id:ksby:20190501082847p:plain

わざと RuntimeException を throw して rollback されるかも確認します。 sample-cmdapp/src/main/java/ksbysample/app/samplecmdapp/EmployeeDataCsvToDbLoader.java を以下のように変更します。

@Component
@ConditionalOnProperty(value = {"batch.execute"}, havingValue = "EmployeeDataCsvToDbLoader")
public class EmployeeDataCsvToDbLoader implements CommandLineRunner {

    ..........

    @Override
    @Transactional
    public void run(String... args) throws Exception {
        ..........

        // CSV ファイルを1行ずつ読み込み employee テーブルに insert する
        try (BufferedReader br = Files.newBufferedReader(Paths.get(this.csvfile), StandardCharsets.UTF_8)) {
            Employee employee = new Employee();
            for (EmployeeCsvRecord employeeCsvRecord : routines.iterate(EmployeeCsvRecord.class, br)) {
                modelMapper.map(employeeCsvRecord, employee);
                employeeDao.insert(employee);
                throw new RuntimeException("error");
            }
        }
    }

}
  • throw new RuntimeException("error"); を追加します。

テストを実行すると1件目の insert 文が実行された後に RuntimeException が throw された結果 Connection.rollback() が呼び出されていました。

f:id:ksby:20190501112522p:plain

トランザクションが有効になっていることが確認できました。

また spring.profiles.active=product に変更すると、

@SpringBootTest(properties = ["spring.profiles.active=product"])
class EmployeeDataCsvToDbLoaderTest extends Specification {

log4jdbc-log4j2 のログは出力されなくなりました。

f:id:ksby:20190501115518p:plain

java.sql.SQLSyntaxErrorException: SELECT command denied to user 'sampledb_user'@'172.18.0.1' for table 'user_variables_by_thread' を解消する

上でテストを実行した時に java.sql.SQLSyntaxErrorException: SELECT command denied to user 'sampledb_user'@'172.18.0.1' for table 'user_variables_by_thread' というエラーメッセージが出ていることに気づきました。flyway が performance_schema.user_variables_by_thread テーブルにアクセスしようとして出来なかったために出力されていました。

f:id:ksby:20190501113132p:plain

doma2-lib/src/main/resources/db/init/create_database.sql を以下のように変更します。

create database if not exists sampledb character set utf8mb4 collate utf8mb4_ja_0900_as_cs_ks;

create user 'sampledb_user'@'%' identified by 'xxxxxxxx';
grant all privileges ON sampledb.* to 'sampledb_user'@'%' with grant option;
grant select ON performance_schema.user_variables_by_thread to 'sampledb_user'@'%';
flush privileges;
  • grant select ON performance_schema.user_variables_by_thread to 'sampledb_user'@'%'; を追加します。

コンテナを再起動しても create_database.sql に追加した grant select ON ... 文が実行されなかったので、adminer に root ユーザでログインして直接 SQL 文を実行します。

f:id:ksby:20190501114610p:plain

テストを実行すると今度はエラーメッセージが出力されなくなりました。

f:id:ksby:20190501114827p:plain

sample-cmdapp をコマンドラインから実行した時にトランザクションが有効か確認する

コマンドラインから実行した時もトランザクションが有効になっているか確認します。まずは clean タスク実行 → Rebuild Project 実行 → build タスク実行を行い、sample-cmdapp-1.0.0-RELEASE.jar を build し直します。

f:id:ksby:20190501120220p:plain f:id:ksby:20190501120330p:plain

java -Dspring.profiles.active=develop -Dbatch.execute=EmployeeDataCsvToDbLoader -jar sample-cmdapp-1.0.0-RELEASE.jar -csvfile=D:\project-springboot\ksbysample-boot-miscellaneous\gradle-multiprj-doma2lib-cmdwebapp\sample-cmdapp\src\test\resources\employee.csv コマンドを実行すると Connection.commit() が呼び出されています。

f:id:ksby:20190501120835p:plain (.....途中省略.....) f:id:ksby:20190501121125p:plain

sample-cmdapp/src/main/java/ksbysample/app/samplecmdapp/EmployeeDataCsvToDbLoader.javathrow new RuntimeException("error"); を追加してから build し直した後、再度コマンドを実行すると1件目を insert した後 Connection.rollback() が呼び出されていました。

f:id:ksby:20190501121804p:plain

コマンドラインから実行した時もトランザクションが有効になっていることが確認できました。

また -Dspring.profiles.active=product に変更した java -Dspring.profiles.active=product -Dbatch.execute=EmployeeDataCsvToDbLoader -jar sample-cmdapp-1.0.0-RELEASE.jar -csvfile=D:\project-springboot\ksbysample-boot-miscellaneous\gradle-multiprj-doma2lib-cmdwebapp\sample-cmdapp\src\test\resources\employee.csv コマンドを実行すると log4jdbc-log4j2 のログは出力されませんでした。

f:id:ksby:20190501122312p:plain f:id:ksby:20190501122422p:plain

履歴

2019/05/01
初版発行。

Gradle で Multi-project を作成する ( その10 )( doma2lib+cmdapp+webapp編、sample-cmdapp プロジェクトを作成する2 )

概要

記事一覧はこちらです。

Gradle で Multi-project を作成する ( その9 )( doma2lib+cmdapp+webapp編、sample-cmdapp プロジェクトを作成する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Doma 2 の Entity、Dao を提供するライブラリ+Spring Boot ベースのコマンドラインアプリケーション+Spring Boot ベースの Web アプリケーションの Multi-project を作成します。
    • sample-cmdapp プロジェクトを作成します。前回からの続きで、今回はテストの作成、及びコマンドラインから実行した動作確認を行います。

参照したサイト・書籍

  1. Shortest way to get File object from resource in Groovy
    https://stackoverflow.com/questions/39245934/shortest-way-to-get-file-object-from-resource-in-groovy

  2. Spring start a transaction with object created by new
    https://stackoverflow.com/questions/4707193/spring-start-a-transaction-with-object-created-by-new

    • applicationContext.getAutowireCapableBeanFactory().applyBeanPostProcessorsAfterInitialization(...) で手動で Bean を登録する方法を参考にしました。

目次

  1. テストを作成する
  2. コマンドラインから実行して動作確認する
  3. 補足
    1. sample-cmdapp でも Flyway は動作している

手順

テストを作成する

最初にテストで利用する CSV ファイルを作成します。sample-cmdapp/src/test/resources/employee.csv を新規作成し、以下の内容を記述します。

"name","age","sex"
"高橋 蓮","15","男"
"渡辺 結月","24","女"

次に sample-cmdapp/src/main/java/ksbysample/app/samplecmdapp/EmployeeDataCsvToDbLoader.java のテストクラスを作成します。

@SpringBootTest アノテーションが付与されたクラスのテストメソッドを実行するとテストメソッド内の処理が実行される前に CommandLineRunner インターフェースを実装したクラスの run メソッドが実行されてしまうのですが、以下の方法で run メソッドが自動で実行されないようにします。

  • EmployeeDataCsvToDbLoader クラスに @ConditionalOnProperty(value = { "batch.execute" }, havingValue = "EmployeeDataCsvToDbLoader") アノテーションを付与して、batch.execute=EmployeeDataCsvToDbLoader が設定されない限り自動で Bean に登録されないようにします。
  • EmployeeDataCsvToDbLoader クラスのコンストラクタに渡すオブジェクト(Bean)は、テストクラスのフィールドに @Autowired アノテーションを付与して定義しておきます。
  • テストメソッドの最初で EmployeeDataCsvToDbLoader クラスのインスタンスを生成してから context.getAutowireCapableBeanFactory().applyBeanPostProcessorsAfterInitialization(...) を呼び出して手動で Bean に登録します。手動で登録すると CommandLineRunner#run メソッドは自動で実行されません。
f:id:ksby:20190501011141p:plainf:id:ksby:20190501011213p:plain

sample-cmdapp/src/test/groovy/ksbysample/app/samplecmdapp/EmployeeDataCsvToDbLoaderTest.groovy が新規作成されますので、以下の内容を記述します。

package ksbysample.app.samplecmdapp

import groovy.sql.Sql
import ksbysample.lib.doma2lib.dao.EmployeeDao
import org.modelmapper.ModelMapper
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.ApplicationContext
import spock.lang.Specification

import javax.sql.DataSource

// 通常は spring.profiles.active は IntelliJ IDEA の JUnit の Run/Debug Configuration と build.gradle に定義するが、
// 今回はテストが1つしかないので @SpringBootTest の properties 属性で指定する
@SpringBootTest(properties = ["spring.profiles.active=develop"])
class EmployeeDataCsvToDbLoaderTest extends Specification {

    @Autowired
    private ApplicationContext context

    @Autowired
    private DataSource dataSource

    @Autowired
    private final EmployeeDao employeeDao

    @Autowired
    private final ModelMapper modelMapper

    def sql

    void setup() {
        sql = new Sql(dataSource)
    }

    void cleanup() {
        sql.close()
    }

    def "EmployeeDataCsvToDbLoader.run メソッドを実行すると employee.csv のデータが employee テーブルに登録される"() {
        setup:
        // EmployeeDataCsvToDbLoader クラスは batch.execute=EmployeeDataCsvToDbLoader が指定されていないと Bean として
        // 登録されず run メソッドが自動で実行されない。テストでは手動で Bean に登録することで run メソッドが自動実行されることを
        // 回避する。
        EmployeeDataCsvToDbLoader employeeDataCsvToDbLoader =
                new EmployeeDataCsvToDbLoader(employeeDao, modelMapper)
        employeeDataCsvToDbLoader = (EmployeeDataCsvToDbLoader) context
                .getAutowireCapableBeanFactory()
                .applyBeanPostProcessorsAfterInitialization(employeeDataCsvToDbLoader, "employeeDataCsvToDbLoader")

        // src/test/resources の下の employee.csv の File オブジェクトを取得する
        def url = getClass().getResource("/employee.csv")
        def employeeCsvFile = new File(url.toURI())

        // employee テーブルからテストデータを削除する
        sql.execute("delete from employee where name in ('高橋 蓮', '渡辺 結月')")

        expect:
        employeeDataCsvToDbLoader.run("-csvfile=${employeeCsvFile.absolutePath}")
        def results = sql.rows("select name, age, sex from employee where name in ('高橋 蓮', '渡辺 結月')")
        results == [
                [name: "高橋 蓮", age: 15, sex: "男"],
                [name: "渡辺 結月", age: 24, sex: "女"]
        ]

        cleanup:
        sql.execute("delete from employee where name in ('高橋 蓮', '渡辺 結月')")
    }

}

テストを実行して成功することを確認します。

f:id:ksby:20190501013930p:plain

コマンドラインから実行して動作確認する

employee テーブルにテストデータが登録されていないことを確認した後、

f:id:ksby:20190501014632p:plain

sample-cmdapp-1.0.0-RELEASE.jar が生成されている D:\project-springboot\ksbysample-boot-miscellaneous\gradle-multiprj-doma2lib-cmdwebapp\sample-cmdapp\build\libs に移動してから java -Dspring.profiles.active=develop -Dbatch.execute=EmployeeDataCsvToDbLoader -jar sample-cmdapp-1.0.0-RELEASE.jar -csvfile=D:\project-springboot\ksbysample-boot-miscellaneous\gradle-multiprj-doma2lib-cmdwebapp\sample-cmdapp\src\test\resources\employee.csv コマンドを実行します。

f:id:ksby:20190501015315p:plain f:id:ksby:20190501015409p:plain

employee テーブルを確認するとテストデータが登録されています。

f:id:ksby:20190501015447p:plain

問題なさそうです。

最後に sample-cmdapp プロジェクトのディレクトリ構成を記載します。

f:id:ksby:20190501015848p:plain

sample-cmdapp-1.0.0-RELEASE.jar の中身を見てみると以下のようになっており doma2-lib-1.0.0-RELEASE.jar が lib ディレクトリの下に入っています。

f:id:ksby:20190501053130p:plain

補足

sample-cmdapp でも Flyway は動作している

現在の設定では sample-cmdapp を実行した時も Flyway は動作しています。確認してみます。

docker-compose down コマンドを実行後、docker/mysql/data の下をクリアします。

docker-compose.yml の flyway の設定をコメントアウトした後、

version: '3'

services:
  ..........

#  flyway:
#    image: boxfuse/flyway:${FLYWAY_VERSION}-alpine
#    container_name: flyway
#    environment:
#      - TZ=Asia/Tokyo
#    volumes:
#      - ./doma2-lib/src/main/resources/db/migration:/flyway/sql
#    command: -url=${FLYWAY_URL} -user=${FLYWAY_USER} -password=${FLYWAY_PASSWORD} -connectRetries=60 migrate
#    depends_on:
      - mysql
    # 下の3行は debug 用
    # うまく動かない時はコメントアウトを解除した後、
    # docker exec -it flyway /bin/sh
    # で接続してから
    # flyway <command に記述した文字列>
    # を実行してみる
    #
    # entrypoint: /bin/sh
    # stdin_open: true
    # tty: true

docker-compose up -d コマンドを実行します。

adminer にログインして sampledb を見るとテーブルは何もありませんが、

f:id:ksby:20190501021157p:plain

コマンドラインから java -Dspring.profiles.active=develop -Dbatch.execute=EmployeeDataCsvToDbLoader -jar sample-cmdapp-1.0.0-RELEASE.jar -csvfile=D:\project-springboot\ksbysample-boot-miscellaneous\gradle-multiprj-doma2lib-cmdwebapp\sample-cmdapp\src\test\resources\employee.csv を実行してから、

f:id:ksby:20190501021340p:plain f:id:ksby:20190501021435p:plain

sampledb を確認すると employee と flyway_schema_history の2つのテーブルが作成されており、

f:id:ksby:20190501021538p:plain

employee テーブルにも doma2-lib/src/main/resources/db/migration/V1__create_table.sql の2件と sample-cmdapp/src/test/resources/employee.csv の2件の合計4件のデータが登録されています。

f:id:ksby:20190501021718p:plain

履歴

2019/05/01
初版発行。

Gradle で Multi-project を作成する ( その9 )( doma2lib+cmdapp+webapp編、sample-cmdapp プロジェクトを作成する )

概要

記事一覧はこちらです。

Gradle で Multi-project を作成する ( その8 )( doma2lib+cmdapp+webapp編、doma2-lib プロジェクトを作成する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Doma 2 の Entity、Dao を提供するライブラリ+Spring Boot ベースのコマンドラインアプリケーション+Spring Boot ベースの Web アプリケーションの Multi-project を作成します。
    • 今回は sample-cmdapp プロジェクトを作成します。

参照したサイト・書籍

  1. univocity-parsers/src/test/java/com/univocity/parsers/examples/RoutineExamples.java
    https://github.com/uniVocity/univocity-parsers/blob/master/src/test/java/com/univocity/parsers/examples/RoutineExamples.java

目次

  1. Spring Initializr で sample-cmdapp プロジェクトを作成する
  2. sample-cmdapp の build.gradle を変更する
  3. gradle-multiprj-doma2lib-cmdwebapp の settings.gradle に include 'sample-cmdapp' を追加する
  4. gradle-multiprj-doma2lib-cmdwebapp の build.gradle の configure の適用対象に sample-cmdapp を追加する
  5. CSV ファイルを読み込んで DB のテーブルにデータを登録する機能を実装する
  6. 続く。。。

手順

Spring Initializr で sample-cmdapp プロジェクトを作成する

IntelliJ IDEA から Spring Initializr を利用して sample-cmdapp プロジェクトを作成します。

f:id:ksby:20190430184635p:plainf:id:ksby:20190430184741p:plain
f:id:ksby:20190430184829p:plainf:id:ksby:20190430184904p:plain

※dependencies のダイアログでは何もチェックしません。

f:id:ksby:20190430185000p:plain

作成後 IntelliJ IDEA のウィンドウが開きますが、何もせずに閉じます。

src ディレクトリと build.gradle だけ残して、それ以外のディレクトリファイルは全て削除します。

また sample-cmdapp/src/test/java/ksbysample/app/samplecmdapp/SampleCmdappApplicationTests.java も削除し、sample-cmdapp/src/test/java/ の下はクリアします。

sample-cmdapp の build.gradle を変更する

sample-cmdapp の build.gradle を以下のように変更します。

dependencies {
    implementation("org.springframework.boot:spring-boot-starter")

    implementation project(":doma2-lib")
    implementation("com.univocity:univocity-parsers:2.8.1")
    implementation("args4j:args4j:2.33")
    implementation("com.github.rozidan:modelmapper-spring-boot-starter:1.0.0")
}
  • doma2-lib プロジェクトへの依存関係と、CSV ファイルを処理するためのライブラリである univocity-parsers、コマンドラインオプションを処理するためのライブラリ args4j、ModelMapper を利用するためのライブラリ com.github.rozidan:modelmapper-spring-boot-starter への依存関係を追加します。

gradle-multiprj-doma2lib-cmdwebapp の settings.gradle に include 'sample-cmdapp' を追加する

settings.gradle に include 'sample-cmdapp' を追加します。

rootProject.name = 'gradle-multiprj-doma2lib-cmdwebapp'
include 'doma2-lib'
include 'sample-cmdapp'

gradle-multiprj-doma2lib-cmdwebapp の build.gradle の configure の適用対象に sample-cmdapp を追加する

gradle-multiprj-doma2lib-cmdwebapp の build.gradle の configure の適用対象に sample-cmdapp を追加します。

configure(subprojects.findAll { it.name ==~ /^(doma2-lib|sample-cmdapp)$/ }) {
    apply plugin: "org.springframework.boot"

    dependencyManagement {
        imports {
            mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
        }
    }

    ext {
        jdbcDriver = "mysql:mysql-connector-java:8.0.15"
        domaVersion = "2.24.0"
    }
    
    dependencies {
        testImplementation("org.springframework.boot:spring-boot-starter-test")

        runtimeOnly(jdbcDriver)
        implementation("org.seasar.doma.boot:doma-spring-boot-starter:1.1.1")
        implementation("org.seasar.doma:doma:${domaVersion}")
        implementation("org.flywaydb:flyway-core:5.2.4")
    }

}
  • /^(doma2-lib)$//^(doma2-lib|sample-cmdapp)$/ に変更します。

変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。Gradle Tool Window に sample-cmdapp が表示されます。

f:id:ksby:20190430190713p:plain

また Run/Debug Configuration の Spring Boot の所に doma2-lib と sample-cmdapp の2つの設定が追加されるので、IntelliJ IDEA の画面右下に「Run Dashboard」のダイアログが表示されます。「Show run configurations in Run Dashboard」リンクをクリックして Run Dashboard Tool Window を表示します。

f:id:ksby:20190430190448p:plain f:id:ksby:20190430190559p:plain

Doma2LibApplication の Run/Debug Configuration は不要なので削除します。

clean タスク実行 → Rebuild Project 実行 → build タスク実行を行い、警告・エラーが出ずに BUILD SUCCESSFUL が出力されることを確認します。

f:id:ksby:20190430192039p:plain

CSV ファイルを読み込んで DB のテーブルにデータを登録する機能を実装する

以下の仕様で実装します。

  • コマンドラインから java -jar -Dspring.profiles.active=<develop|product> -jar sample-cmdapp-1.0.0-RELEASE.jar -csvfile=<CSVファイルのパス> で実行します。
  • CSV ファイルは、
    • 文字コードUTF-8
    • 改行コードは CRLF
    • 1行目はヘッダ行。"name","age","sex"
    • 2行目以降はデータ行。
    • データは必ずダブルクォーテーションで囲む。
  • CSV ファイルのデータチェックは行いません。

doma2-lib プロジェクト内の ksbysample.lib パッケージの下も Component Scan の対象にするために sample-cmdapp/src/main/java/ksbysample/app/samplecmdapp/SampleCmdappApplication.java に @ComponentScan アノテーションを追加し、ksbysample.libksbysample.app の2つの package を指定します(ksbysample だけでも良いのですが複数指定するサンプルを作成したかったので2つ指定しています)。

package ksbysample.app.samplecmdapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.AutoConfigurationExcludeFilter;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.TypeExcludeFilter;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;

@SpringBootApplication
@ComponentScan(
        basePackages = {"ksbysample.lib", "ksbysample.app"},
        excludeFilters = {
                @ComponentScan.Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
                @ComponentScan.Filter(type = FilterType.CUSTOM,
                        classes = AutoConfigurationExcludeFilter.class)})
public class SampleCmdappApplication {

    public static void main(String[] args) {
        SpringApplication.run(SampleCmdappApplication.class, args);
    }

}

Lombok を使用したいので gradle-multiprj-doma2lib-cmdwebapp の build.gradle の configure の dependencies に追加します。

..........

configure(subprojects.findAll { it.name ==~ /^(doma2-lib|sample-cmdapp)$/ }) {
    apply plugin: "org.springframework.boot"

    dependencyManagement {
        imports {
            mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
        }
    }

    ext {
        jdbcDriver = "mysql:mysql-connector-java:8.0.15"
        domaVersion = "2.24.0"
    }
    
    dependencies {
        def lombokVersion = "1.18.6"

        testImplementation("org.springframework.boot:spring-boot-starter-test")

        runtimeOnly(jdbcDriver)
        implementation("org.seasar.doma.boot:doma-spring-boot-starter:1.1.1")
        implementation("org.seasar.doma:doma:${domaVersion}")
        implementation("org.flywaydb:flyway-core:5.2.4")

        // for lombok
        // testAnnotationProcessor、testCompileOnly を併記しなくてよいよう configurations で設定している
        annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
        compileOnly("org.projectlombok:lombok:${lombokVersion}")
    }

}
  • configure の dependencies に以下の3行を追加します。
    • def lombokVersion = "1.18.6"
    • annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
    • compileOnly("org.projectlombok:lombok:${lombokVersion}")

変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

CSV ファイルの1レコードのデータをセットするための POJO クラスを実装します。Univocity Parsers で利用します。sample-cmdapp/src/main/java/ksbysample/app/samplecmdapp/EmployeeCsvRecord.java を新規作成し、以下の内容を記述します。

package ksbysample.app.samplecmdapp;

import com.univocity.parsers.annotations.Parsed;
import lombok.Data;

@Data
public class EmployeeCsvRecord {

    @Parsed(field = "name")
    private String name;

    @Parsed(field = "age")
    private Integer age;

    @Parsed(field = "sex")
    private String sex;

}

null のフィールドを insert 文に出力しないようにしたいので、doma2-lib/src/main/java/ksbysample/lib/doma2lib/dao/EmployeeDao.java の insert メソッドの @Insert アノテーションexcludeNull = true を追加します。この設定を行わないと insert 文で update_time のカラムに null がセットされて MySQL 側で日時をセットしてくれません。

@Dao
@ConfigAutowireable
public interface EmployeeDao {

    /**
     * @param id
     * @return the Employee entity
     */
    @Select
    Employee selectById(Integer id);

    /**
     * @param entity
     * @return affected rows
     */
    @Insert(excludeNull = true)
    int insert(Employee entity);

    /**
     * @param entity
     * @return affected rows
     */
    @Update
    int update(Employee entity);

    /**
     * @param entity
     * @return affected rows
     */
    @Delete
    int delete(Employee entity);
}

CSV ファイルを読み込んで DB のテーブルにデータを登録する処理を実行するクラスを実装します。sample-cmdapp/src/main/java/ksbysample/app/samplecmdapp/EmployeeDataCsvToDbLoader.java を新規作成し、以下の内容を記述します。

package ksbysample.app.samplecmdapp;

import com.univocity.parsers.csv.CsvParserSettings;
import com.univocity.parsers.csv.CsvRoutines;
import ksbysample.lib.doma2lib.dao.EmployeeDao;
import ksbysample.lib.doma2lib.entity.Employee;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;
import org.modelmapper.ModelMapper;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.io.BufferedReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;

@Component
@ConditionalOnProperty(value = { "batch.execute" }, havingValue = "EmployeeDataCsvToDbLoader")
public class EmployeeDataCsvToDbLoader implements CommandLineRunner {

    @Option(name = "-csvfile", metaVar = "<path>", usage = "specifies a path to employee csv file")
    private String csvfile;

    private final EmployeeDao employeeDao;

    private final ModelMapper modelMapper;

    public EmployeeDataCsvToDbLoader(EmployeeDao employeeDao
            , ModelMapper modelMapper) {
        this.employeeDao = employeeDao;
        this.modelMapper = modelMapper;
    }

    @Override
    @Transactional
    public void run(String... args) throws Exception {
        // コマンドラインオプションを解析して @Option アノテーションを付加しているフィールドに値を設定する
        CmdLineParser cmdLineParser = new CmdLineParser(this);
        cmdLineParser.parseArgument(args);

        // Univocity Parses で CSV ファイルを読み込むための準備をする
        CsvParserSettings settings = new CsvParserSettings();
        settings.getFormat().setLineSeparator("\r\n");  // 改行コードは CRLF
        settings.setHeaderExtractionEnabled(true);      // ヘッダ行はスキップする
        CsvRoutines routines = new CsvRoutines(settings);

        // CSV ファイルを1行ずつ読み込み employee テーブルに insert する
        try (BufferedReader br = Files.newBufferedReader(Paths.get(this.csvfile), StandardCharsets.UTF_8)) {
            Employee employee = new Employee();
            for (EmployeeCsvRecord employeeCsvRecord : routines.iterate(EmployeeCsvRecord.class, br)) {
                modelMapper.map(employeeCsvRecord, employee);
                employeeDao.insert(employee);
            }
        }
    }

}

clean タスク実行 → Rebuild Project 実行 → build タスク実行を行い、警告・エラーが出ずに BUILD SUCCESSFUL が出力されることを確認します。

f:id:ksby:20190430221527p:plain

続く。。。

長くなったので一旦区切ります。次回は sample-cmdapp のテストの作成と、コマンドラインから実行した動作確認を行います。

履歴

2019/04/30
初版発行。

Gradle で Multi-project を作成する ( その8 )( doma2lib+cmdapp+webapp編、doma2-lib プロジェクトを作成する )

概要

記事一覧はこちらです。

Gradle で Multi-project を作成する ( その7 )( doma2lib+cmdapp+webapp編、Multi-project の設定ファイルと docker-compose.yml を作成する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Doma 2 の Entity、Dao を提供するライブラリ+Spring Boot ベースのコマンドラインアプリケーション+Spring Boot ベースの Web アプリケーションの Multi-project を作成します。
    • 今回は doma2-lib プロジェクトを作成します。

参照したサイト・書籍

  1. Spring Boot 2 Gradle plugin without executable jar
    https://stackoverflow.com/questions/49352837/spring-boot-2-gradle-plugin-without-executable-jar

  2. Spring Boot and multiple external configuration files
    https://stackoverflow.com/questions/25855795/spring-boot-and-multiple-external-configuration-files

  3. I am using a combination of @PropertySource and @ConfigurationProperties but I want to overwrite them with an external properties file
    https://stackoverflow.com/questions/47042237/i-am-using-a-combination-of-propertysource-and-configurationproperties-but-i-w

  4. Java11からMySQL8への接続時に起きた問題の解決
    https://qiita.com/talesleaves/items/d96ba7d74127799b1523

  5. MySQL 8.0は何が優れていて、どこに注意すべきか。データベース専門家が新機能を徹底解説
    https://employment.en-japan.com/engineerhub/entry/2018/09/18/110000

  6. Amazon RDS での MySQL
    https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/CHAP_MySQL.html

目次

  1. gradle-multiprj-doma2lib-cmdwebapp の build.gradle を変更する
  2. gradle-multiprj-doma2lib-cmdwebapp の settings.gradle に include 'doma2-lib' を追加する
  3. doma2-lib プロジェクトから不要なファイルを削除する
  4. doma2-lib の build.gradle を変更する
  5. ksbysample.lib.doma2lib パッケージを作成する
  6. domaGen タスクを実行して employee テーブルの Dao インターフェース、Entity クラスを生成する
  7. db-develop.properties、db-product.properties を作成する
  8. DataSourceConfig クラスを作成する
  9. EmployeeDao インターフェースのテストクラスを作成する
  10. clean タスク実行 → Rebuild Project 実行 → build タスク実行を行う
  11. メモ書き
    1. doma2-lib の build.gradle に bootJar { enabled = false }jar { enabled = true } を記述しないと build は失敗する

手順

gradle-multiprj-doma2lib-cmdwebapp の build.gradle を変更する

サブプロジェクト共通の設定を gradle-multiprj-doma2lib-cmdwebapp の build.gradle に記述します。

buildscript {
    repositories {
        mavenCentral()
        maven { url "https://plugins.gradle.org/m2/" }
        maven { url "https://repo.spring.io/release/" }
    }
    dependencies {
        classpath "io.spring.gradle:dependency-management-plugin:1.0.7.RELEASE"
        classpath "org.springframework.boot:spring-boot-gradle-plugin:2.1.4.RELEASE"
    }
}

allprojects {
    repositories {
        mavenCentral()
    }
}

subprojects {
    group "ksby.ksbysample-boot-miscellaneous"
    version "1.0.0-RELEASE"

    apply plugin: "java"
    apply plugin: "groovy"
    apply plugin: "io.spring.dependency-management"
    apply plugin: "idea"

    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11

    [compileJava, compileTestGroovy, compileTestJava]*.options*.encoding = "UTF-8"
    [compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = ["-Xlint:all,-options,-processing,-path"]

    idea {
        module {
            inheritOutputDirs = false
            outputDir = file("$buildDir/classes/main/")
        }
    }

    configurations {
        // annotationProcessor と testAnnotationProcessor、compileOnly と testCompileOnly を併記不要にする
        testAnnotationProcessor.extendsFrom annotationProcessor
        testImplementation.extendsFrom compileOnly
    }

    dependencyManagement {
        imports {
            mavenBom("org.junit:junit-bom:5.4.2")
        }
    }

    dependencies {
        def assertjVersion = "3.12.2"
        def spockVersion = "1.3-groovy-2.5"

        // for Spock
        testImplementation("org.spockframework:spock-core:${spockVersion}")
        testImplementation("org.spockframework:spock-spring:${spockVersion}")

        // for JUnit 5 + AssertJ
        // junit-jupiter で junit-jupiter-api, junit-jupiter-params, junit-jupiter-engine の3つが依存関係に追加される
        testCompile("org.junit.jupiter:junit-jupiter")
        testRuntime("org.junit.platform:junit-platform-launcher")
        testImplementation("org.assertj:assertj-core:${assertjVersion}")
    }

    def jvmArgsDefault = [
            "-ea",
            "-Dfile.encoding=UTF-8",
            "-Dsun.nio.cs.map=x-windows-iso2022jp/ISO-2022-JP"
    ]
    def jvmArgsAddOpens = [
            "--add-opens=java.base/java.io=ALL-UNNAMED",
            "--add-opens=java.base/java.lang=ALL-UNNAMED",
            "--add-opens=java.base/java.lang.invoke=ALL-UNNAMED",
            "--add-opens=java.base/java.lang.ref=ALL-UNNAMED",
            "--add-opens=java.base/java.lang.reflect=ALL-UNNAMED",
            "--add-opens=java.base/java.net=ALL-UNNAMED",
            "--add-opens=java.base/java.security=ALL-UNNAMED",
            "--add-opens=java.base/java.util=ALL-UNNAMED"
    ]
    def printTestCount = { desc, result ->
        if (!desc.parent) { // will match the outermost suite
            println "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped)"
        }
    }

    task testJUnit4AndSpock(type: Test) {
        jvmArgs = jvmArgsDefault +
                jvmArgsAddOpens

        testLogging {
            afterSuite printTestCount
        }
    }
    test.dependsOn testJUnit4AndSpock
    test {
        jvmArgs = jvmArgsDefault +
                jvmArgsAddOpens

        // for JUnit 5
        useJUnitPlatform()

        testLogging {
            afterSuite printTestCount
        }
    }
}

configure(subprojects.findAll { it.name ==~ /^(doma2-lib)$/ }) {
    apply plugin: "org.springframework.boot"

    dependencyManagement {
        imports {
            mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
        }
    }

    ext {
        jdbcDriver = "mysql:mysql-connector-java:8.0.15"
        domaVersion = "2.24.0"
    }
    
    dependencies {
        testImplementation("org.springframework.boot:spring-boot-starter-test")

        runtimeOnly(jdbcDriver)
        implementation("org.seasar.doma.boot:doma-spring-boot-starter:1.1.1")
        implementation("org.seasar.doma:doma:${domaVersion}")
        implementation("org.flywaydb:flyway-core:5.2.4")
    }

}
  • 今回作成するサブプロジェクトは全て Spring ベースで作成するので subprojects { ... } の中に設定を記述してもよいのですが、Spring ベースでないサブプロジェクトを作成することも考慮して Spring ベースの設定は configure(subprojects.findAll { it.name ==~ /^(...)$/ }) { ... } に記述します。
  • org.seasar.doma.boot:doma-spring-boot-starterorg.seasar.doma:doma:2.16.1 に依存していたので、Doma 2 の最新バージョンである 2.24.0 が入るように別途指定します。

gradle-multiprj-doma2lib-cmdwebapp の settings.gradle に include 'doma2-lib' を追加する

gradle-multiprj-doma2lib-cmdwebapp の settings.gradle に include 'doma2-lib' を追加します。

rootProject.name = 'gradle-multiprj-doma2lib-cmdwebapp'
include 'doma2-lib'

doma2-lib プロジェクトから不要なファイルを削除する

以下のファイルは不要なので削除します。

  • doma2-lib/src/main/java/ksbysample/lib/doma2lib/Doma2LibApplication.java
  • doma2-lib/src/main/resources/application.properties
  • doma2-lib/src/test/java/ksbysample/lib/doma2lib/Doma2LibApplicationTests.java
  • doma2-lib/src/test/java の下のディレクトリはクリアします。

doma2-lib の build.gradle を変更する

doma2-lib の build.gradle に記述します。

// for Doma 2
// JavaクラスとSQLファイルの出力先ディレクトリを同じにする
processResources.destinationDir = compileJava.destinationDir
// コンパイルより前にSQLファイルを出力先ディレクトリにコピーするために依存関係を逆転する
compileJava.dependsOn processResources

configurations {
    // for Doma 2
    domaGenRuntime
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter")

    annotationProcessor("org.seasar.doma:doma:${domaVersion}")
    domaGenRuntime("org.seasar.doma:doma-gen:${domaVersion}")
    domaGenRuntime("${jdbcDriver}")
}

bootJar {
    enabled = false
}
jar {
    enabled = true
}

// for Doma-Gen
task domaGen {
    doLast {
        // まず変更が必要なもの
        def rootPackageName = "ksbysample.lib.doma2lib"
        def daoPackagePath = "src/main/java/ksbysample/lib/doma2lib/dao"
        def dbUrl = "jdbc:mysql://localhost/sampledb"
        def dbUser = "sampledb_user"
        def dbPassword = "xxxxxxxx"
        def tableNamePattern = "employee"
//        def tableNamePattern = ".*"
        // おそらく変更不要なもの
        def importOfComponentAndAutowiredDomaConfig = "org.seasar.doma.boot.ConfigAutowireable"
        def workDirPath = "work"
        def workDaoDirPath = "${workDirPath}/dao"

        // 作業用ディレクトリを削除する
        clearDir("${workDirPath}")

        // 現在の Dao インターフェースのバックアップを取得する
        copy() {
            from "${daoPackagePath}"
            into "${workDaoDirPath}/org"
        }

        // Dao インターフェース、Entity クラスを生成する
        ant.taskdef(resource: "domagentask.properties",
                classpath: configurations.domaGenRuntime.asPath)
        ant.gen(url: "${dbUrl}", user: "${dbUser}", password: "${dbPassword}", tableNamePattern: "${tableNamePattern}") {
            entityConfig(packageName: "${rootPackageName}.entity", useListener: false)
            daoConfig(packageName: "${rootPackageName}.dao")
            sqlConfig()
        }

        // 生成された Dao インターフェースを作業用ディレクトリにコピーし、
        // @ComponentAndAutowiredDomaConfig アノテーションを付加する
        copy() {
            from "${daoPackagePath}"
            into "${workDaoDirPath}/replace"
            filter {
                line ->
                    line.replaceAll("import org.seasar.doma.Dao;", "import ${importOfComponentAndAutowiredDomaConfig};\nimport org.seasar.doma.Dao;")
                            .replaceAll("@Dao", "@Dao\n@ConfigAutowireable")
            }
        }

        // @ComponentAndAutowiredDomaConfig アノテーションを付加した Dao インターフェースを
        // dao パッケージへ戻す
        copy() {
            from "${workDaoDirPath}/replace"
            into "${daoPackagePath}"
        }

        // 元々 dao パッケージ内にあったファイルを元に戻す
        copy() {
            from "${workDaoDirPath}/org"
            into "${daoPackagePath}"
        }

        // 作業用ディレクトリを削除する
        clearDir("${workDirPath}")
    }
}

void clearDir(String dirPath) {
    delete dirPath
}
  • Spring Boot 関連のライブラリに依存するライブラリ系のプロジェクトで build を成功させるために bootJar { enabled = false }jar { enabled = true } を記述します。

更新後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。Gradle Tool Window に doma2-lib が表示されます。

f:id:ksby:20190430112132p:plain

ksbysample.lib.doma2lib パッケージを作成する

doma2-lib/src/main/java の下に ksbysample.lib.doma2lib パッケージを作成します。

domaGen タスクを実行して employee テーブルの Dao インターフェース、Entity クラスを生成する

Gradle Tool Window から domaGen タスクを実行します。

f:id:ksby:20190430112643p:plain

f:id:ksby:20190430113027p:plain (.....途中省略.....) f:id:ksby:20190430113139p:plain

[ant:gen] Loading class com.mysql.jdbc.Driver. This is deprecated. The new driver class is com.mysql.cj.jdbc.Driver. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary. というメッセージが出力されますが、domaframework/doma-gen を見ると mysql の場合は com.mysql.jdbc.Driver を使用するようになっていました。JDBC Driver を別途指定できればいいのですが、方法が分かりませんでした。現時点では deprecated で使用できない訳ではないので、このまま進めることにします。

下の画像の赤文字のファイルが生成されます。

f:id:ksby:20190430113442p:plain

■ksbysample.lib.doma2lib.dao.EmployeeDao インターフェース

package ksbysample.lib.doma2lib.dao;

import ksbysample.lib.doma2lib.entity.Employee;
import org.seasar.doma.boot.ConfigAutowireable;
import org.seasar.doma.Dao;
import org.seasar.doma.Delete;
import org.seasar.doma.Insert;
import org.seasar.doma.Select;
import org.seasar.doma.Update;

/**
 */
@Dao
@ConfigAutowireable
public interface EmployeeDao {

    /**
     * @param id
     * @return the Employee entity
     */
    @Select
    Employee selectById(Integer id);

    /**
     * @param entity
     * @return affected rows
     */
    @Insert
    int insert(Employee entity);

    /**
     * @param entity
     * @return affected rows
     */
    @Update
    int update(Employee entity);

    /**
     * @param entity
     * @return affected rows
     */
    @Delete
    int delete(Employee entity);
}

■ksbysample.lib.doma2lib.entity.Employee クラス

package ksbysample.lib.doma2lib.entity;

import java.time.LocalDateTime;
import org.seasar.doma.Column;
import org.seasar.doma.Entity;
import org.seasar.doma.GeneratedValue;
import org.seasar.doma.GenerationType;
import org.seasar.doma.Id;
import org.seasar.doma.Table;

/**
 * 
 */
@Entity
@Table(name = "employee")
public class Employee {

    /**  */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    Integer id;

    /**  */
    @Column(name = "name")
    String name;

    /**  */
    @Column(name = "age")
    Integer age;

    /**  */
    @Column(name = "sex")
    String sex;

    /**  */
    @Column(name = "update_time")
    LocalDateTime updateTime;

    ..........

}

db-develop.properties、db-product.properties を作成する

src/main/resources の下に db-develop.properties、db-product.properties を新規作成し、以下の内容を記述します。DB の設定はこれらのファイルに記述し、sample-cmdapp、sample-webapp プロジェクト内では設定しません。

■db-develop.properties

doma.dialect=mysql

spring.datasource.hikari.jdbc-url=jdbc:mysql://localhost/sampledb
spring.datasource.hikari.username=sampledb_user
spring.datasource.hikari.password=xxxxxxxx
spring.datasource.hikari.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.leak-detection-threshold=60000
spring.datasource.hikari.register-mbeans=true

■db-product.properties

# 何も記述しないと git に commit できないので、コメントアウトした行を記述する

最後に動作確認する時に spring.profiles.active に指定する値を変更することで適用する設定ファイルを db-develop.properties か db-product.properties かを切り替えることが出来ることを確認するために2つファイルを用意しますが、今は db-product.properties には何も設定しません。

DataSourceConfig クラスを作成する

doma2-lib/src/main/java/ksbysample/lib/doma2lib の下に config パッケージを作成した後、その下に DataSourceConfig.java を新規作成し以下の内容を記述します。

package ksbysample.lib.doma2lib.config;

import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jmx.export.MBeanExporter;

import javax.sql.DataSource;

@Configuration
@PropertySource(value = "classpath:db-${spring.profiles.active}.properties")
public class DataSourceConfig {

    private final MBeanExporter mbeanExporter;

    public DataSourceConfig(@Autowired(required = false) MBeanExporter mbeanExporter) {
        this.mbeanExporter = mbeanExporter;
    }

    @Bean
    @ConfigurationProperties("spring.datasource.hikari")
    public DataSource dataSource() {
        if (mbeanExporter != null) {
            mbeanExporter.addExcludedBean("dataSource");
        }
        return DataSourceBuilder.create()
                .type(HikariDataSource.class)
                .build();
    }

}

EmployeeDao インターフェースのテストクラスを作成する

@SpringBootApplication アノテーションを付与したクラスが1つもないと @SpringBootTest アノテーションを付与したテストが実行できないので、doma2-lib/src/test/main の下に ksbysample/lib/doma2lib パッケージを作成して TestApplication.java を新規作成し、以下の内容を記述します。生成する jar ファイルに TestApplication クラスを含めないようにするために src/test/main の下に作成しています。

package ksbysample.lib.doma2lib;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class TestApplication {

    public static void main(String[] args) {
        SpringApplication.run(TestApplication.class, args);
    }

}

EmployeeDao インターフェースのテストクラスを Spock で作成します。

f:id:ksby:20190430131552p:plainf:id:ksby:20190430131625p:plain

doma2-lib/src/test/groovy/ksbysample/lib/doma2lib/dao/EmployeeDaoTest.groovy が新規作成されますので、以下の内容を記述します。

package ksbysample.lib.doma2lib.dao

import groovy.sql.Sql
import ksbysample.lib.doma2lib.entity.Employee
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import spock.lang.Specification
import spock.lang.Unroll

import javax.sql.DataSource

// 通常は spring.profiles.active は IntelliJ IDEA の JUnit の Run/Debug Configuration と build.gradle に定義するが、
// 今回はテストが1つしかないので @SpringBootTest の properties 属性で指定する
@SpringBootTest(properties = ["spring.profiles.active=develop"])
class EmployeeDaoTest extends Specification {

    static final String TESTDATA_NAME = "木村 太郎"

    @Autowired
    EmployeeDao employeeDao

    @Autowired
    private DataSource dataSource

    def sql

    void setup() {
        sql = new Sql(dataSource)
    }

    void cleanup() {
        sql.close()
    }

    @Unroll
    def "selectById メソッドのテスト(#id --> #name, #age, #sex)"() {
        setup:
        def result = employeeDao.selectById(id)

        expect:
        result.name == name
        result.age == age
        result.sex == sex

        where:
        id || name    | age | sex
        1  || "田中 太郎" | 20  | "男"
        2  || "鈴木 花子" | 18  | "女"
    }

    def "insert メソッドのテスト"() {
        setup:
        sql.execute("delete from employee where name = ${TESTDATA_NAME}")
        Employee employee = new Employee(id: null, name: "${TESTDATA_NAME}", age: 35, sex: "男", updateTime: null)
        employeeDao.insert(employee)

        expect:
        def result = sql.firstRow("select * from employee where name = ${TESTDATA_NAME}")
        result.name == TESTDATA_NAME
        result.age == 35
        result.sex == "男"

        cleanup:
        sql.execute("delete from employee where name = ${TESTDATA_NAME}")
    }

}

テストを実行して成功することを確認します。

f:id:ksby:20190430133242p:plain

clean タスク実行 → Rebuild Project 実行 → build タスク実行を行う

clean タスク実行 → Rebuild Project 実行 → build タスク実行を行い、エラーなしで doma2-lib-1.0.0-RELEASE.jar が生成されることを確認します。

f:id:ksby:20190430141210p:plain (.....途中省略.....) f:id:ksby:20190430141306p:plain

最後に BUILD SUCCESSFUL のメッセージは出力されましたが、途中で javax.net.ssl.SSLException: closing inbound before receiving peer's close_notify のメッセージが何度も出力されました。

原因を Web で検索してみたところ Java11からMySQL8への接続時に起きた問題の解決 という記事がありました。JDK 11 のバグらしいです。

JDBC の URL に sslMode=DISABLED を付けて SSL を使用しなければメッセージが出ないようにできるそうなので、今回はそれで対応します。また HikariCP を使用していると文字化けするという情報も記載されていたので characterEncoding=utf8 も付けることにします。

doma2-lib/src/main/resources/db-develop.properties を以下のように変更します。

doma.dialect=mysql

spring.datasource.hikari.jdbc-url=jdbc:mysql://localhost/sampledb?sslMode=DISABLED&characterEncoding=utf8
spring.datasource.hikari.username=sampledb_user
spring.datasource.hikari.password=xxxxxxxx
spring.datasource.hikari.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.leak-detection-threshold=60000
spring.datasource.hikari.register-mbeans=true

再度 clean タスク実行 → Rebuild Project 実行 → build タスク実行を行うと、今度は警告・エラーのメッセージは表示されずに BUILD SUCCESSFUL のメッセージが出力されました。

f:id:ksby:20190430142001p:plain

doma2-lib/build/libs の下に doma2-lib-1.0.0-RELEASE.jar が生成されています。現在のディレクトリ構成は以下のようになります。

f:id:ksby:20190430142333p:plain f:id:ksby:20190430142425p:plain

doma2-lib-1.0.0-RELEASE.jar の中身を見てみると以下のようになっています。

f:id:ksby:20190430142618p:plain

ここまでの感想ですが JDK 11+MySQL 8 の組み合わせはまだ安定していないようですね。。。 本番で使うなら MySQL は 5.7 を選択した方が良さそうです。

メモ書き

doma2-lib の build.gradle に bootJar { enabled = false }jar { enabled = true } を記述しないと build は失敗する

doma2-lib の build.gradle には bootJar { enabled = false }jar { enabled = true } を記述しましたが、これらを記述しないと bootJar タスクで実行可能 Jar を生成しようとしても Main class が存在しないため Main class name has not been configured and it could not be resolved のエラーメッセージが表示されて build タスクが失敗します。

f:id:ksby:20190430154132p:plain

履歴

2019/04/30
初版発行。

Gradle で Multi-project を作成する ( その7 )( doma2lib+cmdapp+webapp編、Multi-project の設定ファイルと docker-compose.yml を作成する )

概要

記事一覧はこちらです。

Gradle で Multi-project を作成する ( その6 )( Multi-project は settings.gradle に include を書くだけでもよいのでは? ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Doma 2 の Entity、Dao を提供するライブラリ+Spring Boot ベースのコマンドラインアプリケーション+Spring Boot ベースの Web アプリケーションの Multi-project を作成します。
    • Web とバッチで Doma 2 の Entity と Dao を共有したい場合を想定してみました。
    • 6~8回程度で書ければいいかな。。。

参照したサイト・書籍

  1. dockerhub - mysql https://hub.docker.com/_/mysql

  2. dockerhub - adminer https://hub.docker.com/_/adminer/

  3. MySQL8.0ではGRANT構文でユーザを作成できない
    https://www7390uo.sakura.ne.jp/wordpress/archives/456

  4. MySQL Innovation Day Tokyo で MySQL 8 の文字コードについて話した
    https://tmtms.hatenablog.com/entry/201805/mysql-innovation-day-tokyo

  5. ユーザーを作成する(CREATE USER文)
    https://www.dbonline.jp/mysql/user/index1.html

  6. MySQL 8.0 の AUTO_INCREMENT について
    https://www.s-style.co.jp/blog/2018/08/2284/

  7. MySQL入門】CREATE TABLE文でテーブルを作成する方法
    https://www.sejuku.net/blog/82708

  8. MySQL data types
    http://zetcode.com/databases/mysqltutorial/datatypes/

目次

  1. 方針
  2. gradle-multiprj-doma2lib-cmdwebapp ディレクトリ作成+Gradle Wrapper コピー+Gradle 5.4.1 バージョンアップ+gradlew init
  3. IntelliJ IDEA で gradle-multiprj-doma2lib-cmdwebapp プロジェクトをオープンする
  4. Spring Initializr で doma2-lib プロジェクトを作成する
  5. MySQL と Flyway を起動するための docmer-compose.yml を作成した後、サーバを起動してデータベースとテーブルを作成する

手順

方針

  • 以下のディレクトリ構成(必要最低限のディレクトリ・ファイルのみ記述しています)の Multi-project を作成します。
gradle-multiprj-doma2lib-cmdwebapp
├ doma2-lib    <-- Doma 2 の Entity、Dao を提供するライブラリの Project
│ ├ src
│ │ ├ main
│ │ │ └ java
│ │ │   └ ksbysample
│ │ │     └ lib
│ │ │       └ doma2lib
│ │ │         ├ dao
│ │ │         └ entity
│ │ └ resources
│ │   ├ db
│ │   │ ├ init
│ │   │ │ └ create_database.sql
│ │   │ └ migration
│ │   │   └ V1__create_table.sql    <-- Flyway 用 SQL ファイル
│ │   ├ db-develop.properties    <-- develop profile 用 DB 設定ファイル
│ │   └ db-product.properties    <-- product profile 用 DB 設定ファイル 
│ └ build.gradle
├ sample-cmdapp    <-- Spring Boot ベースのコマンドラインアプリケーションの Project
│ ├ src
│ │ ├ main
│ │ │ └ java
│ │ │   └ ksbysample
│ │ │     └ app
│ │ │       └ samplecmdapp
│ │ └ resources
│ └ build.gradle
├ sample-webapp    <-- Spring Boot ベースの Web アプリケーションの Project
│ ├ src
│ │ ├ main
│ │ │ └ java
│ │ │   └ ksbysample
│ │ │     └ app
│ │ │       └ samplewebapp
│ │ └ resources
│ └ build.gradle
├ build.gradle
└ settings.gradle
  • 今回は全てのプロジェクトを Spring Initializr で作成します。
  • develop と product の2つの Profile を作成・使用します。
  • テスティングフレームワークは全ての Project で JUnit 5 と Spock を使用できるようにします。
  • checkstyle, spotbugs, pmd, error-prone は導入しません。
  • Spring Boot ベースの Web アプリケーションでは 8080番ポートを使用します。
  • Spring Boot ベースのアプリケーションの build.gradle の共通の設定は各サブプロジェクトの build.gradle ではなく gradle-multiprj-doma2lib-cmdwebapp の build.gradle に記述します。
  • DB は MySQL のバージョン 8 を docker-compose で起動して利用します(これまで使ったことがなかったので)。
  • DB へのデータ投入には Flyway を使用します。Flyway に必要なファイルは doma2-lib プロジェクトの下に配置します。
  • コマンドラインアプリケーションでは CSV ファイルを読み込んで DB のテーブルにデータを登録し、Web アプリケーションでは DB のテーブルに登録されたデータを取得して表示します。
  • 最初に doma2-lib-1.0.0-RELEASE.jar を sample-cmdapp-1.0.0-RELEASE.jar、sample-webapp-1.0.0-RELEASE.jar の中に入れる方式(JarLauncher による起動)で一通り書いてから、doma2-lib-1.0.0-RELEASE.jar を外に出す方式(PropertiesLauncher による起動)を書くつもりです。
  • draw.io で全体図を描いてみました。

f:id:ksby:20190422070428p:plain

gradle-multiprj-doma2lib-cmdwebapp ディレクトリ作成+Gradle Wrapper コピー+Gradle 5.4.1 バージョンアップ+gradlew init

ksby/ksbysample-boot-miscellaneous の repository を checkout している D:\project-springboot\ksbysample-boot-miscellaneous の下に gradle-multiprj-doma2lib-cmdwebapp ディレクトリを作成します。

f:id:ksby:20190429010538p:plain

gradle-multiprj-lib-webapp2 プロジェクトから Gradle Wrapper のファイルをコピーします。

f:id:ksby:20190429010921p:plain

コマンドプロンプトから gradlew wrapper --gradle-version=5.4.1gradlew --versiongradlew wrapper コマンドを実行して Gradle を 5.4 → 5.4.1 へバージョンアップします。

f:id:ksby:20190429011306p:plain

gradle/wrapper/gradle-wrapper.properties を見ると 5.4.1 になっています。

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

gradlew init コマンドを実行します。選択肢は 1: basic1: groovy を選択し、Project name は何も入力せずに Enter キーを押します。

f:id:ksby:20190429011701p:plain f:id:ksby:20190429011756p:plain

IntelliJ IDEA で gradle-multiprj-doma2lib-cmdwebapp プロジェクトをオープンする

gradle-multiprj-doma2lib-cmdwebapp プロジェクトをオープンします。

f:id:ksby:20190429140849p:plain

f:id:ksby:20190429141235p:plainf:id:ksby:20190429141319p:plain

Spring Initializr で doma2-lib プロジェクトを作成する

Flyway で使用する SQL ファイルを doma2-lib プロジェクトの下に配置するので、最初に IntelliJ IDEA から Spring Initializr を利用して doma2-lib プロジェクトを作成しておきます。

f:id:ksby:20190429194516p:plainf:id:ksby:20190429194722p:plain
f:id:ksby:20190429194808p:plainf:id:ksby:20190429194858p:plain

※dependencies のダイアログでは何もチェックしません。

f:id:ksby:20190429195017p:plain

作成後 IntelliJ IDEA のウィンドウが開きますが、何もせずに閉じます。

src ディレクトリと build.gradle だけ残して、それ以外のディレクトリファイルは全て削除します。gradle-multiprj-doma2lib-cmdwebapp プロジェクトの settings.gradle にはまだ include 文は追加しません。

MySQL と Flyway を起動するための docmer-compose.yml を作成した後、サーバを起動してデータベースとテーブルを作成する

MySQL のデータファイルをローカルディレクトリに保存するために docker/mysql/data を作成します。

doma2-lib/src/main/resources の下に db/init、db/migration の2つのディレクトリを作成します。

doma2-lib/src/main/resources/db/init の下に create_database.sql を新規作成し、以下の内容を記述します。

create database if not exists sampledb character set utf8mb4 collate utf8mb4_ja_0900_as_cs_ks;

create user 'sampledb_user'@'%' identified by 'xxxxxxxx';
grant all privileges ON sampledb.* to 'sampledb_user'@'%' with grant option;
flush privileges;

doma2-lib/src/main/resources/db/migration の下に V1__create_table.sql を新規作成し、以下の内容を記述します。

create table employee (
    id int(11) not null auto_increment,
    name varchar(128) not null,
    age int(3) not null,
    sex enum('男', '女'),
    update_time datetime default current_timestamp,
    primary key (id)
);
insert into employee (name, age, sex) values ('田中 太郎', 20, '男');
insert into employee (name, age, sex) values ('鈴木 花子', 18, '女');

ルートディレクトリの下に .env を新規作成し、以下の内容を記述します。

MYSQL_VERSION=8.0.16
ADMINER_VERSION=4.7.1

FLYWAY_VERSION=5.2.4
FLYWAY_URL=jdbc:mysql://mysql/sampledb
FLYWAY_USER=sampledb_user
FLYWAY_PASSWORD=xxxxxxxx

ルートディレクトリの下に docker-compose.yml を新規作成し、以下の内容を記述します。

version: '3'

services:
  # 起動したコンテナに /bin/sh でアクセスする場合には以下のコマンドを実行する
  # docker exec -it mysql bash
  mysql:
    image: mysql:${MYSQL_VERSION}
    container_name: mysql
    command: --default-authentication-plugin=mysql_native_password
    restart: always
    ports:
      - 3306:3306
    environment:
      - TZ=Asia/Tokyo
      - MYSQL_ROOT_PASSWORD=xxxxxxxx
    volumes:
      - ./docker/mysql/data:/var/lib/mysql
      - ./doma2-lib/src/main/resources/db/init/create_database.sql:/docker-entrypoint-initdb.d/create_database.sql

  # URL
  # http://localhost:9080/
  adminer:
    image: adminer:${ADMINER_VERSION}
    container_name: adminer
    restart: always
    ports:
      - 9080:8080
    environment:
      - TZ=Asia/Tokyo
      - ADMINER_DEFAULT_SERVER=mysql

  flyway:
    image: boxfuse/flyway:${FLYWAY_VERSION}-alpine
    container_name: flyway
    environment:
      - TZ=Asia/Tokyo
    volumes:
      - ./doma2-lib/src/main/resources/db/migration:/flyway/sql
    command: -url=${FLYWAY_URL} -user=${FLYWAY_USER} -password=${FLYWAY_PASSWORD} -connectRetries=60 migrate
    depends_on:
      - mysql
    # 下の3行は debug 用
    # うまく動かない時はコメントアウトを解除した後、
    # docker exec -it flyway /bin/sh
    # で接続してから
    # flyway <command に記述した文字列>
    # を実行してみる
    #
    # entrypoint: /bin/sh
    # stdin_open: true
    # tty: true

これで必要なファイルを一通り作成しましたので、コマンドラインから docker-compose up -d コマンドを実行します。

f:id:ksby:20190429221827p:plain f:id:ksby:20190429222726p:plain

http://localhost:9080/ にアクセスして sampledb が作成されていること、emplayee テーブルにデータが登録されていることを確認します。

f:id:ksby:20190429222025p:plain f:id:ksby:20190430135340p:plain f:id:ksby:20190430111405p:plain f:id:ksby:20190430111441p:plain

履歴

2019/04/29
初版発行。
2019/04/30
* 「参照したサイト・書籍」に MySQL data types を追加した。
* V1__create_table.sql の create table employee 文に age, sex, update_time のカラムを追加し、投入するデータを日本語のものに変更した。
* create database の collate を utf8mb4_0900_as_cs → utf8mb4_ja_0900_as_cs_ks に変更した。

Gradle で Multi-project を作成する ( 番外編 )( Spring Actuator を利用してアプリ起動時にメールサーバに接続できない場合には起動を中断させる )

概要

記事一覧はこちらです。

本編とは全く関係ありません。。。

何となく思いついたことで Spring Actuator を入れると DB サーバやメールサーバの UP/DOWN を検知できますが、起動時にサーバに接続できなかったら起動を中断させることができたりするのかな?、と思って調べた時の内容です。

結論としては HealthChecker の実装に依存しますが、メールサーバは可能でした。

参照したサイト・書籍

  1. Programmatically shut down Spring Boot application
    https://stackoverflow.com/questions/22944144/programmatically-shut-down-spring-boot-application

目次

  1. 動作確認のための demo プロジェクトを作成する
  2. Spring Actuator でメールサーバをチェックするよう設定する
  3. メールサーバを起動せずに demo アプリを起動してみる
  4. 起動時にメールサーバに接続できなかったら起動を中断するための CustomMailHealthIndicator クラスを作成する
  5. 動作確認

手順

動作確認のための demo プロジェクトを作成する

IntelliJ IDEA から Spring Initializr を利用して demo プロジェクトを作成します。

f:id:ksby:20190426070614p:plainf:id:ksby:20190426070709p:plain
f:id:ksby:20190426070819p:plainf:id:ksby:20190426070854p:plain
f:id:ksby:20190426070931p:plainf:id:ksby:20190426071002p:plain

※DevTools、Web、Mail、Actuator をチェックします。

f:id:ksby:20190426071139p:plainf:id:ksby:20190426071356p:plain

IntelliJ IDEA のメイン画面が表示されたら、lombok@Slf4j アノテーションを使用したいので build.gradle の dependencies に lombok の依存関係を追加します。

plugins {
    id 'org.springframework.boot' version '2.1.4.RELEASE'
    id 'java'
}

apply plugin: 'io.spring.dependency-management'

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
    // annotationProcessor と testAnnotationProcessor、compileOnly と testCompileOnly を併記不要にする
    testAnnotationProcessor.extendsFrom annotationProcessor
    testImplementation.extendsFrom compileOnly
}

repositories {
    mavenCentral()
}

dependencies {
    def lombokVersion = "1.18.6"

    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-mail'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    runtimeOnly 'org.springframework.boot:spring-boot-devtools'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    // for lombok
    // testAnnotationProcessor、testCompileOnly を併記しなくてよいよう configurations で設定している
    annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
    compileOnly("org.projectlombok:lombok:${lombokVersion}")
}

変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

Spring Actuator でメールサーバをチェックするよう設定する

Spring Actuator でメールサーバの health チェックを行うために application.properties に spring.mail.host=localhost の設定を追加します。

spring.mail.host=localhost

メールサーバを起動せずに demo アプリを起動してみる

メールサーバを起動しない状態で demo アプリを起動すると、Spring Actuator の MailHealthIndicator クラスから警告ログが出力されますがアプリは終了せず起動したままです。

f:id:ksby:20190426072646p:plain f:id:ksby:20190426072804p:plain

Health チェックの画面では mail が黄色アイコンで表示されています。

f:id:ksby:20190426072941p:plain

起動時にメールサーバに接続できなかったら起動を中断するための CustomMailHealthIndicator クラスを作成する

警告ログを出力している org.springframework.boot.actuate.mail.MailHealthIndicator クラスを見ると以下のように実装されています。

f:id:ksby:20190426080800p:plain

実際に health チェックしているのが doHealthCheck メソッドで、this.mailSender.testConnection(); で接続してみて例外が throw されなければ builder.up(); を呼び出してステータスを UP に変更する、という実装でした。

MailHealthIndicator クラスには @Component 等のアノテーションが付与されていないので Bean を定義しているクラスを探したところ、org.springframework.boot.actuate.autoconfigure.mail.MailHealthIndicatorAutoConfiguration クラスで定義されていました。

f:id:ksby:20190427015400p:plain

mailHealthIndicator という名前の Bean が存在しない時だけ MailHealthIndicatorAutoConfiguration クラスで Bean を生成しています。

MailHealthIndicator クラスを継承して CustomMailHealthIndicator クラスを作成し、一番最初の health チェックで例外が throw されたら Web アプリを強制終了させるようにしてみます。src/main/java/com/example/demo/CustomMailHealthIndicator.java クラスを新規作成した後、以下の内容を記述します。

package com.example.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ExitCodeGenerator;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.mail.MailHealthIndicator;
import org.springframework.context.ApplicationContext;
import org.springframework.mail.javamail.JavaMailSenderImpl;

import java.util.concurrent.atomic.AtomicBoolean;

@Slf4j
@Component("mailHealthIndicator")
public class CustomMailHealthIndicator extends MailHealthIndicator {

    private final ApplicationContext context;

    private final AtomicBoolean isFirstTime = new AtomicBoolean(true);

    public CustomMailHealthIndicator(JavaMailSenderImpl mailSender, ApplicationContext context) {
        super(mailSender);
        this.context = context;
    }

    @Override
    protected void doHealthCheck(Health.Builder builder) throws Exception {
        try {
            super.doHealthCheck(builder);
        } catch (Exception e) {
            // 一番最初の health チェックでメールサーバに接続できなかった時にはアプリを強制終了させる
            if (isFirstTime.get()) {
                log.error("メールサーバが起動していないので強制終了します", e);
                int exitCode = SpringApplication.exit(context, (ExitCodeGenerator) () -> 1);
                System.exit(exitCode);
            }
            throw e;
        } finally {
            isFirstTime.compareAndExchange(true, false);
        }
    }

}

動作確認

先程と同様にメールサーバを起動しない状態で demo アプリを起動すると、最後に Process finished with exit code 1 のメッセージが出力されてアプリが終了しました。

f:id:ksby:20190427080959p:plain f:id:ksby:20190427081114p:plain

メールサーバ(smtp4dev)を起動してから demo アプリを起動すると、アプリは終了せずに起動したままとなり、

f:id:ksby:20190427081344p:plain

メールサーバを終了させると、health チェックのエラーログが出力されますがアプリは終了せずに起動したままでした。

f:id:ksby:20190427081532p:plain

想定通りの動きです。

履歴

2019/04/27
初版発行。