技術スタック¶
本記事シリーズで使用する技術スタックを定義します。
バックエンド¶
| カテゴリ | 技術 | バージョン |
|---|---|---|
| 言語 | Java | 25 |
| フレームワーク | Spring Boot | 4.0.0 |
| ORM | MyBatis | 4.0.0 |
| データベース | PostgreSQL | 16 |
| マイグレーション | Flyway | - |
| テスト | JUnit 5 + TestContainers | - |
| 品質管理 | JaCoCo, Checkstyle, PMD, SpotBugs | - |
フロントエンド¶
| カテゴリ | 技術 | バージョン |
|---|---|---|
| 言語 | TypeScript | 5.x |
| フレームワーク | React | 19.x |
| ビルドツール | Vite | 6.x |
| スタイリング | Tailwind CSS | 4.x |
| 状態管理 | TanStack Query | 5.x |
| HTTP クライアント | Axios | - |
| テスト | Vitest + Testing Library | - |
インフラストラクチャ¶
| カテゴリ | 技術 |
|---|---|
| コンテナ | Docker / Docker Compose |
| CI/CD | GitHub Actions |
命名規則¶
| 要素 | 言語 | 例 |
|---|---|---|
| テーブル名 | 日本語 | "商品マスタ", "受注データ" |
| カラム名 | 日本語 | "商品コード", "商品名" |
| ENUM 型 | 日本語 | 商品区分, 取引先区分 |
| ENUM 値 | 日本語 | '商品', '顧客' |
| Java クラス名 | 英語 | Product, Order, Customer |
| Java フィールド名 | 英語 | productCode, productName |
| MyBatis resultMap | 日本語 ↔ 英語 | column="商品コード" property="productCode" |
| TypeScript 型名 | 英語 | Product, Order, Customer |
| TypeScript プロパティ名 | 英語 | productCode, productName |
プロジェクト構成¶
各サブシステムは以下のディレクトリ構成に従います。
apps/{system}/
├── backend/
│ ├── build.gradle.kts # Gradle ビルド設定(Kotlin DSL)
│ ├── settings.gradle.kts # Gradle 設定
│ ├── config/ # 品質管理ツール設定
│ │ ├── checkstyle/
│ │ │ └── checkstyle.xml
│ │ ├── pmd/
│ │ │ └── ruleset.xml
│ │ └── spotbugs/
│ │ └── exclude.xml
│ │
│ └── src/
│ ├── main/
│ │ ├── java/com/example/sms/
│ │ │ │
│ │ │ ├── domain/ # ドメイン層
│ │ │ │ ├── model/ # ドメインモデル
│ │ │ │ │ ├── master/ # マスタエンティティ
│ │ │ │ │ └── transaction/ # トランザクションエンティティ
│ │ │ │ ├── type/ # 基本型(通貨、単位、数量等)
│ │ │ │ └── exception/ # ドメイン例外
│ │ │ │
│ │ │ ├── application/ # アプリケーション層
│ │ │ │ ├── port/
│ │ │ │ │ ├── in/ # Input Port(UseCase)
│ │ │ │ │ └── out/ # Output Port(Repository)
│ │ │ │ └── service/ # Application Service
│ │ │ │
│ │ │ ├── infrastructure/ # インフラストラクチャ層
│ │ │ │ ├── in/ # Input Adapter
│ │ │ │ │ └── rest/ # REST API Controller
│ │ │ │ ├── out/ # Output Adapter
│ │ │ │ │ └── persistence/ # 永続化(MyBatis Mapper)
│ │ │ │ └── config/ # 設定クラス
│ │ │ │
│ │ │ └── Application.java # メインクラス
│ │ │
│ │ └── resources/
│ │ ├── application.yml # アプリケーション設定
│ │ ├── db/migration/ # Flyway マイグレーション
│ │ └── mapper/ # MyBatis XML マッパー
│ │
│ └── test/
│ ├── java/com/example/sms/
│ │ ├── testsetup/ # テスト基盤クラス
│ │ │ └── BaseIntegrationTest.java
│ │ ├── domain/ # ドメイン層テスト
│ │ ├── application/ # アプリケーション層テスト
│ │ └── infrastructure/ # インフラ層テスト
│ │
│ └── resources/
│ └── application-test.yml
│
├── frontend/ # フロントエンド
│ └── src/
│ ├── components/ # UI コンポーネント
│ ├── pages/ # ページコンポーネント
│ ├── hooks/ # カスタムフック
│ ├── services/ # API クライアント
│ └── types/ # 型定義
│
└── docker-compose.yml # Docker 構成
サブシステム一覧¶
| システム | ディレクトリ | 説明 |
|---|---|---|
| 販売管理システム | apps/sms |
Sales Management System |
| 財務会計システム | apps/fas |
Financial Accounting System |
| 生産管理システム | apps/pms |
Production Management System |
環境構築¶
TDD でデータベース設計を進めるための開発環境を構築します。テスト駆動開発のゴールは 動作するきれいなコード ですが、それを実現するためには ソフトウェア開発の三種の神器 が必要です。
今日のソフトウェア開発の世界において絶対になければならない3つの技術的な柱があります。 三本柱と言ったり、三種の神器と言ったりしていますが、それらは
- バージョン管理
- テスティング
- 自動化
の3つです。
前提条件¶
以下のツールがインストールされていることを確認してください。
- Java 21 以上(推奨:Java 25)
- Gradle 8.x 以上(推奨:Gradle Wrapper 使用)
- Docker & Docker Compose
- Git
- Node.js 22.x 以上
バックエンドのセットアップ¶
1. Gradle プロジェクトの初期化¶
cd apps/sms/backend
mkdir -p src/main/java/com/example/sms
mkdir -p src/main/resources/db/migration
mkdir -p src/test/java/com/example/sms/testsetup
mkdir -p src/test/resources
mkdir -p config/checkstyle config/pmd config/spotbugs
2. build.gradle.kts の設定¶
build.gradle.kts(Kotlin DSL)を以下のように作成します。
plugins {
java
jacoco
checkstyle
pmd
id("org.springframework.boot") version "4.0.0"
id("io.spring.dependency-management") version "1.1.7"
id("com.github.spotbugs") version "6.0.27"
}
group = "com.example.sms"
version = "0.0.1-SNAPSHOT"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
}
}
repositories {
mavenCentral()
}
// バージョン管理
val mybatisVersion = "4.0.0"
val testcontainersVersion = "1.20.4"
dependencies {
// === implementation ===
// Spring Boot Starters
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-jdbc")
// Database
implementation("org.mybatis.spring.boot:mybatis-spring-boot-starter:$mybatisVersion")
implementation("org.springframework.boot:spring-boot-starter-flyway")
implementation("org.flywaydb:flyway-database-postgresql")
// === runtimeOnly ===
runtimeOnly("org.postgresql:postgresql")
// === compileOnly ===
compileOnly("org.projectlombok:lombok")
// === annotationProcessor ===
annotationProcessor("org.projectlombok:lombok")
// === testImplementation ===
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.boot:spring-boot-testcontainers")
testImplementation("org.mybatis.spring.boot:mybatis-spring-boot-starter-test:$mybatisVersion")
testImplementation(platform("org.testcontainers:testcontainers-bom:$testcontainersVersion"))
testImplementation("org.testcontainers:junit-jupiter")
testImplementation("org.testcontainers:postgresql")
// === testCompileOnly ===
testCompileOnly("org.projectlombok:lombok")
// === testAnnotationProcessor ===
testAnnotationProcessor("org.projectlombok:lombok")
// === testRuntimeOnly ===
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.withType<Test> {
useJUnitPlatform()
finalizedBy(tasks.jacocoTestReport)
// TestContainers の共有を有効化
jvmArgs("-Dtestcontainers.reuse.enable=true")
// テストを順次実行(並列実行しない)
maxParallelForks = 1
}
// JaCoCo
jacoco {
toolVersion = "0.8.14" // Java 25 support
}
tasks.jacocoTestReport {
dependsOn(tasks.test)
reports {
xml.required = true
html.required = true
}
classDirectories.setFrom(
files(classDirectories.files.map {
fileTree(it) {
exclude(
"**/Application.class",
"**/Application$*.class"
)
}
})
)
}
// Checkstyle
checkstyle {
toolVersion = "10.20.2"
configFile = file("${rootDir}/config/checkstyle/checkstyle.xml")
isIgnoreFailures = false
}
// SpotBugs (Java 25 対応: 4.9.7+)
spotbugs {
ignoreFailures = false
excludeFilter = file("${rootDir}/config/spotbugs/exclude.xml")
toolVersion = "4.9.8"
}
tasks.withType<com.github.spotbugs.snom.SpotBugsTask> {
reports.create("html") {
required = true
}
reports.create("xml") {
required = true
}
}
// PMD (Java 25 対応: 7.16.0+)
pmd {
toolVersion = "7.16.0"
isConsoleOutput = true
ruleSetFiles = files("${rootDir}/config/pmd/ruleset.xml")
ruleSets = listOf()
isIgnoreFailures = false
}
// カスタムタスク: TDD用の継続的テスト実行
tasks.register<Test>("tdd") {
description = "Run tests in TDD mode (always executes)"
group = "verification"
useJUnitPlatform()
testLogging {
events("passed", "skipped", "failed")
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
}
outputs.upToDateWhen { false }
}
// カスタムタスク: 品質チェック全実行
tasks.register("qualityCheck") {
description = "Run all quality checks (Checkstyle, PMD, SpotBugs)"
group = "verification"
dependsOn("checkstyleMain", "checkstyleTest", "pmdMain", "pmdTest", "spotbugsMain", "spotbugsTest")
}
// カスタムタスク: すべてのテストと品質チェックを実行
tasks.register("fullCheck") {
description = "Run all tests and quality checks"
group = "verification"
dependsOn("test", "qualityCheck", "jacocoTestReport")
}
3. settings.gradle.kts の設定¶
rootProject.name = "sms-backend"
4. gradle-wrapper.properties の設定¶
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
5. Docker Compose のセットアップ¶
docker-compose.yml を作成します。
services:
postgres:
image: postgres:16-alpine
container_name: sms-postgres
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
POSTGRES_DB: ${POSTGRES_DB:-sms}
TZ: 'Asia/Tokyo'
ports:
- "${POSTGRES_PORT:-5432}:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:
6. Flyway マイグレーションのセットアップ¶
src/main/resources/db/migration/V001__create_enum_types.sql を作成します。
-- 商品区分
CREATE TYPE 商品区分 AS ENUM ('商品', '製品', 'サービス', '諸口');
-- 取引先区分
CREATE TYPE 取引先区分 AS ENUM ('顧客', '仕入先', '両方');
-- 税区分
CREATE TYPE 税区分 AS ENUM ('外税', '内税', '非課税');
-- 請求区分
CREATE TYPE 請求区分 AS ENUM ('都度', '締め');
-- 支払方法
CREATE TYPE 支払方法 AS ENUM ('現金', '振込', '手形', '小切手', 'その他');
7. TestContainers のセットアップ¶
テスト用のデータベースコンテナを管理する基盤クラスを作成します。
まず、TestContainers の設定クラスを作成します。
src/test/java/com/example/sms/TestcontainersConfiguration.java:
package com.example.sms;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.utility.DockerImageName;
/**
* TestContainers 設定クラス.
* PostgreSQL コンテナを Spring Bean として定義し、
* ServiceConnection で自動的にデータソースを設定する。
*/
@TestConfiguration(proxyBeanMethods = false)
public class TestcontainersConfiguration {
@Bean
@ServiceConnection
PostgreSQLContainer<?> postgresContainer() {
return new PostgreSQLContainer<>(DockerImageName.parse("postgres:16-alpine"));
}
}
次に、統合テストの基底クラスを作成します。
src/test/java/com/example/sms/testsetup/BaseIntegrationTest.java:
package com.example.sms.testsetup;
import com.example.sms.TestcontainersConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
/**
* 統合テストの基底クラス。
* TestContainers を使用して PostgreSQL コンテナを起動し、
* Spring Boot の @ServiceConnection で自動的にデータソースを設定する。
* Flyway マイグレーションが自動実行される。
*/
@SpringBootTest
@Import(TestcontainersConfiguration.class)
@org.springframework.test.context.ActiveProfiles("test")
@SuppressWarnings({"PMD.AbstractClassWithoutAbstractMethod"})
public abstract class BaseIntegrationTest {
/**
* 継承のみを許可するための protected コンストラクタ。
*/
protected BaseIntegrationTest() {
// TestContainers の基底クラスのため空実装
}
}
src/test/resources/application-test.yml:
spring:
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
logging:
level:
org.flywaydb: DEBUG
8. 最初のテスト:データベース接続確認¶
src/test/java/com/example/sms/DatabaseConnectionTest.java:
package com.example.sms;
import com.example.sms.testsetup.BaseIntegrationTest;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("データベース接続")
class DatabaseConnectionTest extends BaseIntegrationTest {
@Autowired
private DataSource dataSource;
@Test
@DisplayName("PostgreSQLに接続できる")
void canConnectToPostgres() throws Exception {
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT 1")) {
assertThat(rs.next()).isTrue();
assertThat(rs.getInt(1)).isEqualTo(1);
}
}
@Test
@DisplayName("商品区分ENUMが作成されている")
void productTypeEnumExists() throws Exception {
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(
"SELECT unnest(enum_range(NULL::商品区分))::text")) {
assertThat(rs.next()).isTrue();
assertThat(rs.getString(1)).isEqualTo("商品");
}
}
@Test
@DisplayName("取引先区分ENUMが作成されている")
void partnerTypeEnumExists() throws Exception {
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(
"SELECT unnest(enum_range(NULL::取引先区分))::text")) {
assertThat(rs.next()).isTrue();
assertThat(rs.getString(1)).isEqualTo("顧客");
}
}
@Test
@DisplayName("税区分ENUMが作成されている")
void taxTypeEnumExists() throws Exception {
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(
"SELECT unnest(enum_range(NULL::税区分))::text")) {
assertThat(rs.next()).isTrue();
assertThat(rs.getString(1)).isEqualTo("外税");
}
}
}
テストを実行します。
./gradlew test
BUILD SUCCESSFUL
コード品質管理ツールの設定¶
良いコードを書き続けるためにはコードの品質を維持していく必要があります。
Checkstyle の設定¶
Checkstyle は、Java のコーディング規約を検証するツールです。
config/checkstyle/checkstyle.xml を作成します。
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="Checker">
<property name="charset" value="UTF-8"/>
<property name="severity" value="warning"/>
<property name="fileExtensions" value="java, properties, xml"/>
<!-- ファイルサイズのチェック -->
<module name="FileLength">
<property name="max" value="500"/>
</module>
<!-- TreeWalker -->
<module name="TreeWalker">
<!-- 命名規約 -->
<module name="ConstantName"/>
<module name="LocalFinalVariableName"/>
<module name="LocalVariableName"/>
<module name="MemberName"/>
<module name="MethodName"/>
<module name="PackageName"/>
<module name="ParameterName"/>
<module name="StaticVariableName"/>
<module name="TypeName"/>
<!-- インポート -->
<module name="AvoidStarImport"/>
<module name="IllegalImport"/>
<module name="RedundantImport"/>
<module name="UnusedImports"/>
<!-- サイズ制限 -->
<module name="MethodLength">
<property name="max" value="150"/>
</module>
<module name="ParameterNumber">
<property name="max" value="7"/>
</module>
<!-- 空白 -->
<module name="EmptyForIteratorPad"/>
<module name="GenericWhitespace"/>
<module name="MethodParamPad"/>
<module name="NoWhitespaceAfter"/>
<module name="NoWhitespaceBefore"/>
<module name="OperatorWrap"/>
<module name="ParenPad"/>
<module name="TypecastParenPad"/>
<module name="WhitespaceAfter"/>
<module name="WhitespaceAround"/>
<!-- 複雑度 -->
<module name="CyclomaticComplexity">
<property name="max" value="10"/>
</module>
</module>
</module>
PMD の設定¶
PMD は、Java のコード品質を分析するツールです。
config/pmd/ruleset.xml を作成します。
<?xml version="1.0"?>
<ruleset name="Custom Rules"
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0
https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
<description>PMD Custom Ruleset</description>
<!-- ベストプラクティス -->
<rule ref="category/java/bestpractices.xml">
<!-- テストでは複数アサートを許可 -->
<exclude name="UnitTestContainsTooManyAsserts"/>
<!-- ResultSet のチェックはテストコードでは必須ではない -->
<exclude name="CheckResultSet"/>
</rule>
<!-- コードスタイル -->
<rule ref="category/java/codestyle.xml">
<exclude name="AtLeastOneConstructor"/>
<exclude name="OnlyOneReturn"/>
<exclude name="LongVariable"/>
<exclude name="ShortVariable"/>
<!-- final パラメータは強制しない -->
<exclude name="MethodArgumentCouldBeFinal"/>
<!-- ローカル変数の final は強制しない -->
<exclude name="LocalVariableCouldBeFinal"/>
<!-- デフォルトアクセス修飾子のコメントは強制しない -->
<exclude name="CommentDefaultAccessModifier"/>
<!-- var の使用を許可 -->
<exclude name="UseExplicitTypes"/>
</rule>
<!-- 設計 -->
<rule ref="category/java/design.xml">
<exclude name="LawOfDemeter"/>
<exclude name="LoosePackageCoupling"/>
<!-- Spring Boot Application クラスは例外 -->
<exclude name="UseUtilityClass"/>
<!-- データモデルはフィールド数が多くなることがある -->
<exclude name="TooManyFields"/>
</rule>
<!-- マルチスレッド -->
<rule ref="category/java/multithreading.xml"/>
<!-- エラープローン -->
<rule ref="category/java/errorprone.xml">
<!-- テストで文字列リテラルの重複は許可 -->
<exclude name="AvoidDuplicateLiterals"/>
<!-- @TestConfiguration クラスはテストケースを持たない -->
<exclude name="TestClassWithoutTestCases"/>
</rule>
<!-- パフォーマンス -->
<rule ref="category/java/performance.xml"/>
</ruleset>
SpotBugs の設定¶
config/spotbugs/exclude.xml を作成します。
<?xml version="1.0" encoding="UTF-8"?>
<FindBugsFilter>
<!-- Application クラスの除外 -->
<Match>
<Class name="com.example.sms.Application"/>
</Match>
<!-- Lombok 生成コードの除外 -->
<Match>
<Bug pattern="EI_EXPOSE_REP"/>
</Match>
<Match>
<Bug pattern="EI_EXPOSE_REP2"/>
</Match>
<!-- テストクラスの除外 -->
<Match>
<Source name="~.*Test\.java"/>
</Match>
</FindBugsFilter>
コード品質チェックの実行¶
# Checkstyle 実行
./gradlew checkstyleMain checkstyleTest
# PMD 実行
./gradlew pmdMain pmdTest
# SpotBugs 実行
./gradlew spotbugsMain spotbugsTest
# すべての品質チェックを実行
./gradlew check
テストカバレッジの確認¶
# テストを実行してカバレッジレポートを生成
./gradlew test jacocoTestReport
# レポートの表示
open build/reports/jacoco/test/html/index.html
コミット前の品質チェック¶
husky + lint-staged により、コミット時に自動で品質チェックが実行されます。
セットアップ¶
プロジェクトルートで以下を実行します。
# 依存関係のインストール
npm install
# husky の初期化(初回のみ)
npx husky init
設定ファイル¶
package.json¶
プロジェクトルートの package.json に以下の設定を追加します。
{
"name": "practical-database-design",
"private": true,
"scripts": {
"prepare": "husky"
},
"devDependencies": {
"husky": "^9.1.7",
"lint-staged": "^15.2.11"
}
}
.lintstagedrc.sms.json¶
SMS バックエンド用の lint-staged 設定ファイルを作成します。
{
"apps/sms/backend/src/**/*.java": [
"bash -c 'cd apps/sms/backend && ./gradlew checkstyleMain checkstyleTest pmdMain spotbugsMain --no-daemon'"
]
}
.husky/pre-commit¶
.husky/pre-commit ファイルを作成します。
npx lint-staged --config .lintstagedrc.sms.json
自動実行される品質チェック¶
バックエンド(Java ファイルに変更がある場合):
| ツール | 目的 |
|---|---|
| Checkstyle | コーディング規約の検証 |
| PMD | バグパターン検出 |
| SpotBugs | 潜在バグ検出 |
手動での品質チェック¶
cd apps/sms/backend
# 全品質チェック
./gradlew qualityCheck
# テストのみ
./gradlew test
# カバレッジレポート
./gradlew jacocoTestReport
# 結果: build/reports/jacoco/test/html/index.html
# すべてのテストと品質チェックを実行
./gradlew fullCheck
pre-commit フックが失敗する場合¶
# バックエンドのチェック
cd apps/sms/backend
./gradlew qualityCheck
# エラーを確認してコードを修正
# 再度コミット
トラブルシューティング¶
Java 25 と Gradle の互換性¶
問題: Gradle Kotlin DSL が Java 25 をサポートしていない
java.lang.IllegalArgumentException: 25.0.1
at org.jetbrains.kotlin.com.intellij.util.lang.JavaVersion.parse
解決策: Gradle 9.2.1 以上を使用する
# gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
Spring Boot 4.0.0 の Flyway 自動設定¶
問題: Flyway マイグレーションが実行されない
Spring Boot 4.0.0 では auto-configuration がモジュール化され、flyway-core だけでは自動設定されません。
解決策: spring-boot-starter-flyway を使用する
// build.gradle.kts
dependencies {
implementation("org.springframework.boot:spring-boot-starter-flyway")
implementation("org.flywaydb:flyway-database-postgresql")
}
参考: Spring Boot 4 Modularization
TestContainers と Spring Boot 4.0.0¶
問題: AutoConfigureTestDatabase が見つからない
Spring Boot 4.0.0 では @ServiceConnection を使用した自動設定が推奨されます。
解決策: @TestConfiguration + @Import パターンを使用する
// TestcontainersConfiguration.java
@TestConfiguration(proxyBeanMethods = false)
public class TestcontainersConfiguration {
@Bean
@ServiceConnection
PostgreSQLContainer<?> postgresContainer() {
return new PostgreSQLContainer<>(DockerImageName.parse("postgres:16-alpine"));
}
}
// BaseIntegrationTest.java
@SpringBootTest
@Import(TestcontainersConfiguration.class)
public abstract class BaseIntegrationTest {
// ...
}
このパターンを使用すると、複数のテストクラス間でコンテナを共有でき、接続の問題を回避できます。
注意: ~/.testcontainers.properties に以下の設定を追加すると、コンテナの再利用が有効になります。
testcontainers.reuse.enable=true
PMD 7.x のルール変更¶
問題: PMD 7.x で除外ルール名が見つからない
PMD 7.x では一部のルールがカテゴリ間で移動されています。
解決策1: ruleset.xml で正しいカテゴリから除外する
<!-- コードスタイル -->
<rule ref="category/java/codestyle.xml">
<exclude name="LocalVariableCouldBeFinal"/>
<exclude name="UseExplicitTypes"/>
</rule>
<!-- 設計 -->
<rule ref="category/java/design.xml">
<exclude name="TooManyFields"/>
</rule>
<!-- エラープローン -->
<rule ref="category/java/errorprone.xml">
<exclude name="AvoidDuplicateLiterals"/>
<exclude name="TestClassWithoutTestCases"/>
</rule>
解決策2: @SuppressWarnings で個別に抑制する
// Lombok @Builder.Default を使う場合はデフォルト値を明示する必要があるため
@SuppressWarnings("PMD.RedundantFieldInitializer")
public class Partner {
@Builder.Default
private boolean isCustomer = false;
}
.gitignore での out ディレクトリ¶
問題: application/port/out が git に追跡されない
Next.js の出力ディレクトリ out が無視される設定がある場合、Java の port/out も無視されます。
解決策: .gitignore で Java ソースを例外にする
/out
# Java source out directories should not be ignored
!**/src/**/out/
TDD アプローチの導入¶
Red-Green-Refactor サイクル¶
TDD の基本サイクルを、データベース設計に適用します。
- Red(失敗): まず、欲しい機能のテストを書きます。この時点ではテーブルも実装もないため、テストは失敗します。
- Green(成功): テストを通すための最小限の実装(テーブル定義、リポジトリなど)を行います。
- Refactor(改善): テストが通った状態を維持しながら、コードを改善します。
データベース設計への TDD 適用¶
従来のデータベース設計では、最初に完璧な ER 図を描こうとします。しかし、TDD アプローチでは:
- 最小限のテストから始める: 「商品を登録できる」という単純なテストから開始
- 段階的に拡張: テストを追加しながら、テーブル構造を進化させる
- リファクタリングを恐れない: テストがあるので、安心して構造を変更できる
MyBatis での日本語カラム対応¶
MyBatis では、日本語カラム名をそのまま使用できます。
// エンティティクラス
@Data
public class 商品マスタ {
private Integer id;
private String 商品コード;
private String 商品正式名;
private String 商品名;
private String 商品名カナ;
private String 商品区分;
private String 製品型番;
private BigDecimal 販売単価;
private BigDecimal 仕入単価;
private String 税区分;
private String 商品分類コード;
private Boolean 雑区分;
private Boolean 在庫管理対象区分;
private Boolean 在庫引当区分;
private String 仕入先コード;
private LocalDateTime 作成日時;
private String 作成者名;
private LocalDateTime 更新日時;
private String 更新者名;
}
// Mapper インターフェース
@Mapper
public interface 商品マスタMapper {
@Insert("""
INSERT INTO 商品マスタ (
商品コード, 商品正式名, 商品名, 商品名カナ, 商品区分,
製品型番, 販売単価, 仕入単価, 税区分, 商品分類コード,
雑区分, 在庫管理対象区分, 在庫引当区分, 仕入先コード,
作成者名, 更新者名
) VALUES (
#{商品コード}, #{商品正式名}, #{商品名}, #{商品名カナ}, #{商品区分}::商品区分,
#{製品型番}, #{販売単価}, #{仕入単価}, #{税区分}::税区分, #{商品分類コード},
#{雑区分}, #{在庫管理対象区分}, #{在庫引当区分}, #{仕入先コード},
#{作成者名}, #{更新者名}
)
""")
@Options(useGeneratedKeys = true, keyProperty = "id")
void insert(商品マスタ product);
@Select("SELECT * FROM 商品マスタ WHERE 商品コード = #{商品コード}")
Optional<商品マスタ> findBy商品コード(String 商品コード);
@Delete("TRUNCATE TABLE 商品マスタ CASCADE")
void deleteAll();
}
フロントエンドのセットアップ¶
1. Vite プロジェクトの初期化¶
cd apps/sms/frontend
npm create vite@latest . -- --template react-ts
npm install
2. 追加パッケージのインストール¶
npm install axios @tanstack/react-query
npm install -D tailwindcss postcss autoprefixer
npm install -D vitest @testing-library/react @testing-library/jest-dom
npx tailwindcss init -p
起動手順¶
# 1. Docker コンテナ起動
cd apps/sms
docker compose up -d
# 2. バックエンド起動
cd backend
./gradlew bootRun
# 3. フロントエンド起動
cd frontend
npm run dev