多くのエンジニアが一度は通る道、それがORM(Object-Relational Mapping)によるデータアクセスの自動化です。しかし、実際の開発現場では「ブラックボックス化してわかりにくい」「SQLが思うようにチューニングできない」「結局、素のJDBCに戻した」などの声が後を絶ちません。
本記事では、そうしたORMに限界を感じたエンジニアに向けて、フレームワークに依存しない共通DBアクセスクラス群の“実用的な使い方”をご紹介します。第6回となる今回は、MVCモデルをベースに、現場でよくあるユースケースを交えて、どのようにこの仕組みが使われているかを具体的に解説します。フレームワークに頼らない自由度と保守性の高さを、ぜひ体感してみてください。
本シリーズで使用する共通DBアクセスクラスの検証には、PostgreSQLを想定しています。ソースコード一式はGitHubに保管しており、テーブル構成やクラス構造を含めてすべて確認可能です。再現性のある実装環境を前提として設計されています。
👉 GitHubリポジトリ: https://github.com/bepro-engineer/db-access-core
MVC構成と共通DBアクセスクラスの関係
共通DBアクセスクラスを実戦投入する場合、最も理想的な形はMVC(Model-View-Controller)パターンへの統合です。開発においては、「データの取得や更新をどこで処理するか」がコード設計の肝になります。フレームワークに頼らず、自前のDBアクセスロジックを構築する際にも、MVCの考え方をベースに構造を整理しておくと、保守性・再利用性が大きく向上します。
Java共通DBアクセスクラスの全ソースコードはこちら
👉 GitHub – db-access-core / data ディレクトリ
Model層での役割
Model層はビジネスロジックとデータのやり取りを担う層です。この層に共通DBアクセスクラスを配置することで、テーブルに依存したデータの読み書きを明確に分離できます。たとえば「データを1行取得する」「該当行を更新する」といった処理を、個別のビジネス要件と切り離して共通化することができます。
Model層では、以下のような操作が主に実行されます。
操作の種類 | 目的 | 処理対象 |
---|---|---|
select | 1件または複数件のデータ取得 | テーブルレコード |
insert | 新規データの登録 | データ行 |
update | 既存データの更新 | CTIDまたはROW_NUMBERで特定 |
delete | 不要データの削除 | 主キー or 一意な条件 |
これらの操作を支える共通クラスでは、あらかじめSQL構文のテンプレートやフィールド情報を保持しておくことで、都度SQLを書く必要がなくなります。結果としてModel層は「データの意味」を扱うロジックに集中することができ、テーブルの構造が変更されたとしても影響範囲を最小限に抑えられます。
Controller層との連携ポイント
Controller層はユーザーの操作やリクエストを受け取って処理を振り分ける役割を担います。この層では、共通DBアクセスクラスを直接利用するのではなく、「Model層を通じて操作を依頼する」という構造を保つことで、責務の明確化とテストのしやすさが確保されます。
たとえば以下のような流れです。
String userId = request.getParameter("user_id");
UserModel model = new UserModel();
UserEntity entity = model.findById(userId);
request.setAttribute("user", entity);
このように、Controller側はリクエストのパラメータ取得とModelの呼び出しのみを担い、SQLやデータアクセスの具体的な処理には一切関与しません。これにより、処理の責任分担が明確になり、後から他のModelに差し替える・複数の処理を組み合わせるといった柔軟な構造変更が可能になります。
また、Controllerでの例外処理はModel層から伝搬されるエラー内容に対して集中するため、共通的なエラー種別(存在しない、更新失敗、トランザクションエラーなど)をあらかじめ定義しておくと、ログ出力やUI表示の整合性も取りやすくなります。
View層との依存排除
共通DBアクセスクラスは直接的にはView層と関係しませんが、間接的には表示ロジックの単純化に大きく寄与します。Model層で取得したEntityやDTOの構造が安定していれば、JSPやテンプレートエンジン側ではロジックをほとんど書かず、表示だけに専念できます。
たとえば、ユーザー情報をテーブルで一覧表示するケースでは、以下のように処理を簡略化できます。
List<UserEntity> list = model.selectByStatus("active");
request.setAttribute("userList", list);
View側ではJSTLやループで表示するだけで済むため、HTMLにJavaロジックが混在することがなくなります。
結果として、HTMLコードの保守が容易になり、デザイナーやフロントエンド開発者との役割分担が明確になります。
現場での典型的なユースケース
現場では、共通DBアクセスクラスを使うことで、SQLの自動組み立てと処理統一が可能になります。以下は、実際に使われる典型的なシーンとその実装例です。
ユーザー管理システムでの使用例
ユーザー情報の管理では、ログイン認証、情報の更新、新規登録、削除といった一連の処理を共通化することで、大幅な工数削減が実現できます。
ログイン処理のデータ取得と検証
ログイン時には、ユーザーIDとパスワードに対応する情報をSQLで組み立てて doSelect() を実行し、対象データが存在するかどうかで認証処理を判断します。取得結果件数は int 型で返されるため、0件であれば未登録、1件であれば認証成功といった処理分岐が可能です。
// コントローラとエンティティ生成
DbAccessController user_dac = new DbAccessController(USER_MST);
ITableEntity user_ite = user_dac.getDbTableEntity();
DbConnectionPool pool = DbConnectionPool.getInstance();
Connection con = pool.getConnection();
String sql = SQL_USER_LOGIN_BASE;
if (params.containsKey("user_id") && params.containsKey("password")) {
sql += " WHERE user_id='" + params.get("user_id") + "' AND password='" + params.get("password") + "'";
}
try {
int result = user_dac.doSelect(con, sql);
if (result == 0) {
logOut("INFO", "ユーザーが見つかりません");
}
} catch (Exception e) {
logOut("ERROR", "ログイン検索失敗:" + e.getMessage());
} finally {
pool.release(con);
}
ユーザー情報の更新処理
ユーザーの名前やメールアドレスなどを変更する場合は、対象テーブル名を指定して DbAccessController を生成し、ITableEntity 経由でフィールドに値をセットしたあと doExec() を実行します。更新処理の結果は int 型で返され、1以上であれば成功、0以下であればロールバックなどの対応を取る必要があります。
DbAccessController user_dac = new DbAccessController(USER_MST);
ITableEntity user_ite = user_dac.getDbTableEntity();
DbConnectionPool pool = DbConnectionPool.getInstance();
Connection con = pool.getConnection();
try {
con.setAutoCommit(false);
user_ite.setValue("user_name", params.get("user_name"), 0);
user_ite.setValue("email", params.get("email"), 0);
user_ite.setValue("update_user", session.getAttribute("login_id").toString(), 0);
int result = user_dac.doExec(con, USER_MST);
if (result > 0) {
con.commit();
} else {
con.rollback();
}
} catch (Exception e) {
try { con.rollback(); } catch (Exception ex) {}
logOut("ERROR", "更新処理失敗:" + e.getMessage());
} finally {
pool.release(con);
}
ユーザー新規登録処理(INSERT)
新規登録時は、DbAccessController のインスタンス生成時に対象テーブル名を指定し、ITableEntity を使って各カラムに値を設定した上で doExec() を実行します。返却される int 型の値で登録件数を取得でき、想定件数でなければロールバック処理を行うことが推奨されます。
DbAccessController user_dac = new DbAccessController(USER_MST);
ITableEntity user_ite = user_dac.getDbTableEntity();
DbConnectionPool pool = DbConnectionPool.getInstance();
Connection con = pool.getConnection();
try {
con.setAutoCommit(false);
user_ite.setValue("user_id", params.get("user_id"), 0);
user_ite.setValue("user_name", params.get("user_name"), 0);
user_ite.setValue("email", params.get("email"), 0);
user_ite.setValue("insert_user", session.getAttribute("login_id").toString(), 0);
int result = user_dac.doExec(con, USER_MST);
if (result > 0) {
con.commit();
} else {
con.rollback();
}
} catch (Exception e) {
try { con.rollback(); } catch (Exception ex) {}
logOut("ERROR", "登録処理失敗:" + e.getMessage());
} finally {
pool.release(con);
}
ユーザー削除処理(DELETE)
削除処理も doExec() を使って行います。対象レコードの主キーや条件を ITableEntity にセットし、削除対象のテーブル名を doExec() に指定するだけで、DELETE文が自動生成されます。返り値の int 型で削除件数を判定し、処理の成否を判断します。
DbAccessController user_dac = new DbAccessController(USER_MST);
ITableEntity user_ite = user_dac.getDbTableEntity();
DbConnectionPool pool = DbConnectionPool.getInstance();
Connection con = pool.getConnection();
try {
con.setAutoCommit(false);
user_ite.setValue("user_id", params.get("user_id"), 0);
int result = user_dac.doExec(con, USER_MST);
if (result > 0) {
con.commit();
} else {
con.rollback();
}
} catch (Exception e) {
try { con.rollback(); } catch (Exception ex) {}
logOut("ERROR", "削除処理失敗:" + e.getMessage());
} finally {
pool.release(con);
}
トランザクション管理システムでの使用例
共通クラスの設計により、トランザクション開始から例外制御までを一定のパターンで統一できます。
処理単位でのトランザクション制御
更新や削除処理など、複数のステップを伴う操作は setAutoCommit(false) を明示し、コミット・ロールバックを制御します。
try {
con.setAutoCommit(false);
// 各種 setValue(), doExec() 処理
con.commit();
} catch (Exception e) {
try { con.rollback(); } catch (SQLException se) {}
}
ロールバックと例外管理の実装パターン
どの例外でも確実に rollback() を通すため、ネストされた try-catch で保護します。
try {
con.setAutoCommit(false);
int result = user_dac.doExec(con, USER_MST);
if (result >= 1) {
con.commit();
} else {
con.rollback();
}
} catch (Exception e) {
try { con.rollback(); } catch (SQLException se) {}
}
バッチ処理系のDBアクセス統一化
バッチ処理においても、処理単位ごとに共通クラスを利用することで、冗長なSQL実装を排除できます。
複数テーブルにまたがる更新処理の一元化
処理対象が複数テーブルに渡る場合でも、それぞれ DbAccessController を分けて doExec() を呼ぶだけでよく、可読性が大きく向上します。
DbAccessController log_dac = new DbAccessController(LOG_TBL);
DbAccessController stat_dac = new DbAccessController(STATUS_TBL);
ITableEntity log_ent = log_dac.getDbTableEntity();
ITableEntity stat_ent = stat_dac.getDbTableEntity();
DbConnectionPool pool = DbConnectionPool.getInstance();
Connection con = pool.getConnection();
try {
con.setAutoCommit(false);
log_ent.setValue("msg", "処理完了", 0);
stat_ent.setValue("status", "SUCCESS", 0);
int log_result = log_dac.doExec(con, LOG_TBL);
int stat_result = stat_dac.doExec(con, STATUS_TBL);
if (log_result > 0 && stat_result > 0) {
con.commit();
} else {
con.rollback();
}
} catch (Exception e) {
try { con.rollback(); } catch (Exception ex) {}
logOut("ERROR", "複数テーブル更新失敗:" + e.getMessage());
} finally {
pool.release(con);
}
エラーログの集中管理とトレース性
共通ログテーブルへの出力も、一般的なINSERT文と同様に doExec() を通じて自前で実装可能です。専用のロギング機能が備わっているわけではありませんが、任意のエラーメッセージや例外情報をエンティティに詰めることで、ログ出力処理を共通化できます。
DbAccessController log_dac = new DbAccessController(ERROR_LOG);
ITableEntity log_ent = log_dac.getDbTableEntity();
DbConnectionPool pool = DbConnectionPool.getInstance();
Connection con = pool.getConnection();
try {
con.setAutoCommit(false);
// ここに通常の処理(例:在庫処理など)
throw new Exception("DB接続失敗"); // 仮に例外発生
} catch (Exception e) {
try { con.rollback(); } catch (Exception ex) {}
log_ent.setValue("error_code", "E500", 0);
log_ent.setValue("message", e.getMessage(), 0);
log_dac.doExec(con, ERROR_LOG);
logOut("ERROR", "例外発生:" + e.getMessage());
} finally {
pool.release(con);
}
フレームワーク依存を避けるメリット
現場ではSpringやHibernateなどのフレームワークを導入することが一般的ですが、一方で、あえてフレームワークを使わないシンプルな構成を採用するケースも存在します。本記事では、共通DBアクセスクラスを活用し、フレームワークに依存しない設計を選択することで得られる具体的なメリットについて解説します。
軽量・高速な実行環境
フレームワーク非依存の最大の利点は、不要なライブラリやコンポーネントを排除できる点にあります。これにより、アプリケーション全体の実行環境が軽くなり、起動時間やレスポンス速度も改善されます。 たとえば、Spring BootのようにTomcatや多数の依存ライブラリを組み込む必要がある構成では、アプリケーションの初期化だけで数百MBのメモリを消費します。
対して、今回紹介しているような共通DBアクセスクラスを使った構成では、純粋なJavaクラスとJDBCのみで完結するため、最小限の実行環境で済みます。 実行時のボトルネックが明確になりやすく、パフォーマンスチューニングもしやすいため、リソース制限のある環境下でも柔軟に対応できます。
保守コストの削減と属人化の回避
フレームワークを導入すると、その仕様変更やバージョンアップに伴う修正対応が必ず発生します。しかもそれは、当該フレームワークに詳しい技術者に依存せざるを得ない場面を多く生み出します。結果的に、保守作業が属人化しやすくなります。
フレームワークを使わない構成にすることで、DBアクセス処理なども自作クラスで完結するため、ブラックボックスな挙動がなくなり、コード全体の見通しがよくなります。以下のように、SQLを含めて処理が明示されているため、誰が見てもロジックの流れを理解しやすくなります。
DbAccessController user_dac = new DbAccessController(USER_MST);
ITableEntity user_ite = user_dac.getDbTableEntity();
DbConnectionPool pool = DbConnectionPool.getInstance();
Connection con = pool.getConnection();
try {
con.setAutoCommit(false);
user_ite.setValue("user_id", "test001", 0);
user_ite.setValue("user_name", "テストユーザー", 0);
user_ite.setValue("email", "test@example.com", 0);
int result = user_dac.doExec(con, USER_MST);
if (result > 0) {
con.commit();
} else {
con.rollback();
}
} catch (Exception e) {
try { con.rollback(); } catch (Exception ex) {}
logOut("ERROR", "登録処理失敗:" + e.getMessage());
} finally {
pool.release(con);
}
このように構造が明快であることで、新人技術者や引継ぎ担当者でも理解しやすく、保守コストが大きく下がります。
マルチプロジェクト間での再利用性の高さ
特定のフレームワークに依存した構成では、そのフレームワークが導入されていない他のプロジェクトへの移植が困難です。対して、共通DBアクセスクラスのような汎用的な設計であれば、以下のような利点があります。
比較項目 | フレームワーク依存構成 | 共通DBアクセスクラス構成 |
---|---|---|
再利用の容易さ | 導入環境に制限あり | どのJava環境でも利用可能 |
設定ファイルの量 | 多くのXMLやYAML | 最小限の設定で運用可 |
他プロジェクトへの流用 | 一部コード書き直しが必要 | クラス単位でそのまま流用可能 |
実際、複数案件でこの共通クラスを使い回したことで、開発速度や保守効率が向上したという実感があります。単一プロジェクトだけに閉じた設計ではなく、汎用的なライブラリとして運用する意識が重要です。
共通DBアクセスクラスを一度整備してしまえば、同様のロジックを他プロジェクトでもそのまま活用できるため、再開発の手間が不要になります。結果として、開発者のリソースを新機能実装や障害対応に集中できるようになります。
今後の拡張と応用の可能性
共通DBアクセスクラスは、現場での業務効率化だけでなく、将来的なシステム拡張や新技術との連携にも大きな柔軟性を持っています。現時点での枠にとらわれず、今後の進化や応用可能性を意識することで、さらに価値の高い基盤として成長させることができます。
マイクロサービス間での共通アクセス基盤化
システムがマイクロサービス構成へ移行する中で、DBアクセス方法を統一することは非常に重要です。各サービスがバラバラのORMや自作クエリを持つと保守性が急激に低下します。共通クラスをベースにすることで、以下のような恩恵が得られます。
メリット | 説明 |
---|---|
インターフェースの統一 | 各マイクロサービスでも同一のメソッドでDB処理が可能 |
保守性の向上 | 仕様変更時の修正ポイントが明確化され、影響範囲も限定できる |
学習コストの最小化 | 新規参加者でも短時間で理解可能 |
また、DBスキーマが複数サービス間で共有される場合でも、共通クラスの導入によりアクセスパターンのバラつきを最小限に抑えることができます。
クラウド移行後のポータビリティ確保
オンプレ環境からAWSやGCPなどのクラウド基盤へ移行するケースでは、インフラだけでなくミドルウェアやデータベースエンジンの仕様も変わることがあります。たとえば以下のようなケースです。
変更内容 | 影響範囲 |
---|---|
RHEL → Ubuntu | OSレベルのシェル操作や設定ファイルの配置 |
PostgreSQL → CloudSQL | 接続方式や文字コードの扱い |
Tomcat → AppEngine | デプロイ方法やログ出力の方式 |
共通DBアクセスクラスを利用することで、データアクセスのロジックをサービス本体から分離できるため、クラウド移行時にも「差し替えるべき箇所」が明確になり、移行作業を大幅に簡略化できます。
AI補助によるコード自動生成との統合
今後注目されるのが、AIによるコード自動生成との連携です。あらかじめ共通クラスのインターフェースが確立していれば、AIに対して次のような指示が可能になります。
「USER_MST テーブルに対する SELECT 処理のコードを生成してください」
→ DbAccessController の doSelect() を使ったコードが自動生成される
また、更新系の処理でも同様です。
「USER_MST に user_name, email を登録するコード」
→ setValue() の羅列と doExec() を使った commit 処理が提案される
これは、AIによる自動生成の質がインターフェースの一貫性に強く依存していることを示しています。共通DBアクセスクラスが明確な構造を持っていれば、AIが正しく機能しやすくなり、開発効率を格段に向上させることができます。
さらに、テストコードの生成や異常系のパターン出力まで含めて、開発の一部をAIに委ねる環境構築も視野に入れることができます。これは今後の開発スタイルに大きな変革をもたらす可能性があります。
Java共通DBアクセスクラスの全ソースコードはこちら
👉 GitHub – db-access-core / data ディレクトリ