원클릭으로
dev-database
データベースアダプター(PostgreSQL/MySQL両対応)の開発・修正を行う際に使用。SqlExecutorパターン、マイグレーション、DB固有SQL実装時に役立つ。
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
データベースアダプター(PostgreSQL/MySQL両対応)の開発・修正を行う際に使用。SqlExecutorパターン、マイグレーション、DB固有SQL実装時に役立つ。
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
| name | dev-database |
| description | データベースアダプター(PostgreSQL/MySQL両対応)の開発・修正を行う際に使用。SqlExecutorパターン、マイグレーション、DB固有SQL実装時に役立つ。 |
libs/idp-server-database/README.md - データベース設計・マイグレーションデータベースアダプターは、PostgreSQL/MySQL両方をサポートする永続化層。
@Transaction / @Transaction(readOnly = true) アノテーションlibs/
├── idp-server-core-adapter/ # 永続化実装
│ └── .../adapters/datasource/
│ ├── audit/
│ │ ├── command/
│ │ │ ├── AuditLogSqlExecutor.java # インターフェース
│ │ │ ├── AuditLogSqlExecutors.java # ファクトリ
│ │ │ ├── PostgresqlExecutor.java # PostgreSQL実装
│ │ │ └── MysqlExecutor.java # MySQL実装
│ │ └── query/
│ │ └── (同様の構造)
│ ├── token/
│ ├── identity/
│ ├── authentication/
│ ├── ciba/
│ └── ...
│
├── idp-server-database/ # マイグレーション
│ ├── postgresql/
│ │ ├── V0_9_0__init_lib.sql # 初期スキーマ
│ │ └── V1_0_0__*.sql # バージョン別マイグレーション
│ └── mysql/
│ ├── V0_9_0__init_lib.mysql.sql # ※ .mysql.sql 接尾辞
│ └── V0_9_21_1__*.mysql.sql
│
└── idp-server-platform/ # 共通基盤
└── .../datasource/
├── DatabaseType.java # POSTGRESQL, SPANNER, MYSQL
├── SqlExecutor.java # SQL実行ヘルパー
└── TransactionManager.java # トランザクション管理
AuditLogSqlExecutor.java:
// 1. インターフェース定義(Tenant第一引数)
public interface AuditLogSqlExecutor {
void insert(Tenant tenant, AuditLog auditLog);
}
PostgresqlExecutor.java:
// 2. PostgreSQL実装
public class PostgresqlExecutor implements AuditLogSqlExecutor {
@Override
public void insert(Tenant tenant, AuditLog auditLog) {
// SqlExecutorが内部でTransactionManager.getConnection()を使用
SqlExecutor sqlExecutor = new SqlExecutor();
String sqlTemplate = """
INSERT INTO audit_log (
id, type, description, tenant_id, client_id, user_id, ...
) VALUES (
?::uuid, ?, ?, ?::uuid, ...
);
""";
List<Object> params = new ArrayList<>();
params.add(auditLog.id().value());
// ...
sqlExecutor.execute(sqlTemplate, params);
}
}
MysqlExecutor.java:
// 3. MySQL実装
public class MysqlExecutor implements AuditLogSqlExecutor {
@Override
public void insert(Tenant tenant, AuditLog auditLog) {
SqlExecutor sqlExecutor = new SqlExecutor();
String sqlTemplate = """
INSERT INTO audit_log (
id, type, description, tenant_id, client_id, user_id, ...
) VALUES (
?, ?, ?, ?, ...
);
""";
// MySQL固有: UUIDキャストなし
// ...
}
}
AuditLogSqlExecutors.java:
// 4. ファクトリ(ディスパッチャー)
public class AuditLogSqlExecutors {
Map<DatabaseType, AuditLogSqlExecutor> executors;
public AuditLogSqlExecutors() {
executors = new HashMap<>();
executors.put(DatabaseType.POSTGRESQL, new PostgresqlExecutor());
executors.put(DatabaseType.MYSQL, new MysqlExecutor());
}
public AuditLogSqlExecutor get(DatabaseType databaseType) {
return executors.get(databaseType);
}
}
EntryServiceレイヤーで @Transaction アノテーションにより制御:
// 書き込み(Primary)
@Transaction
public class OAuthFlowEntryService { ... }
// 読み取り専用(Replica)
@Transaction(readOnly = true)
public class UserinfoEntryService { ... }
@Transaction(readOnly = true)
public class OidcMetaDataEntryService { ... }
| 操作 | PostgreSQL | MySQL |
|---|---|---|
| UPSERT | ON CONFLICT (id) DO UPDATE SET ... | ON DUPLICATE KEY UPDATE ... |
| INSERT IGNORE | ON CONFLICT DO NOTHING | INSERT IGNORE INTO ... |
| 操作 | PostgreSQL | MySQL |
|---|---|---|
| UUIDキャスト | ?::uuid | ? (文字列として保存) |
| 操作 | PostgreSQL | MySQL |
|---|---|---|
| JSON型 | jsonb | JSON |
| JSON抽出 | column->>'key' | JSON_UNQUOTE(JSON_EXTRACT(column, '$.key')) |
| JSON検索 | column @> '{"key": "value"}' | JSON_CONTAINS(column, '"value"', '$.key') |
| 操作 | PostgreSQL | MySQL |
|---|---|---|
| 現在時刻 | NOW() | NOW() |
| タイムゾーン | AT TIME ZONE 'UTC' | CONVERT_TZ(..., '+00:00', 'UTC') |
| 間隔 | INTERVAL '1 day' | INTERVAL 1 DAY |
PostgreSQL: V{major}_{minor}_{patch}__{description}.sql
MySQL: V{major}_{minor}_{patch}__{description}.mysql.sql
例:
- postgresql/V1_2_0__add_user_status_column.sql
- mysql/V1_2_0__add_user_status_column.mysql.sql
注意: MySQLは .mysql.sql 接尾辞が必須。
# Docker経由(推奨)
docker compose up flyway-migrator
# 直接実行
./gradlew :libs:idp-server-database:flywayMigrate
PostgreSQL と MySQL で同じバージョン番号のファイルを作成:
libs/idp-server-database/
├── postgresql/
│ └── V1_2_0__add_user_status_column.sql
└── mysql/
└── V1_2_0__add_user_status_column.mysql.sql
datasource/{domain}/
├── command/ # 書き込み操作
│ ├── {Domain}CommandSqlExecutor.java
│ ├── {Domain}CommandSqlExecutors.java
│ ├── PostgresqlExecutor.java
│ └── MysqlExecutor.java
└── query/ # 読み取り操作
├── {Domain}QuerySqlExecutor.java
├── {Domain}QuerySqlExecutors.java
├── PostgresqlExecutor.java
└── MysqlExecutor.java
DatabaseType.java:
public enum DatabaseType {
POSTGRESQL,
SPANNER, // Google Cloud Spanner
MYSQL;
}
# ビルド
./gradlew :libs:idp-server-core-adapter:compileJava
./gradlew :libs:idp-server-database:build
# マイグレーション確認
./gradlew :libs:idp-server-database:flywayInfo
# テスト
./gradlew :libs:idp-server-core-adapter:test
新規DataSource実装時:
ORDER BY created_at DESC だけでは、同一created_atのレコード間でページ境界が非決定的。ORDER BY created_at DESC, id DESC のようにPKを追加して完全に決定的にする。-- NG: 全行JOIN→GROUP BY→LIMIT(200万行で3秒)
SELECT idp_user.*, roles, permissions
FROM idp_user
LEFT JOIN idp_user_roles ON ...
WHERE idp_user.tenant_id = ?
GROUP BY idp_user.id
ORDER BY idp_user.created_at DESC, idp_user.id DESC
LIMIT ? OFFSET ?;
-- OK: CTEで先にLIMIT→JOIN(200万行で185ms)
WITH paged_users AS (
SELECT id, created_at FROM idp_user
WHERE tenant_id = ?
ORDER BY created_at DESC, id DESC
LIMIT ? OFFSET ?
)
SELECT idp_user.*, roles, permissions
FROM paged_users
JOIN idp_user ON idp_user.id = paged_users.id
LEFT JOIN idp_user_roles ON ...
WHERE idp_user.id IN (SELECT id FROM paged_users)
GROUP BY idp_user.id
ORDER BY idp_user.created_at DESC, idp_user.id DESC;
role/permissionなどJOINテーブルでフィルタする場合、CTEの外に条件を残すとページネーションが壊れる(LIMIT後にさらに絞り込まれて件数が減る)。CTE内にJOINを含めて正しく絞り込んでからLIMITする。
if (hasRoleOrPermissionFilter) {
cteFrom = """
SELECT DISTINCT idp_user.id, idp_user.created_at FROM idp_user
LEFT JOIN idp_user_roles ON idp_user.id = idp_user_roles.user_id
LEFT JOIN role ON idp_user_roles.role_id = role.id
...
""";
} else {
cteFrom = "SELECT id, created_at FROM idp_user ";
}
authentication_transaction のように状態遷移を持つリソースに対して、複数リクエストが同時にDELETEを実行すると、ON DELETE CASCADE による子テーブル(authentication_interactions)の行削除順序が異なり、PostgreSQLでデッドロックが発生する。
Process A: DELETE authentication_transaction → CASCADE DELETE interactions (row 1, row 2)
Process B: DELETE authentication_transaction → CASCADE DELETE interactions (row 2, row 1)
→ 循環待ち → deadlock detected
親テーブルの行を SELECT FOR UPDATE で先にロックし、並行操作を直列化する。
// QueryRepository インターフェース
AuthenticationTransaction getForUpdate(Tenant tenant, AuthenticationTransactionIdentifier identifier);
AuthenticationTransaction getForUpdate(Tenant tenant, AuthorizationIdentifier identifier);
-- PostgreSQL
SELECT ... FROM authentication_transaction
WHERE id = ?::uuid AND tenant_id = ?::uuid
FOR UPDATE
-- MySQL(同一構文で動作)
SELECT ... FROM authentication_transaction
WHERE id = ? AND tenant_id = ?
FOR UPDATE
@Transaction 内で getForUpdate() により再取得し、以降の処理はロック済みオブジェクトを使う。
@Transaction
public class CibaFlowEntryService {
public AuthenticationInteractionRequestResult interact(
..., AuthenticationTransaction authenticationTransaction, ...) {
// 引数の authenticationTransaction はトランザクション外で取得済み。
// @Transaction 内で FOR UPDATE 付きで再取得して悲観ロックを取る。
AuthenticationTransaction lockedTransaction =
authenticationTransactionQueryRepository.getForUpdate(
tenant, authenticationTransaction.identifier());
// 以降は lockedTransaction を使い、元の authenticationTransaction は使わない
}
}
状態遷移を持ち、並行書き込み(DELETE/UPDATE)が発生しうるリソースに適用:
| EntryService | メソッド | 理由 |
|---|---|---|
CibaFlowEntryService | interact() | FIDO-UAF認証完了 + キャンセルの同時到着 |
OAuthFlowEntryService | interact(), authorize(), deny(), callbackFederation() | 二重クリック、タブ重複 |
UserOperationEntryService | interact() | デバイス認証操作の並行実行 |
SELECT FOR UPDATE はトランザクション内でのみ有効(@Transaction 必須)get() / findList())には FOR UPDATE を付けないSqlErrorClassifier がDB固有のエラーコードを統一分類し、SqlExecutor が適切な例外に変換する。
| SqlError | PostgreSQL | MySQL | 例外クラス | HTTP |
|---|---|---|---|---|
UNIQUE_VIOLATION | 23505 | 1062 | SqlDuplicateKeyException | 409 |
FK_VIOLATION | 23503 | 1451/1452 | SqlForeignKeyViolationException | 404 |
NOT_NULL_VIOLATION | 23502 | 1048 | SqlBadRequestException | 400 |
CHECK_VIOLATION | 23514 | 3819 | SqlBadRequestException | 400 |
DEADLOCK_DETECTED | 40P01 | 1213 | SqlTransactionConflictException | 409 |
SERIALIZATION_FAILURE | 40001 | 1205 | SqlTransactionConflictException | 409 |
OTHER | — | — | SqlRuntimeException | 500 |
authentication_transaction がCASCADE DELETEされた後に、別トランザクションが子テーブルへINSERT/UPDATEしようとした場合に発生。ApiExceptionHandler で「セッション期限切れ」として404を返す。
SELECT FOR UPDATE を導入してもデッドロックが完全にゼロにはならない(想定外のロック順序、アプリケーション外からの操作等)。SqlTransactionConflictException → 409でクライアントにリトライ可能であることを伝える。
| 問題 | 原因 | 解決策 |
|---|---|---|
| PostgreSQLで動くがMySQLでエラー | DB固有構文の混在 | 両Executor実装を確認 |
| UUIDキャストエラー | ?::uuidがMySQLで失敗 | MySQL版では文字列として扱う |
| JSONクエリ失敗 | JSON関数の差異 | 上記JSON操作表を参照 |
| 問題 | 原因 | 解決策 |
|---|---|---|
deadlock detected (PostgreSQL 40P01) | CASCADE DELETE で子テーブルの行ロック順序が競合 | 親テーブルに SELECT FOR UPDATE を適用 |
Lock wait timeout exceeded (MySQL 1205) | FOR UPDATE のロック保持時間が長い | 外部サービス呼び出しをロック区間外に分離検討 |
| FK violation after concurrent DELETE | 別トランザクションが先に親行を削除済み | SqlForeignKeyViolationException で404返却 |
| 問題 | 原因 | 解決策 |
|---|---|---|
| MySQLでファイル認識されない | 接尾辞が.sql | .mysql.sqlに変更 |
| バージョン競合 | 既存マイグレーションとの衝突 | flyway repair 実行 |
| スキーマ不整合 | 手動変更との不一致 | docker compose down -v で初期化 |
public interface CacheStore {
<T> void put(String key, T value);
<T> void put(String key, T value, int timeToLiveSeconds);
<T> Optional<T> find(String key, Class<T> type);
boolean exists(String key);
void delete(String key);
long increment(String key, int timeToLiveSeconds); // Lua スクリプトによるアトミックカウンター
}
実装: JedisCacheStore(Jedis ベース)。全操作で例外はログ出力のみ(non-blocking)。
increment)| 要件 | AesCipher | HmacHasher |
|---|---|---|
| 元の値を復元する必要がある | ✓ (decrypt) | ✗ |
| 検索キーとして使う | ✗ | ✓ (hash して比較) |
| 機密データ保存 | ✓ | ✗ |
| PII(email, phone) | ✓ (復号して表示) | ✗ |
EncryptedData(ciphertext + IV)を返すPostgreSQL で行レベルのテナント分離を実現。
ALTER TABLE client_configuration ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON client_configuration
USING (tenant_id = current_setting('app.tenant_id')::UUID);
TransactionManager が SET LOCAL app.tenant_id = '{tenantId}' でコネクションレベルの設定を行い、RLS ポリシーが自動でフィルタリングする。LOCAL スコープのためトランザクション終了時に自動クリア。
MySQL では RLS 非対応のため、アプリケーション層での明示的 WHERE 句が必要。
| スキル | 用途 |
|---|---|
/ops-local-env | ローカルDB環境構築 |
/dev-architecture | アーキテクチャ全体像 |
/ops-deployment | 運用・監視 |
UserInfoエンドポイント(UserInfo Endpoint)機能の開発・修正を行う際に使用。UserInfo claims、scopeフィルタリング、verified_claims実装時に役立つ。
外部API認証ユースケースの設定ガイド。外部API連携(認証委譲、リスク判定、OTP等)の interaction 設計、identity_match_field、MFA 2段階目、previous_interaction のヒアリングと設定JSONを提供。
ユースケース別セットアップのエントリポイント。ユーザーにユースケースを選択してもらい、対応するスキル(use-case-login, use-case-mfa等)にルーティングする。共通ワークフロー、前提条件、組み合わせパターンの概要を提供。
認証機能(Authentication Policy, MFA)の開発・修正を行う際に使用。認証ポリシー、パスワード、OTP、FIDO2、条件付き認証実装時に役立つ。
セキュリティ・脆弱性対策の開発・テストを行う際に使用。OAuth/OIDC攻撃対策、認証識別子切り替え攻撃、Session Fixation、マルチテナント分離、セキュリティテスト実装時に役立つ。
外部サービス連携(External Service Integration)機能の開発・修正を行う際に使用。HTTP Request Executor, MappingRule, OAuth/HMAC認証実装時に役立つ。