はじめに (対象読者・この記事でわかること)

この記事は、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 が提供するストアドプロシージャ専用のインタフェースです。以下の点が特徴です。

  1. IN / OUT パラメータの扱い
    setXXX 系メソッドで入力パラメータを設定し、registerOutParameter で出力パラメータを登録できる。
  2. 複数結果セットの取得
    getMoreResults() を呼び出すことで、ストアドプロシージャが返す複数の ResultSet を順に取得可能。
  3. 例外処理の統一
    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 側のストアドプロシージャ作成

Sql
CREATE 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 接続設定

Java
import 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 による呼び出し

Java
public 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:トランザクション管理と例外処理

複数の結果セットを扱う際は、トランザクション境界 を明示的に制御すると安全です。

Java
conn.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 availablestmt.getInt() が失敗)

  • 原因:OUT パラメータのインデックス指定が 1 ベースなのに対し、実装で 0 ベースと勘違いした。
  • 解決策registerOutParametergetInt のインデックスは 1 から始める。例: stmt.registerOutParameter(2, Types.INTEGER);stmt.getInt(2);

3. java.sql.SQLFeatureNotSupportedExceptiongetMoreResults() が未実装)

  • 原因:古い JDBC ドライバ(mssql-jdbc の 6 系)を使用していた。
  • 解決策:最新の Microsoft JDBC Driver(12.x 系以上)にアップデート。ドライバのバージョンが jre8jre11jre17 のいずれかに合わせる。

ベストプラクティスまとめ

項目 推奨設定
ドライバ 最新版 mssql-jdbc(Java バージョンに合わせる)
接続文字列 encrypt=false(開発環境)/ encrypt=true;trustServerCertificate=true(本番)
SET NOCOUNT ON 必ずストアドプロシージャに記載
リソース管理 try-with-resourcesConnectionCallableStatementResultSet を自動クローズ
エラーロギング SLF4J + Logback などで例外情報を残す
トランザクション 必要に応じて autoCommit=false で明示的に制御

まとめ

本記事では、Java の JDBC を使用して SQL Server のストアドプロシージャから複数の結果セットと出力パラメータを安全に取得する方法 を詳しく解説しました。

  • 接続設定ドライバのバージョン管理 が成功の鍵
  • CallableStatement の正しい使い方(IN/OUT パラメータ、getMoreResults()SET NOCOUNT ON)で複数結果セットを確実に取得
  • トランザクション管理例外ハンドリング によって、実運用でも安定した動作を実現

これらを適用すれば、業務システムでのデータ取得ロジックをシンプルかつ高速に実装できます。次回は、Spring Framework の JdbcTemplate と連携させたパターン、および 非同期処理でのストアドプロシージャ呼び出し について取り上げる予定です。

参考資料