0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

s2jdbcを他のフレームワークで使えるようにしたnos2jdbc

0
Last updated at Posted at 2026-10-24

はじめのはじめ

私は、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) を紹介します。

Maven Central Repositoryに登録済みのため、すぐにプロジェクトへ導入できます。


nos2jdbcの特徴

  1. Seasar2コンテナへの依存を完全に排除
    • DIコンテナなしのスタンドアローンアプリでも、Spring Boot でも、WildFly (Jakarta EE / CDI) でも動作します。
    • 関連エンティティの取得を Entityの定義時ではなくクエリ作成時 に innerJoin() / leftOuterJoin() で指定できます。遅延ロードがないため、意図しないN+1は発生しません。
  2. モダンな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として刷新)
  3. 実務で欲しかった機能を追加(後述)
    • JSONシリアライズ時の循環参照(無限ループ)回避メソッド
    • 2Way SQL(SqlSelect / SqlFileSelect)の検索結果を関連エンティティ(@OneToMany / @OneToOne)へ階層マッピングする機能(ROLLUP / CUBE 等の集計クエリにも対応)
    • JPAの @Embedded / @Embeddable サポート
    • Java record と Bean(エンティティ等)の相互変換ユーティリティ Records
    • @CreatedAt / @UpdatedAt による作成・更新日時の自動セット、upsert() メソッドの追加
  4. 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検索の結果も 関連を持つエンティティ階層へ自動マッピング できるようになりました。

使い方

  1. マッピング先のクラスに @Entity と @NonAuto アノテーションを付与します(@NonAuto を付けることで、nos2jdbc-gen のDDL自動生成などの対象から除外されます)。
  2. SQLのSELECT句のカラム名が重複しないように別名(AS)を付けます。
  3. 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 にありますので、あわせて参考にしていただければ幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?