この連載では、Javaのデータベース・アクセスAPIである「JDBC」の機能を、サンプルコードを交えて解説していきます。また、J2EEにおけるJDBCの位置付けや、JDBCを利用するさまざまなテクノロジについても解説していく予定です。前提知識としては、Javaとリレーショナル・データベースに関するベーシックな知識があれば十分です。
前回は、JDBCでデータベースに接続し、検索(select文)を実行するサンプルを紹介しました。今回は、DDL文やDML文を実行するサンプル・コードを見ながら、これらの処理について解説をしていきます。併せて、例外処理やトランザクションについても、取り上げます。
最初に、DDL(データ定義言語:Data Definition Language)について見ていきましょう。DDL文には、表やビューをはじめとするデータベース・オブジェクトの作成(create文)、変更(alter文)、削除(drop文)などがあります。以下のサンプル・コードでは、SCOTTスキーマに、EMPLOYEE表とPL/SQLストアド・プロシージャを作成しています。
// Javaデータアクセスの基礎 サンプル・コード(2) // DML文を実行するJavaアプリケーション import java.sql.*; class JavaDataAccess02 { public static void main (String args[]) { Connection conn = null; Statement stmt = null; String sql_str = null; try { Class.forName("oracle.jdbc.driver.OracleDriver"); System.out.println("JDBCドライバをロードしました..."); conn = DriverManager.getConnection ("jdbc:oracle:thin:@localhost:1521:ORCL", "scott", "tiger"); System.out.println("データベースに接続しました..."); // ステートメントを作成 stmt = conn.createStatement(); sql_str = "create table EMPLOYEE ( " + " EMPNO NUMERIC(4) primary key, " + " ENAME VARCHAR(10), " + " JOB VARCHAR(9), " + " MGR NUMBER(4), " + " HIREDATE DATE, " + " SAL NUMERIC(7,2), " + " COMM NUMERIC(7,2), " + " DEPTNO NUMERIC(2) " + ")"; stmt.executeUpdate(sql_str); System.out.println("EMPLOYEE表を作成しました..."); sql_str = "create or replace procedure RAISE_SAL ( " + " p_deptno in numeric, " + " p_percent in numeric " + ") " + "is " + "begin " + " update EMPLOYEE " + " set SAL = SAL + (SAL * p_percent * .01) " + " where deptno = p_deptno; " + " commit; " + "end RAISE_SAL; "; stmt.executeUpdate(sql_str); System.out.println("RAISE_SALプロシージャを作成しました..."); // ステートメントをクローズ stmt.close(); conn.close(); System.out.println("接続をクローズしました..."); // 例外を処理 } catch (Exception ex) { System.out.println("Exceptionが発生しました..."); // エラー・メッセージを出力 System.out.print(ex.toString()); // SQLExceptionの場合、エラー・コードを出力 if (ex instanceof SQLException) { System.out.println("エラー・コード: " + ((SQLException)ex).getErrorCode()); } try { // ステートメントがクローズされていない場合、 // それをクローズ if (stmt != null) { stmt.close(); } // 接続がクローズされていない場合、それをクローズ if (conn != null) { conn.close(); System.out.println("接続をクローズしました..."); } } catch (SQLException se) {} } } }
このコードを、2回続けて実行すると、次のようになります。
C:\JDBC>java JavaDataAccess02 JDBCドライバをロードしました... データベースに接続しました... EMPLOYEE表を作成しました... RAISE_SALプロシージャを作成しました... 接続をクローズしました... C:\JDBC>java JavaDataAccess02 JDBCドライバをロードしました... データベースに接続しました... Exceptionが発生しました... java.sql.SQLException: ORA-00955: すでに使用されているオブジェクト名です。 エラー・コード: 955 接続をクローズしました...
では、順を追ってコードを見てみましょう。データベース接続を確立するコードは、前回紹介したサンプル・コード(JavaDataAccess01.java)と同様です。
select文の実行にはexecuteQuery()メソッドを利用しましたが、DDL文の実行にはexecuteUpdate()メソッドを利用します。ここでは、(EMP表とほぼ同じ構造の)EMPLOYEE表、およびストアド・プロシージャ(特定の部門番号(DEPTNO列)の社員の給与(SAL列)の増加率(%単位)を受け取り、EMPLOYEE表をupdateする)を作成しています。
sql_str = "create table EMPLOYEE ..."; stmt.executeUpdate(sql_str); ... sql_str = "create or replace procedure RAISE_SAL ..."; stmt.executeUpdate(sql_str);
select文の場合とは違い、問い合わせ結果はありませんので、executeUpdate()メソッドはResultSetオブジェクトを返しません。
JDBC APIで、データベース・アクセスを行うすべてのメソッドは、データベース・エラーが発生したときに、java.sql.SQLException例外をthrowします。
前回のコード(JavaDataAccess01.java)では、mainメソッドで、SQLException、およびjava.lang.Class.forNameメソッドがthrowするjava.lang.ClassNotFoundExceptionを、そのままthrowさせていました。
public static void main (String args[]) throws SQLException, ClassNotFoundException {
今回のコードでは、例外をcatchし、標準出力にエラー・メッセージを出力しています。
さらに、catchした例外がSQLExceptionの場合には、getErrorCode()メソッドで、データベース・ベンダ固有のエラー・コードを取得し出力しています。
try { ... // 例外を処理 } catch (Exception ex) { System.out.println("Exceptionが発生しました..."); // エラー・メッセージを出力 System.out.print(ex.toString()); // SQLExceptionの場合、エラー・コードを出力 if (ex instanceof SQLException) { System.out.println("エラー・コード: " + ((SQLException)ex).getErrorCode()); }
このコードの2回目の実行時には、EMPLOYEE表がすでに存在しているため、create table文を実行するexecuteUpdate()メソッドでSQLExceptionが発生し、次の出力が生成されます。
java.sql.SQLException: ORA-00955: すでに使用されているオブジェクト名です。 エラー・コード: 955
これを見れば分かるように、Oracleデータベースの場合には、getErrorCode()メソッドが返すエラー・コードは、最大5けたのエラー番号(ORA-xxxxx)となります。
続いて、DML(データ操作言語:Data Manipulation Language)文について見ていきましょう。
DML文には、表やビューなどのデータベース・オブジェクト内のデータに対する、クエリ(select文)、挿入(insert文)、変更(update文)、削除(delete文)などがあります。以下のサンプル・コードでは、EMPLOYEE表に対してさまざまなDML文を実行しています。
// Javaデータアクセスの基礎 サンプル・コード(3) // EMPLOYEE表を更新するJavaアプリケーション // JDBC APIをインポート import java.sql.*; class JavaDataAccess03 { public static void main (String args[]) { Connection conn = null; Statement stmt = null; ResultSet rset = null; String sql_str = null; int count = 0; try { Class.forName("oracle.jdbc.driver.OracleDriver"); System.out.println("JDBCドライバをロードしました..."); conn = DriverManager.getConnection ("jdbc:oracle:thin:@localhost:1521:ORCL", "scott", "tiger"); System.out.println("データベースに接続しました..."); // 自動コミット・モードを設定 conn.setAutoCommit(false); // ステートメントを作成 stmt = conn.createStatement(); sql_str = "delete from EMPLOYEE"; count = stmt.executeUpdate(sql_str); System.out.println("delete文を実行しました..."); System.out.println(" 行数: " + count); sql_str = "insert into EMPLOYEE (EMPNO, ENAME, SAL, DEPTNO) " + "values (1, 'NAOKI', 10000, 10)"; count =stmt.executeUpdate(sql_str); System.out.println("insert文を実行しました..."); System.out.println(" 行数: " + count); // トランザクションをコミット conn.commit(); System.out.println("コミットしました..."); sql_str = "select ENAME, SAL from EMPLOYEE " + "where EMPNO = 1"; rset = stmt.executeQuery(sql_str); System.out.println("select文を実行しました..."); // 結果セットの1行目を取り出す rset.next(); System.out.println(" " + rset.getString("ENAME") + "\t" + rset.getInt("SAL")); sql_str = "update EMPLOYEE set SAL = 20000 " + "where EMPNO = 1"; count = stmt.executeUpdate(sql_str); System.out.println("update文を実行しました..."); System.out.println(" 行数: " + count); sql_str = "select ENAME, SAL from EMPLOYEE " + "where EMPNO = 1"; rset = stmt.executeQuery(sql_str); System.out.println("select文を実行しました..."); // 結果セットの1行目を取り出す rset.next(); System.out.println(" " + rset.getString("ENAME") + "\t" + rset.getInt("SAL")); // トランザクションをロールバック conn.rollback(); System.out.println("ロールバックしました..."); rset = stmt.executeQuery(sql_str); System.out.println("select文を実行しました..."); // 結果セットの1行目を取り出す rset.next(); System.out.println(" " + rset.getString("ENAME") + "\t" + rset.getInt("SAL")); // ステートメントをクローズ stmt.close(); // 接続をクローズ conn.close(); System.out.println("接続をクローズしました..."); // 例外を処理 } catch (Exception ex) { System.out.println("Exceptionが発生しました..."); // エラー・メッセージを出力 System.out.print(ex.toString()); try { // ステートメントがクローズされていない場合、 // それをクローズ if (stmt != null) { stmt.close(); } // 接続がクローズされていない場合、 // ロールバックしてから、接続をクローズ if (conn != null) { conn.rollback(); System.out.println("ロールバックしました..."); conn.close(); System.out.println("接続をクローズしました..."); } } catch (SQLException se) {} } } }
JavaDataAccess02を実行して、EMPLOYEE表を作成した後にこのコードを実行すると、次のようになります。
C:\JDBC>java JavaDataAccess03 JDBCドライバをロードしました... データベースに接続しました... delete文を実行しました... 行数: 0 insert文を実行しました... 行数: 1 コミットしました... select文を実行しました... NAOKI 10000 update文を実行しました... 行数: 1 select文を実行しました... NAOKI 20000 ロールバックしました... select文を実行しました... NAOKI 10000 接続をクローズしました...
では、順を追ってコードを見てみましょう。select文を除くDML文の実行には、DDL文と同様に、executeUpdate()メソッドを利用します。また、executeUpdate()メソッドは、DML文によって変更された行数を返します。ここでは、delete文、insert文、update文を実行し、変更行数の取得も行っています。
sql_str = "delete from EMPLOYEE"; count = stmt.executeUpdate(sql_str); ... sql_str = "insert into EMPLOYEE ..."; count = stmt.executeUpdate(sql_str); ... sql_str = "update EMPLOYEE ..."; count = stmt.executeUpdate(sql_str);
JDBCでのトランザクションを取り上げる前に、「トランザクション」について簡単にまとめておきましょう。
トランザクションとは、一連のデータ処理を1つの単位として管理することを指しています。例えば、銀行の口座管理システムでは、「口座Aから口座Bへの振替」は、「口座Aから引き落とす」「口座Bへ振り込む」という複数の処理を含む、1つのトランザクションである、といえます。
トランザクションは、次の4つの特性を持っています(通常、各属性の頭文字を取って「ACID特性」といわれます)。
(1)原子性(Atomicity)
トランザクションは、処理が完全に完了するか、あるいは一切実行されないかのいずれかである必要があります。
リレーショナル・データベースでは、一連のDML処理に続いて、COMMITコマンドを実行することによって、トランザクション開始後のすべての変更内容が永続的に保存されます(トランザクションのコミット)。代わりに、ROLLBACKコマンドを実行すると、トランザクション開始後のすべての変更内容が取り消されます(トランザクションのロールバック)。
(2)一貫性(Consistency)
トランザクションは、データの一貫性を破壊してはなりません。データの一貫性とは、データが整合性のとれた状態であることを指します。
リレーショナル・データベースでは、制約、ストアド・プロシージャ、トリガなどを利用して、一貫性を維持することが多いでしょう。
(3)独立性(Isolation)
同時に実行されている複数のトランザクションは、互いに影響を及ぼしてはなりません。トランザクションが1つずつ順番に実行された場合と、同じ結果になる必要があるわけです。
リレーショナル・データベースでは、ロックやバージョニングといわれるテクノロジを利用して、トランザクションの独立性を保障しています。
(4)耐久性(Durability)
コミットされたトランザクションの結果は、耐久性のある方法で保存されなくてはなりません。耐久性とは、障害などが発生してもその内容が失われることがない、という意味です。
多くのデータベースは、ログ・ファイル(Oracleデータベースでは「REDOログ・ファイル」)に、トランザクションの変更履歴を随時書き込むことによって、トランザクションの耐久性を保障しています。
JDBCのデータベース接続には、「自動コミット・モード」という概念があります。
自動コミット・モードがオン(true)の場合、SQL文を実行するたびに、自動的にコミットされます。トランザクション管理が必要でない場合は、こちらが便利です。しかし、コミット処理の回数が増えるため、パフォーマンスの悪化を招くことがあります。
一方、自動コミット・モードがオフ(false)の場合は、手動でコミットやロールバックを実行する必要があります。トランザクション管理が必要な場合には、当然こちらを利用する必要があります。
自動コミット・モードを設定するには、java.sql.ConnectionオブジェクトのsetAutoCommit()メソッドを利用します。デフォルト値はtrueであることに注意しましょう。
// 自動コミット・モードを設定 conn.setAutoCommit(false);
トランザクションのコミットおよびロールバックには、それぞれcommit()メソッド、rollback()メソッドを利用します。
// トランザクションをコミット conn.commit(); .. .// トランザクションをロールバック conn.rollback();
サンプル・コードでは、更新処理の後にロールバックを実行しています。実際にロールバックされていることは、コードの実行結果を見れば分かるでしょう。
sql_str = "update EMPLOYEE ..."; count = stmt.executeUpdate(sql_str); ... sql_str = "select ENAME, SAL ..."; rset = stmt.executeQuery(sql_str); ... // トランザクションをロールバック conn.rollback(); ... sql_str = "select ENAME, SAL ..."; rset = stmt.executeQuery(sql_str);
catch節の中でロールバックを実行するようにしておけば、「例外が発生しなければコミットし、例外が発生すればロールバックする」という振る舞いを実装できます。これによって、JDBCアプリケーションでトランザクションの原子性を保証することが可能となるわけです。
try { ... // トランザクションをコミット conn.commit(); ... // 例外を処理 } catch (Exception ex) { ... // 接続がクローズされていない場合、 // ロールバックしてから、接続をクローズ if (conn != null) { conn.rollback(); ... conn.close();
次回は、PreparedStatementやCallableStatement(ストアド・プロシージャの実行)を取り上げる予定です。
参考リファレンス
▼Java Solution FAQ(@IT)
▼J2EEの基礎(@IT)
▼JDBC APIドキュメント(Sun)
▼JDBC Data Access API(Sun、英語)
▼OTN Japan - Java(日本オラクル)
▼OTN - Java (米オラクル、英語)
Copyright © ITmedia, Inc. All Rights Reserved.