はじめのはじめ
私は、Seasar2が終了した直後に、S2JDBCをS2Container非依存にして自分のプロダクトに使っておりました。nos2jdbcです。
この度、ChatGPTにREADME.mdを書いてもらい、他人に紹介できるようになりました。
そして、antigravity cliに以下の記事を書いてもらいました。
実際の使用でs2jdbcの元の機能は、www.seasar.orgの文書をお読みください。
はじめに
かつてJavaのWeb開発で広く使われていた Seasar2 ですが、その中でも特に人気が高かったコンポーネントがO/Rマッパーの S2JDBC です。
- 流れるようなインターフェース(Fluent API) と Namesクラス による、IDE補完が効くタイプセーフで直感的なSQL自動生成
-
Entity定義時ではなくクエリ作成時に指定するJOIN — JPAの
EAGER/LAZY指定のように静的な定義に縛られず、必要な場面でinnerJoin()やleftOuterJoin()を書いてN+1問題をシンプルに回避 - 複雑なクエリもそのまま書けてテストしやすい 2Way SQL
- エンティティからDDLやサービスクラス、マイグレーションまで自動生成してくれる S2JDBC-Gen
この快適さが忘れられず、「Spring Bootや現在のJava環境でもS2JDBCを使いたい」と思ったことはないでしょうか?
本記事では、S2JDBCからSeasar2コンテナ(DIコンテナ)への依存を取り除き、モダンなJava環境(Java 17+、Jakarta EE、Spring Boot、JUnit 5、Kotlinなど)で利用できるようにし、さらに実用的な機能拡張を加えた nos2jdbc (No Seasar2 Container's S2JDBC) を紹介します。
- GitHubリポジトリ: https://github.com/ns2j/nos2jdbc
- チュートリアル集: https://github.com/ns2j/nos2jdbc-tutorial
Maven Central Repositoryに登録済みのため、すぐにプロジェクトへ導入できます。
nos2jdbcの特徴
-
Seasar2コンテナへの依存を完全に排除
- DIコンテナなしのスタンドアローンアプリでも、Spring Boot でも、WildFly (Jakarta EE / CDI) でも動作します。
- 関連エンティティの取得を Entityの定義時ではなくクエリ作成時 に
innerJoin()/leftOuterJoin()で指定できます。遅延ロードがないため、意図しないN+1は発生しません。
-
モダンなJava環境へのアップデート
- Java 17+ 対応
-
javax.*からjakarta.*(jakarta.persistence,jakarta.transaction,jakarta.annotation) への移行済み -
java.time.*(LocalDate,LocalDateTime,OffsetDateTimeなど)に標準対応 - ロガーを
commons-loggingから SLF4J に変更 -
JUnit 5 対応(Excel
.xlsx/ LibreOffice.odsによるテストデータ投入・検証機能もJUnit 5 Extensionとして刷新)
-
実務で欲しかった機能を追加(後述)
- JSONシリアライズ時の循環参照(無限ループ)回避メソッド
- 2Way SQL(
SqlSelect/SqlFileSelect)の検索結果を関連エンティティ(@OneToMany/@OneToOne)へ階層マッピングする機能(ROLLUP/CUBE等の集計クエリにも対応) - JPAの
@Embedded/@Embeddableサポート - Java
recordと Bean(エンティティ等)の相互変換ユーティリティRecords -
@CreatedAt/@UpdatedAtによる作成・更新日時の自動セット、upsert()メソッドの追加
-
S2JDBC-Genも
nos2jdbc-genとして進化- エンティティからのDDL生成・DBマイグレーション・Namesクラス生成・Service生成に対応
- 生成するサービスクラスのDIアノテーションを
componenttype(spring/cdi/ejb/none)で切り替え可能 - Kotlin コードの生成にも対応
モジュール構成とMaven設定
nos2jdbc は用途に応じて以下の4モジュールに分かれています。
| モジュール | 説明 |
|---|---|
nos2jdbc-core |
S2JDBC本体(コネクションプール・トランザクション管理内蔵) |
nos2jdbc-gen |
DDL・Entity・Names・Service・テストクラスの自動生成およびマイグレーションツール |
nos2jdbc-util |
Beans や Records などのユーティリティ群(nos2jdbc-core に含まれます) |
nos2jdbc-test |
JUnit 5用のDBテスト支援ライブラリ |
pom.xml の記述例
Maven Centralから取得できます。
<dependencies>
<dependency>
<groupId>com.github.ns2j</groupId>
<artifactId>nos2jdbc-core</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>jakarta.transaction</groupId>
<artifactId>jakarta.transaction-api</artifactId>
<version>2.0.1</version>
</dependency>
</dependencies>
基本的な使い方
1. データソース設定 (nos2jdbc-datasource.properties)
クラスパス直下(src/main/resources/nos2jdbc-datasource.properties)に設定ファイルを配置します。
driverClassName=org.postgresql.Driver
URL=${DB_URL:jdbc:postgresql://localhost:5432/mydb}
user=${DB_USER:postgres}
password=${DB_PASS:password}
dialect=PostgreDialect
poolSize=50
-
${環境変数名:デフォルト値}の形式で環境変数を展開できます。 - JVM起動オプション
-Ddatabase=devを指定すると、nos2jdbc-datasource-dev.propertiesを読み込むように切り替えることも可能です。
2. スタンドアローンでの利用
DIコンテナを使わない場合は、NoS2JdbcManager.getJdbcManager() を呼ぶだけで JdbcManager を取得できます。
JdbcManager jdbcManager = NoS2JdbcManager.getJdbcManager();
List<Club> clubs = jdbcManager
.from(Club.class)
.leftOuterJoin(clubMemberRelList())
.leftOuterJoin(clubMemberRelList().member())
.getResultList();
このように、クエリを作成するタイミングで leftOuterJoin() や innerJoin() を必要な階層だけ連ねて記述できるため、JPAの LazyInitializationException やN+1問題に悩まされることなく、1回のクエリで関連エンティティをまとめて取得できます。
3. Spring Boot と組み合わせて使う場合
Spring Bootと組み合わせる場合も、以下のような設定クラスを1つ用意するだけで、Springの @Transactional と JdbcManager をシームレスに連携できます。
package com.example.config;
import org.seasar.extension.jdbc.JdbcManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.jta.JtaTransactionManager;
import nos2jdbc.TransactionManagerRegistry;
import nos2jdbc.standalone.NoS2JdbcManager;
@EnableTransactionManagement
@Configuration
public class AppConfig {
@Bean
public JdbcManager getJdbcManager() {
return NoS2JdbcManager.getJdbcManager();
}
@Bean
public PlatformTransactionManager getPlatformTransactionManager() {
return new JtaTransactionManager(TransactionManagerRegistry.get());
}
}
設定不要でそのまま動かせるSpring Bootのサンプルプロジェクトを nos2jdbc-tutorial に用意しています。
4. DDL作成とマイグレーション (nos2jdbc-gen)
チュートリアルのpom設定が前提ですが、
Entityクラスを作成したら、nos2jdbc-gen を使ってDDLの作成やデータベースのマイグレーションを実行できます。
コンパイル
mvn compile -Pcompile-least
これは、リソースのコピーとEntityのコンパイルだけをして、nos2jdbc-gen を使えるようにします。
DDL作成
nos2jdbc-gen をMavenから実行します。
mvn antrun:run -Dant.target=gen-ddl
migrate
生成したDDLをデータベースへ反映(マイグレーション)します。
mvn antrun:run -Dant.target=migrate
nos2jdbc で追加された新機能
オリジナルのS2JDBCから、現代のアプリケーション開発で便利な機能を多数追加しています。
1. JSONシリアライズ時の無限ループ回避
REST APIなどでエンティティをそのままJSONに変換しようとした際、@OneToMany と @ManyToOne の双方向リレーションがあると「親 → 子 → 親 → …」と循環参照になり、Jackson等で StackOverflowError(無限ループ)が発生してしまいます。
nos2jdbc では、検索実行時に 逆方向(inverse側)のフィールドへ参照をセットしない メソッドを追加しました。
getResultListWithoutInverseField()getSingleResultWithoutInverseField()iterateWithoutInverseField(callback)
List<Club> clubs = jdbcManager
.from(Club.class)
.leftOuterJoin(clubMemberRelList())
.getResultListWithoutInverseField(); // 子から親への逆参照をセットしない
エンティティにJSONライブラリ固有のアノテーション(@JsonIgnore など)を付けなくても、そのまま安全にJSONシリアライズできます。
2. SQL検索(SqlSelect / SqlFileSelect)の階層エンティティマッピング
オリジナルのS2JDBCでは、selectBySqlFile などのSQL検索はフラットなDTO(1階層)にしかマッピングできず、JOINした結果を @OneToMany や @OneToOne のリスト・ネスト構造にまとめる処理は自前で書く必要がありました。
nos2jdbc では、SQL検索の結果も 関連を持つエンティティ階層へ自動マッピング できるようになりました。
使い方
- マッピング先のクラスに
@Entityと@NonAutoアノテーションを付与します(@NonAutoを付けることで、nos2jdbc-genのDDL自動生成などの対象から除外されます)。 - SQLのSELECT句のカラム名が重複しないように別名(AS)を付けます。
-
mappedByを指定していない側のリレーションフィールドには@NoFkアノテーションを付けます。
@Entity @NonAuto
public class Ym {
@Id
public Integer y;
@Id
public Integer m;
@OneToMany(mappedBy = "ym")
public List<YmItem> itemList;
}
@Entity @NonAuto
public class YmItem {
@Id
public Long lunchFeeId;
public LocalDate payOn;
public BigDecimal amount;
@OneToOne(mappedBy = "ymItem")
public YmItemMember member;
@Transient
public Ym ym;
}
@Entity @NonAuto
public class YmItemMember {
public Long memberId;
public String memberName;
@OneToOne
@NoFk
public YmItem ymItem;
}
// 2Way SQLの結果をYm -> List<YmItem> -> YmItemMemberの階層構造に一発マッピング!
List<Ym> list = jdbcManager
.selectBySqlFile(Ym.class, "sql/sum.sql")
.getResultList();
さらに、GROUP BY ROLLUP(...) や CUBE(...) のような集計SQLで小計・合計行のキー(@Id)が null になるケースでも、エンティティオブジェクトが生成されるように対応しています。
3. JPAの @Embedded / @Embeddable サポート(v2.0.0〜)
S2JDBCではサポートされていなかったJPAの @Embedded と @Embeddable に対応しました。
@Embeddable
public class Address {
public String zipCode;
public String city;
}
@Entity
public class Customer {
@Id
@GeneratedValue
public Long id;
public String name;
@Embedded
public Address address;
}
- テーブルには
「埋め込み先のフィールド名」__「埋め込みクラス内のフィールド名」(アンダースコア2つ区切り、上記の例ではADDRESS__ZIP_CODE,ADDRESS__CITY)のカラムがマッピングされます。 -
nos2jdbc-genによるDDL生成やNamesクラス生成、AutoSelect/AutoInsert/AutoUpdateすべてに対応しています。
4. Java record と Bean の生成・コピーユーティリティ Records(v2.0.0〜)
Java 16以降で導入された record をDTOやControllerのリクエスト/レスポンスとして使い、DBアクセス層ではエンティティ(Bean)を使うケースが増えています。
そこで Beans ユーティリティに加え、record と Bean を相互変換する Records クラス(org.seasar.framework.beans.util.Records)を追加しました。
// エンティティ(Bean)から record を生成(ネストした record や List<Record> にも再帰的に対応!)
ClubRecord record = Records.createRecord(ClubRecord.class, clubEntity);
// エンティティのリストから record のリストを一括生成
List<ClubRecord> recordList = Records.createRecordList(ClubRecord.class, clubEntityList);
// record の値をエンティティ(Bean)へコピー(ネストしたオブジェクトにも対応)
Club entity = new Club();
Records.copyToBean(clubRequestRecord, entity);
リレーションを持つネストしたエンティティ構造も、対応する record の階層構造へそのまま変換できるため、ボイラープレートな詰め替えコードを書く必要がありません。
5. @CreatedAt / @UpdatedAt と upsert()
@CreatedAt / @UpdatedAt
エンティティや @MappedSuperclass の日時フィールドに @CreatedAt / @UpdatedAt を付与しておくと、insert 時に @CreatedAt と @UpdatedAt、更新(update)時に @UpdatedAt へ現在日時が自動設定されます。
@MappedSuperclass
public class EntityBase {
@Id
@GeneratedValue
public Long id;
@CreatedAt
public OffsetDateTime createdAt;
@UpdatedAt
public OffsetDateTime updatedAt;
}
upsert() とバッチ更新メソッド
nos2jdbc-gen が生成する抽象サービスクラス(NoS2AbstractServiceBase)には、主キー(@Id)が null なら insert、非 null なら update を実行する upsert(entity) メソッドや、insertBatch / updateBatch / deleteBatch が標準で用意されています。
6. JUnit 5によるExcel / LibreOfficeテストデータ連携 (nos2jdbc-test)
S2Unitで好評だった「テストデータをExcelで用意し、テスト実行前にDBへ投入・期待値と比較する機能」も、JUnit 5のExtension(SsDataSetExtension)として利用できます。Excel(.xlsx)だけでなくLibreOffice Calc(.ods)にも対応しています。
@ExtendWith(SsDataSetExtension.class)
public class MemberServiceTest {
JdbcManager jdbcManager = NoS2JdbcManager.getJdbcManager();
@Test
@SsInsertion("testdata/init-data.xlsx")
@SsReading("testdata/expected-data.xlsx")
void testUpdate(DataSet expected, DataSetOpe ope) {
// テスト対象の処理を実行
...
// DBの状態とExcelの期待値を比較検証
DataSet actual = ope.read(expected);
DataSetAssertions.assertEquals(expected, actual);
}
}
おわりに
nos2jdbc は、S2JDBCのシンプルで生産性の高い設計思想をそのまま引き継ぎながら、現在のJava 17+ / Spring Boot / Jakarta EE / Kotlin環境で快適に使えるようにメンテナンス・機能拡張を続けています。
- 「S2JDBCの書き心地が好きだった」
- 「JPA(Hibernate)は複雑すぎるが、生SQLばかり書くのも大変」
- 「旧Seasar2プロジェクトをSpring Boot等へマイグレーションしたい」
という方は、ぜひ一度試してみてください!
すぐに動かせるサンプルは nos2jdbc-tutorial にありますので、あわせて参考にしていただければ幸いです。