jOOQ
jOOQは「Java Object Oriented Querying」の略称で、JavaでSQLを書くための最良の方法を提供するライブラリです。データベースファーストのDSL(Domain Specific Language)により、SQLライクな流暢APIで型安全なクエリ構築を実現。複雑なSQLと型安全性の両立を実現する特殊なアプローチで、SQL知識を活かしながらJavaの型システムの恩恵を受けられます。データベーススキーマからJavaコードを生成し、コンパイル時にSQL構文の正確性を保証する革新的なアプローチを採用しています。
GitHub概要
トピックス
スター履歴
ライブラリ
jOOQ
概要
jOOQは「Java Object Oriented Querying」の略称で、JavaでSQLを書くための最良の方法を提供するライブラリです。データベースファーストのDSL(Domain Specific Language)により、SQLライクな流暢APIで型安全なクエリ構築を実現。複雑なSQLと型安全性の両立を実現する特殊なアプローチで、SQL知識を活かしながらJavaの型システムの恩恵を受けられます。データベーススキーマからJavaコードを生成し、コンパイル時にSQL構文の正確性を保証する革新的なアプローチを採用しています。
詳細
jOOQ 2025年版は、SQLファーストアプローチによる型安全なデータベースアクセスの決定版として確固たる地位を築いています。従来のORMが抽象化により隠蔽するSQL制御権を開発者に完全に委ね、同時にJavaの型システムによる安全性を提供する画期的な設計思想。データベーススキーマから自動生成される型安全なクラス群により、存在しないテーブルやカラムへのアクセスをコンパイル時に検出可能。PostgreSQL、MySQL、Oracle、SQL Server等の主要DBを完全サポートし、方言固有の機能も活用できます。MULTISET演算子による入れ子クエリ、JSONサポート、ストアドプロシージャ実行など、エンタープライズレベルの高度な機能を提供します。
主な特徴
- データベースファーストアプローチ: スキーマ定義からJavaコード自動生成
- 型安全SQL DSL: コンパイル時のSQL構文チェックとテーブル・カラム検証
- 豊富なSQL機能サポート: 複雑なJOIN、サブクエリ、ウィンドウ関数対応
- マルチデータベース対応: 主要DBの方言と固有機能完全サポート
- 高度なデータ構造: MULTISET、配列、JSON、XMLサポート
- パフォーマンス最優先: 生SQLと同等の実行効率
メリット・デメリット
メリット
- SQL知識を直接活かせる型安全なDSLによる高い開発効率
- コンパイル時エラー検出による実行時例外の大幅削減
- 複雑なクエリやパフォーマンスチューニングの完全制御
- 豊富なSQL機能と各データベース固有機能の活用
- ORMレイヤーのオーバーヘッドがない高いパフォーマンス
- スキーマ変更の自動反映によるメンテナンス性向上
デメリット
- 学習コストが高く、SQL知識が必須
- データベーススキーマからのコード生成必須で初期設定が複雑
- オブジェクト指向的なドメインモデル設計には不向き
- スキーマ変更時のコード再生成が必要
- 他ORMと比較してボイラープレートコード量が多い
- ライセンスコストが発生する場合がある(商用版)
参考ページ
書き方の例
プロジェクト設定とコード生成
<!-- Maven pom.xml -->
<dependencies>
<dependency>
<groupId>org.jooq</groupId>
<artifactId>jooq</artifactId>
<version>3.19.1</version>
</dependency>
<dependency>
<groupId>org.jooq</groupId>
<artifactId>jooq-meta</artifactId>
<version>3.19.1</version>
</dependency>
<dependency>
<groupId>org.jooq</groupId>
<artifactId>jooq-codegen</artifactId>
<version>3.19.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.jooq</groupId>
<artifactId>jooq-codegen-maven</artifactId>
<version>3.19.1</version>
<configuration>
<jdbc>
<driver>org.postgresql.Driver</driver>
<url>jdbc:postgresql://localhost:5432/testdb</url>
<user>username</user>
<password>password</password>
</jdbc>
<generator>
<database>
<name>org.jooq.meta.postgres.PostgresDatabase</name>
<inputSchema>public</inputSchema>
</database>
<target>
<packageName>com.example.generated</packageName>
<directory>target/generated-sources/jooq</directory>
</target>
</generator>
</configuration>
</plugin>
</plugins>
</build>
# コード生成実行
mvn jooq-codegen:generate
# Gradle設定例
dependencies {
implementation 'org.jooq:jooq:3.19.1'
jooqGenerator 'org.postgresql:postgresql:42.7.1'
}
jooq {
configurations {
main {
generationTool {
jdbc {
driver = 'org.postgresql.Driver'
url = 'jdbc:postgresql://localhost:5432/testdb'
user = 'username'
password = 'password'
}
generator {
database {
name = 'org.jooq.meta.postgres.PostgresDatabase'
inputSchema = 'public'
}
target {
packageName = 'com.example.generated'
}
}
}
}
}
}
基本的なクエリ操作(SELECT/INSERT/UPDATE/DELETE)
import static com.example.generated.Tables.*;
import static org.jooq.impl.DSL.*;
// DSLContext設定
DataSource ds = // DataSource設定
DSLContext dsl = DSL.using(ds, SQLDialect.POSTGRES);
// 基本的なSELECTクエリ
Result<Record> result = dsl
.select()
.from(USERS)
.where(USERS.AGE.greaterThan(18))
.orderBy(USERS.NAME)
.fetch();
// 特定カラムの選択
List<String> names = dsl
.select(USERS.NAME)
.from(USERS)
.where(USERS.EMAIL.like("%@example.com"))
.fetch(USERS.NAME);
// JOINクエリ
Result<Record> joinResult = dsl
.select(USERS.NAME, ORDERS.TOTAL_AMOUNT)
.from(USERS)
.innerJoin(ORDERS).on(USERS.ID.eq(ORDERS.USER_ID))
.where(ORDERS.ORDER_DATE.greaterThan(LocalDate.of(2024, 1, 1)))
.fetch();
// INSERT操作
UsersRecord newUser = dsl.newRecord(USERS);
newUser.setName("田中太郎");
newUser.setEmail("[email protected]");
newUser.setAge(30);
newUser.store(); // INSERT実行
// 別のINSERT方法
dsl.insertInto(USERS)
.set(USERS.NAME, "佐藤花子")
.set(USERS.EMAIL, "[email protected]")
.set(USERS.AGE, 25)
.execute();
// バッチINSERT
dsl.insertInto(USERS, USERS.NAME, USERS.EMAIL, USERS.AGE)
.values("山田一郎", "[email protected]", 28)
.values("鈴木二郎", "[email protected]", 32)
.values("高橋三郎", "[email protected]", 29)
.execute();
// UPDATE操作
int updatedCount = dsl
.update(USERS)
.set(USERS.AGE, USERS.AGE.add(1))
.where(USERS.ID.eq(1))
.execute();
// 条件付きUPDATE
dsl.update(USERS)
.set(USERS.STATUS, "ACTIVE")
.where(USERS.LAST_LOGIN.greaterThan(LocalDateTime.now().minusDays(30)))
.execute();
// DELETE操作
int deletedCount = dsl
.deleteFrom(USERS)
.where(USERS.STATUS.eq("INACTIVE")
.and(USERS.LAST_LOGIN.lessThan(LocalDateTime.now().minusYears(1))))
.execute();
// レコード型での安全な操作
UsersRecord user = dsl
.selectFrom(USERS)
.where(USERS.ID.eq(1))
.fetchOne();
if (user != null) {
user.setAge(user.getAge() + 1);
user.update(); // UPDATE実行
}
高度なクエリとMULTISET機能
// サブクエリの使用
List<UsersRecord> activeUsers = dsl
.selectFrom(USERS)
.where(USERS.ID.in(
select(ORDERS.USER_ID)
.from(ORDERS)
.where(ORDERS.ORDER_DATE.greaterThan(LocalDate.now().minusDays(30)))
))
.fetch();
// ウィンドウ関数
Result<Record4<String, Integer, Integer, Integer>> rankedUsers = dsl
.select(
USERS.NAME,
USERS.AGE,
count().over().as("total_users"),
rowNumber().over().orderBy(USERS.AGE.desc()).as("age_rank")
)
.from(USERS)
.orderBy(field("age_rank"))
.fetch();
// Common Table Expression (WITH句)
Result<Record> withResult = dsl
.with("young_users").as(
select(USERS.ID, USERS.NAME, USERS.AGE)
.from(USERS)
.where(USERS.AGE.lessThan(30))
)
.select()
.from(table(name("young_users")))
.fetch();
// MULTISETを使用した階層データ取得
record Film(String title, List<Actor> actors, List<String> categories) {}
record Actor(String firstName, String lastName) {}
// MULTISET helper関数定義
public static <T> Field<List<T>> multisetArray(SelectFieldOrAsterisk<Record1<T>> select) {
return field(
"({0})",
SQLDataType.CLOB.array(),
select(coalesce(
jsonArrayAgg(select),
jsonArray()
))
).map(Convert.convert(Object[].class, (List<T>) null));
}
// 階層データ取得クエリ
List<Film> films = dsl
.select(
FILM.TITLE,
multisetArray(
select(ACTOR.FIRST_NAME, ACTOR.LAST_NAME)
.from(FILM_ACTOR)
.join(ACTOR).on(FILM_ACTOR.ACTOR_ID.eq(ACTOR.ACTOR_ID))
.where(FILM_ACTOR.FILM_ID.eq(FILM.FILM_ID))
).as("actors"),
multisetArray(
select(CATEGORY.NAME)
.from(FILM_CATEGORY)
.join(CATEGORY).on(FILM_CATEGORY.CATEGORY_ID.eq(CATEGORY.CATEGORY_ID))
.where(FILM_CATEGORY.FILM_ID.eq(FILM.FILM_ID))
).as("categories")
)
.from(FILM)
.orderBy(FILM.TITLE)
.fetchInto(Film.class);
// 動的クエリ構築
Condition condition = trueCondition();
if (nameFilter != null) {
condition = condition.and(USERS.NAME.containsIgnoreCase(nameFilter));
}
if (minAge != null) {
condition = condition.and(USERS.AGE.greaterOrEqual(minAge));
}
if (emailDomain != null) {
condition = condition.and(USERS.EMAIL.like("%" + emailDomain));
}
List<UsersRecord> filteredUsers = dsl
.selectFrom(USERS)
.where(condition)
.orderBy(USERS.NAME)
.fetch();
トランザクション管理とバッチ処理
// 基本的なトランザクション
dsl.transaction(configuration -> {
DSLContext ctx = DSL.using(configuration);
// ユーザー作成
UsersRecord user = ctx.newRecord(USERS);
user.setName("新規ユーザー");
user.setEmail("[email protected]");
user.store();
// 注文作成
OrdersRecord order = ctx.newRecord(ORDERS);
order.setUserId(user.getId());
order.setTotalAmount(new BigDecimal("1000.00"));
order.store();
// ポイント付与
ctx.insertInto(USER_POINTS)
.set(USER_POINTS.USER_ID, user.getId())
.set(USER_POINTS.POINTS, 100)
.execute();
});
// ネストトランザクション(セーブポイント)
dsl.transaction(configuration -> {
DSLContext ctx = DSL.using(configuration);
UsersRecord user = ctx.newRecord(USERS);
user.setName("メインユーザー");
user.store();
try {
ctx.transaction(nestedConfig -> {
DSLContext nestedCtx = DSL.using(nestedConfig);
// リスクの高い操作
nestedCtx.update(USERS)
.set(USERS.CREDIT_LIMIT, new BigDecimal("999999"))
.where(USERS.ID.eq(user.getId()))
.execute();
// 何らかのチェック処理
if (someValidationFails()) {
throw new RuntimeException("検証失敗");
}
});
} catch (RuntimeException e) {
// ネストトランザクションのみロールバック
logger.warn("ネスト処理失敗、メイン処理は継続: " + e.getMessage());
}
});
// バッチ処理
List<UsersRecord> usersToUpdate = // 更新対象ユーザー取得
dsl.batchUpdate(usersToUpdate).execute();
// Bulk Insert with ON CONFLICT
dsl.insertInto(USERS)
.columns(USERS.EMAIL, USERS.NAME, USERS.AGE)
.values("[email protected]", "ユーザー1", 25)
.values("[email protected]", "ユーザー2", 30)
.values("[email protected]", "ユーザー3", 35)
.onConflict(USERS.EMAIL)
.doUpdate()
.set(USERS.NAME, excluded(USERS.NAME))
.set(USERS.AGE, excluded(USERS.AGE))
.execute();
// バッチ操作のパフォーマンス測定
Stopwatch stopwatch = Stopwatch.createStarted();
int[] batchResult = dsl.batch(
dsl.insertInto(USER_ACTIVITY)
.set(USER_ACTIVITY.USER_ID, (Integer) null)
.set(USER_ACTIVITY.ACTIVITY_TYPE, (String) null)
.set(USER_ACTIVITY.TIMESTAMP, (LocalDateTime) null)
)
.bind(1, "LOGIN", LocalDateTime.now())
.bind(2, "VIEW_PAGE", LocalDateTime.now())
.bind(3, "PURCHASE", LocalDateTime.now())
.execute();
stopwatch.stop();
logger.info("バッチ処理完了: {}ms, 処理件数: {}",
stopwatch.elapsed(TimeUnit.MILLISECONDS), batchResult.length);
ストアドプロシージャとカスタム関数
// ストアドプロシージャ実行
Result<Record> procedureResult = dsl
.select()
.from(table(call("get_user_orders",
param("user_id", 123),
param("start_date", LocalDate.of(2024, 1, 1))
)))
.fetch();
// カスタムSQL関数の定義
public static Field<BigDecimal> calculateDiscount(Field<BigDecimal> amount, Field<String> userType) {
return field("calculate_discount({0}, {1})", SQLDataType.DECIMAL, amount, userType);
}
// カスタム関数の使用
List<Record3<String, BigDecimal, BigDecimal>> discountedOrders = dsl
.select(
USERS.NAME,
ORDERS.TOTAL_AMOUNT,
calculateDiscount(ORDERS.TOTAL_AMOUNT, USERS.USER_TYPE).as("discounted_amount")
)
.from(ORDERS)
.join(USERS).on(ORDERS.USER_ID.eq(USERS.ID))
.fetch();
// データベース固有機能の活用(PostgreSQL配列)
List<String> tags = List.of("java", "database", "orm");
dsl.insertInto(ARTICLES)
.set(ARTICLES.TITLE, "jOOQ記事")
.set(ARTICLES.CONTENT, "jOOQについて")
.set(ARTICLES.TAGS, tags.toArray(new String[0])) // PostgreSQL配列
.execute();
// JSON操作(PostgreSQL)
dsl.update(USERS)
.set(USERS.PREFERENCES,
jsonObject(
key("theme").value("dark"),
key("language").value("ja"),
key("notifications").value(true)
))
.where(USERS.ID.eq(1))
.execute();
// JSON検索
List<UsersRecord> darkThemeUsers = dsl
.selectFrom(USERS)
.where(USERS.PREFERENCES.extract("$.theme").eq("dark"))
.fetch();
エラーハンドリングとログ出力
// SQLエラーハンドリング
try {
dsl.insertInto(USERS)
.set(USERS.EMAIL, "[email protected]") // 重複制約違反
.execute();
} catch (DataAccessException e) {
if (e.getCause() instanceof PSQLException) {
PSQLException psql = (PSQLException) e.getCause();
if ("23505".equals(psql.getSQLState())) { // UNIQUE_VIOLATION
logger.warn("重複キーエラー: {}", psql.getMessage());
// 重複処理ロジック
}
}
throw e;
}
// クエリ実行時間監視
ExecuteListener listener = new DefaultExecuteListener() {
@Override
public void executeStart(ExecuteContext ctx) {
logger.debug("Query start: {}", ctx.sql());
}
@Override
public void executeEnd(ExecuteContext ctx) {
logger.debug("Query end: {}ms", ctx.sqlExecutionTime());
if (ctx.sqlExecutionTime() > 1000) {
logger.warn("Slow query detected: {}ms - {}",
ctx.sqlExecutionTime(), ctx.sql());
}
}
};
DSLContext monitoredDsl = DSL.using(connection, SQLDialect.POSTGRES)
.configuration()
.derive(listener)
.dsl();
// SQL出力とデバッグ
DSLContext debugDsl = DSL.using(connection, SQLDialect.POSTGRES)
.configuration()
.derive(new DefaultExecuteListener() {
@Override
public void renderEnd(ExecuteContext ctx) {
logger.info("Generated SQL: {}", ctx.sql());
logger.debug("Bind values: {}", Arrays.toString(ctx.bindings()));
}
})
.dsl();
// 実行統計の取得
ExecuteContext stats = new DefaultExecuteContext();
Result<Record> result = dsl
.select()
.from(USERS)
.where(USERS.STATUS.eq("ACTIVE"))
.fetch();
logger.info("実行時間: {}ms, 取得件数: {}",
stats.sqlExecutionTime(), result.size());