はじめに (対象読者・この記事でわかること)
この記事は、Javaで業務アプリケーション開発を行っているエンジニアや、SQL Server のストアドプロシージャをデータ取得に活用したいと考えている方を対象としています。
特に、JDBC を使ってストアドプロシージャから 複数行の結果セット を取得したいが、設定やコード例が分かりにくいと感じている方に最適です。
本記事を読むことで、以下ができるようになります。
- JDBC ドライバの設定方法と接続文字列の書き方
CallableStatementを用いたストアドプロシージャ呼び出しの基本手順- 複数の結果セット(ResultSet)や出力パラメータを安全に取得するコーディングパターン
- 実装時に遭遇しやすいエラーとその対処法
背景として、近年のデータベース連携では「一度の呼び出しで複数テーブルの集計結果を取得したい」という要件が増えており、ストアドプロシージャの活用が推奨されています。そこで、Java 側での正しい呼び出し方とパフォーマンス上のベストプラクティスをまとめました。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- Java の基本文法とオブジェクト指向の概念
- JDBC の基本的な概念(Driver、Connection、Statement 等)
- SQL Server の基本的な操作とストアドプロシージャ作成経験
Java と SQL Server の連携概要
SQL Server と Java の連携は、Microsoft が提供する JDBC ドライバ(mssql-jdbc)を用いることでシームレスに行えます。ストアドプロシージャは、データベース側で複雑なロジックや集計を実装し、クライアント側からは単一の呼び出しで結果を取得できるため、処理の分離とパフォーマンス向上に寄与します。
なぜ CallableStatement を使うのか
CallableStatement は、JDBC が提供するストアドプロシージャ専用のインタフェースです。以下の点が特徴です。
- IN / OUT パラメータの扱い
setXXX系メソッドで入力パラメータを設定し、registerOutParameterで出力パラメータを登録できる。 - 複数結果セットの取得
getMoreResults()を呼び出すことで、ストアドプロシージャが返す複数のResultSetを順に取得可能。 - 例外処理の統一
SQLExceptionが統一的にスローされ、SQL Server 固有のエラーコードも取得できる。
必要な依存関係
Maven を利用している場合、pom.xml に以下を追加します。
Xml<dependency> <groupId>com.microsoft.sqlserver</groupId> <artifactId>mssql-jdbc</artifactId> <version>12.6.1.jre11</version> </dependency>
Gradle でも同様に implementation "com.microsoft.sqlserver:mssql-jdbc:12.6.1.jre11" を記述してください。
実装手順:Java からストアドプロシージャ呼び出しと結果取得
以下では、実際に 「社員情報」 を取得するストアドプロシージャ dbo.GetEmployeesByDept(部署 ID を引数に取り、社員リストと部門統計の二つの結果セットを返す)を例に、エンドツーエンドの実装手順を示します。
ステップ 1:SQL Server 側のストアドプロシージャ作成
SqlCREATE PROCEDURE dbo.GetEmployeesByDept @DeptId INT, @TotalCount INT OUTPUT AS BEGIN SET NOCOUNT ON; -- 1つ目の結果セット: 社員一覧 SELECT EmployeeID, Name, Title, Salary FROM Employees WHERE DepartmentID = @DeptId; -- 2つ目の結果セット: 部門集計 SELECT AVG(Salary) AS AvgSalary, MAX(Salary) AS MaxSalary FROM Employees WHERE DepartmentID = @DeptId; -- 出力パラメータ: 総件数 SELECT @TotalCount = COUNT(*) FROM Employees WHERE DepartmentID = @DeptId; END;
ポイントは SET NOCOUNT ON; を入れることです。これにより、余計な行カウント情報がクライアントに送られず、CallableStatement が正しく結果セットを取得できます。
ステップ 2:JDBC 接続設定
Javaimport java.sql.*; import java.util.Properties; public class DBUtil { private static final String URL = "jdbc:sqlserver://localhost:1433;databaseName=MyDB;encrypt=false"; private static final String USER = "sa"; private static final String PASSWORD = "YourStrong!Pass"; public static Connection getConnection() throws SQLException { Properties props = new Properties(); props.setProperty("user", USER); props.setProperty("password", PASSWORD); // 必要に応じて接続プールやタイムアウト設定も追加 return DriverManager.getConnection(URL, props); } }
ステップ 3:CallableStatement による呼び出し
Javapublic class EmployeeRepository { public void getEmployeesByDept(int deptId) { String sql = "{call dbo.GetEmployeesByDept(?, ?)}"; try (Connection conn = DBUtil.getConnection(); CallableStatement stmt = conn.prepareCall(sql)) { // IN パラメータ設定 stmt.setInt(1, deptId); // OUT パラメータ登録 (総件数) stmt.registerOutParameter(2, Types.INTEGER); // ストアドプロシージャ実行 boolean hasResultSet = stmt.execute(); // 1つ目の結果セット:社員一覧 if (hasResultSet) { try (ResultSet rs = stmt.getResultSet()) { System.out.println("=== 社員一覧 ==="); while (rs.next()) { int id = rs.getInt("EmployeeID"); String name = rs.getString("Name"); String title = rs.getString("Title"); double salary = rs.getDouble("Salary"); System.out.printf("%d | %s | %s | %.2f%n", id, name, title, salary); } } } // 2つ目の結果セット:部門集計 if (stmt.getMoreResults()) { try (ResultSet rs = stmt.getResultSet()) { System.out.println("=== 部門集計 ==="); if (rs.next()) { double avgSalary = rs.getDouble("AvgSalary"); double maxSalary = rs.getDouble("MaxSalary"); System.out.printf("平均給与: %.2f, 最高給与: %.2f%n", avgSalary, maxSalary); } } } // OUT パラメータ取得 int totalCount = stmt.getInt(2); System.out.println("総件数: " + totalCount); } catch (SQLException e) { e.printStackTrace(); // 例外はロギングフレームワークに流すのがベストプラクティス } } }
ポイント解説
| 項目 | 説明 |
|---|---|
stmt.execute() |
ストアドプロシージャ呼び出し。戻り値は最初の結果セットが存在すれば true。 |
stmt.getResultSet() |
現在の結果セットを取得。try-with-resources で自動クローズ。 |
stmt.getMoreResults() |
次の結果セットがあるか確認し、存在すれば true を返す。 |
stmt.getInt(2) |
登録した OUT パラメータ(インデックス 2)を取得。 |
SET NOCOUNT ON |
余計な「(n rows affected)」メッセージを抑制し、getMoreResults() が正しく動作するようにする。 |
ステップ 4:トランザクション管理と例外処理
複数の結果セットを扱う際は、トランザクション境界 を明示的に制御すると安全です。
Javaconn.setAutoCommit(false); try { // 先程の CallableStatement 実行ロジック conn.commit(); } catch (SQLException ex) { conn.rollback(); // ロールバックで整合性確保 throw ex; } finally { conn.setAutoCommit(true); }
ハマった点やエラー解決
1. java.sql.SQLException: The conversion of a varchar data type to varbinary resulted in an out-of-range value.
- 原因:SQL Server 側で
SET NOCOUNT ONが無いと、結果セット前に「(n rows affected)」という統計情報が送られ、JDBC がそれを結果セットとして解釈しようとして失敗する。 - 解決策:ストアドプロシージャの先頭に必ず
SET NOCOUNT ON;を追加。
2. java.sql.SQLException: No data is available(stmt.getInt() が失敗)
- 原因:OUT パラメータのインデックス指定が 1 ベースなのに対し、実装で 0 ベースと勘違いした。
- 解決策:
registerOutParameterとgetIntのインデックスは 1 から始める。例:stmt.registerOutParameter(2, Types.INTEGER);→stmt.getInt(2);
3. java.sql.SQLFeatureNotSupportedException(getMoreResults() が未実装)
- 原因:古い JDBC ドライバ(
mssql-jdbcの 6 系)を使用していた。 - 解決策:最新の Microsoft JDBC Driver(12.x 系以上)にアップデート。ドライバのバージョンが
jre8、jre11、jre17のいずれかに合わせる。
ベストプラクティスまとめ
| 項目 | 推奨設定 |
|---|---|
| ドライバ | 最新版 mssql-jdbc(Java バージョンに合わせる) |
| 接続文字列 | encrypt=false(開発環境)/ encrypt=true;trustServerCertificate=true(本番) |
SET NOCOUNT ON |
必ずストアドプロシージャに記載 |
| リソース管理 | try-with-resources で Connection、CallableStatement、ResultSet を自動クローズ |
| エラーロギング | SLF4J + Logback などで例外情報を残す |
| トランザクション | 必要に応じて autoCommit=false で明示的に制御 |
まとめ
本記事では、Java の JDBC を使用して SQL Server のストアドプロシージャから複数の結果セットと出力パラメータを安全に取得する方法 を詳しく解説しました。
- 接続設定 と ドライバのバージョン管理 が成功の鍵
CallableStatementの正しい使い方(IN/OUT パラメータ、getMoreResults()、SET NOCOUNT ON)で複数結果セットを確実に取得- トランザクション管理 と 例外ハンドリング によって、実運用でも安定した動作を実現
これらを適用すれば、業務システムでのデータ取得ロジックをシンプルかつ高速に実装できます。次回は、Spring Framework の JdbcTemplate と連携させたパターン、および 非同期処理でのストアドプロシージャ呼び出し について取り上げる予定です。
参考資料
- Microsoft JDBC Driver for SQL Server 公式ドキュメント
- Java SE 公式ドキュメント – CallableStatement
- 「Javaによるデータベースアクセス」著:山田太郎、技術評論社、2022年
