Apache Derby/Programming

出典: Fukudat.com

100% pure Java の関係データベース管理システム(RDBMS)であるApache Derbyを Java のプログラムから使用する方法を説明する.

本稿の目的は,Apache Derby を使った JDBCプログラムの初歩を例示することにあり,オブジェクト指向プログラミングを解説することを目的としていないので,実用的な応用に対しては不適切な部分があることに注意していただきたい. (例:全部が main 関数に記述されている,2度実行できないなど)

目次

準備

Java プログラムの開発には Eclipse を使用することを前提にするので,EclipseのインストールApache Derbyのインストールは完了しているものとする.

  • Eclipse を起動.
    • Windows の場合、c:\eclipse\eclipse.exe を実行。 (ただし,c:\eclipseはEclipseのインストールディレクトリ)
    • Linux の場合、/opt/eclipse/eclipse を実行。 (ただし,/opt/eclipseはEclipseのインストールディレクトリ)
  • 新規プロジェクトを作成する .
    • プルダウンメニュー "File" ⇒ "New" ⇒ "Project..." ⇒
    • "Java Project" を選んで Next ⇒
    • 適当なプロジェクト名(例えば sample)を入力して Finish
  • 作成したプロジェクトで、Apache Derby を使えるようにする.
    • Java Perspective を開く (ウィンドウ右上の Open Perspective アイコンを押して、Java を選択)
    • Package Explorer view (エキスプローラ風) 中に,さっき作成したプロジェクトを見つけ、右クリック ⇒ Apache Derby ⇒ Add Apache Derby nature
  • ここで,もし下の図のようなエラーが出るようだったら…
    • プルダウンメニュー "Window" ⇒ "Preferences..." ⇒ "Java/Build Path/Classpath Variables" を開き
    • New ボタンを押して,"ECLIPSE_HOME=c:\eclipse" を追加(下の図参照.c:\eclipse はEclipseをインストールしたトップディレクトリ)
    • "OK" ⇒ "OK"... で完了.下図のようにエラーが消えるはず.

ここまでは,Apache Derbyの対話的な使い方の準備と同じ.

プログラムからデータベースに接続する

  • まずクラスを新しく作ろう.
    • Package Explorer view でプロジェクトを右クリックして New ⇒ Class を選択.
    • New Java Class というダイアログボックスが出てくるので,Name: に適当クラス名 (ここでは Sample としよう.Javaのクラス名は普通大文字で始める) を入れて OK を押す.
  • mainメソッドを作って,データベースに接続しよう.
    • 真ん中のテキストエディタに Sample.java が現れているはずなので,そこに以下の内容を打ち込む.
import java.sql.*;

public class Sample {
    public static void main(String[] argv) {
	try {
	    Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance();
	    String url = "jdbc:derby:myDB;create=true";
	    Connection conn = DriverManager.getConnection(url);
	    conn.close();
	} catch (SQLException e) {
	    System.err.println(e.getMessage());
	} catch (Exception e) {
	    System.err.println(e.getMessage());
	}
    }
}
  • エラー (赤いバッテン印) がなければ,このプログラムを実行してみよう.
    • プルダウンメニューの "Run" ⇒ "Open Run Dialog..." を選択.
    • Java Application を右クリックして "New" を選択.
    • すると次のような画面ができるので,何も変更せずに "Run" ボタンを押す.
  • もしうまくいっていれば,何も言わずに瞬時に黙ってプログラムは終了するはず.
    • 成功したかどうかは,Package Explorer のあいているところを右クリックして,"Refresh" してみよう.myDB というフォルダが作成されていることが確認できるはずだ.

解説

プログラムの意味を簡単に説明する.

import java.sql.*;

JDBCのクラスを使うことを宣言.ここで使われているのは,java.sql.Connectionクラス, java.sql.DriverManagerクラス,java.sql.SQLExceptionクラス.


try {
...
} catch (SQLException e) {
    System.err.println(e.getMessage());
} catch (Exception e) {
    System.err.println(e.getMessage());
}

エラー処理.try の中で発生する可能性のある例外を捕まえて表示している.


Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance();

Apache Derby用のJDBCドライバをロードしている.プログラム内に具体的なドライバクラスが埋まるのは本来はお行儀の良いことではないが,ここでは簡単のためにそうしている.ここで org.apache.derby.jdbc.EmbeddedDriver というのは,Apache Derby のドライバクラスでこのドライバを用いた時は,データベースはこのプログラムと同じプロセス(Java VM)内で動作する.(データベースサーバーを別プロセスで動かすことも可能で,その場合は org.apache.derby.jdbc.ClientDriver を用いる.)


String url = "jdbc:derby:myDB;create=true";
Connection conn = DriverManager.getConnection(url);

データベースに接続している."jdbc:derby:myDB;create=true" というのがデータベースを指定する URL で,jdbc:derby でJDBC経由でApache Derbyに接続することを示し,myDBは接続するデータベース名,create=true は(もしデータベースが存在しなければ)新規作成することを意味する. この書き方は,対話的な使い方と同じ.このあとConnectionクラスのオブジェクト (変数 conn) を使ってデータベースに対して指示を出す.


conn.close();

(なにもしないで)データベースに対する接続を閉じている.何事も開けたら閉める.やりっぱなしにしてはいけない.

テーブルの作成

今後は,conn = DriverManager.getConnection() と conn.close() の間にやりたいことを追加していくことにする.

  • Sample.javaを次のように書き換えよう.
...前略...
Connection conn = DriverManager.getConnection(url);
Statement stmt = conn.createStatement();
stmt.execute("CREATE TABLE 酒(名前 VARCHAR(20), 製造元名 VARCHAR(20), PRIMARY KEY(名前))");
conn.close();
...後略...
  • エラーがないことを確認したら,実行してみよう.
    • 先ほどと同じように "Run" メニューから実行してもよいのだが,ツールバーの緑の三角アイコンを見つけて,その横の下向き矢印▼を押してみよう.すると次のようなメニューが現れるので,一番上の "Sample" を選択しよう.
  • うまくいけば瞬時に黙って終了する.もしエラーがあったら,画面のしたの "Console" ビューに赤字でエラーが表示されるはずだ.エラーメッセージをみて修正しよう.
    • ただし,このプログラムは1度きりしか実行できない.2回目は必ずエラーになり,それが正しい.

解説

Statement stmt = conn.createStatement();

SQL文を表すオブジェクト Statement を作成している.


stmt.execute("CREATE TABLE 酒(名前 VARCHAR(20), 製造元名 VARCHAR(20), PRIMARY KEY(名前))");

酒(名前, 製造元名)というテーブルを作成するSQL文を文字列として Statement オブジェクトに渡し,実行させている. この文が2回実行されると,2回目は Table/View '酒' already exists in Schema 'APP'. (すでにテーブル「酒」は存在するので作れないよ)という SQLException が発生する.このため,このプログラムは1度きりしか正常に実行できない.

レコードの挿入

次に,レコード(行 または タプルとも言う)を追加してみよう.

  • 先ほどの CREATE TABLE 文は1度しか実行できないので,いったんコメントアウトしてしまおう.
  • さらに次の様に書き換える.
Connection conn = DriverManager.getConnection(url);
Statement stmt = conn.createStatement();
// stmt.execute("CREATE TABLE 酒(名前 VARCHAR(20), 製造元 VARCHAR(20), PRIMARY KEY(名前))");
stmt.execute("INSERT INTO 酒(名前, 製造元) VALUES" +
             "('久保田千寿', '朝日酒造'), " +
             "('越乃寒梅',   '石本酒造')");
conn.close();
  • 先ほどと同様に,緑三角アイコンをクリックして実行してみよう.
  • うまくいけば,やはり黙って終了する.
    • ただし2回目は必ずエラーになり,それが正しい.

解説

stmt.execute("INSERT INTO 酒(名前, 製造元) VALUES" +
             "('久保田千寿', '朝日酒造'), " +
             "('越乃寒梅',   '石本酒造')");
  • 1つのSQL文で(名前 = 久保田千寿, 製造元 = 朝日酒造)というレコードと,(名前 = 越乃寒梅, 製造元 = 石本酒造)というレコードを同時に挿入ている.
  • Java の文字列を + で連結して SQL文を作っている.これは単に見やすさを考慮してのことで,始めから一つの文字列(つまり,"INSERT INTO 酒(名前, 製造元) VALUES('久保田千寿', '朝日酒造'), ('越乃寒梅', '石本酒造')") としたのとまったく違いはない.
  • SQL文中の文字列定数はシングルクオート (') で囲う必要がある.逆にテーブル名 (酒) やカラム名 (名前,製造元) ましてや INSERT などは,シングルクオートで囲ってはいけない.
  • 2度目に実行するとエラーが起こる理由は,酒の名前は PRIMARY KEY と宣言されており,同じ名前の酒が複数存在することが許されていないからである.

レコードの検索・取得

いよいよ挿入したレコードを取り出してみよう.

  • 先ほどの INSERT 文もコメントアウトして,以下のように書き換える.
Connection conn = DriverManager.getConnection(url);
Statement stmt = conn.createStatement();
// stmt.execute("CREATE TABLE 酒(名前 VARCHAR(20), 製造元 VARCHAR(20), PRIMARY KEY(名前))");
// stmt.execute("INSERT INTO 酒(名前, 製造元) VALUES" +
//		"('久保田千寿', '朝日酒造'), " +
//		"('越乃寒梅', '石本酒造')");
ResultSet rs = stmt.executeQuery("SELECT 名前, 製造元 FROM 酒 WHERE 製造元 = '朝日酒造'");
while (rs.next()) {
    String name = rs.getString(1);
    String make = rs.getString(2);
    System.out.println(name + ", " + make);
}
conn.close();
  • エラーがないことを確認したら,例によって緑三角アイコンをクリックして実行してみよう.
    • 今度は黙って終了するのではなく,画面下の Console view に結果が出力されるはずである.

解説

ResultSet rs = stmt.executeQuery("SELECT 名前, 製造元 FROM 酒 WHERE 製造元 = '朝日酒造'");
  • Statementオブジェクト (変数 stmt) に対して,結果セットを返すSQL文 (SELECT文) を与えて,その結果を ResultSetオブジェクト (変数 rs) に得ている.
  • このSQL文は,「酒テーブルから製造元が朝日酒造に等しいレコードを選んで,その名前と製造元を返せ」という意味である.

while (rs.next()) {
    ...中略...
}
  • SELECT文は複数のレコードを結果として返すことがある.このコード片で結果セット内のレコードひとつ一つを列挙している.
    • 問合せを実行した直後,next()が呼ばれる前は,ResultSetオブジェクトは結果セット内の最初のレコードの手前を指している.
    • 最初に next()が呼ばれると(結果セットにレコードが存在するならば),ResultSetオブジェクトは最初のレコードを指している状態となり,true が返される.
    • 次に next() が呼ばれると,ResultSetオブジェクトは次のレコード(が存在すればそれ)を指している状態となり true が返される.
    • ResultSetオブジェクトが結果セット内の最後のレコードを指している状態で next() が呼び出されると,ResultSetは最後のレコードの直後を指している状態となり,false が返される.

String name = rs.getString(1);
String make = rs.getString(2);
System.out.println(name + ", " + make);
  • while ループ内のコードなので,ResultSetオブジェクト (変数 rs) はあるレコードを指している.
  • その状態で getXxx(colNum) (Xxx は String, Integer, Double など型の名前,colNum は1から始まるカラム番号) を呼び出すと,SQLのSELECT listで指定した出力カラムを型 Xxx の値として取り出すことができる.
    • この例では,第1カラム (酒の名前)と第2カラム(酒の製造元)をString型で,それぞれ変数 nameと変数 make に取得している.

レコードの更新

同様に以下のように書き換えよう.(これまでコメントアウトしてきた部分は省略する)

Connection conn = DriverManager.getConnection(url);
Statement stmt = conn.createStatement();
int c = stmt.executeUpdate("UPDATE 酒 SET 名前='久保田万寿' WHERE 名前='久保田千寿'");
System.out.println(c + " records were updated.");
conn.close();

解説

int c = stmt.executeUpdate("UPDATE 酒 SET 名前='久保田万寿' WHERE 名前='久保田千寿'");
  • このSQL文は「酒テーブルのレコードのうち,名前が久保田千寿であるようなものの名前を久保田万寿に変更せよ」という意味である.
  • Statementオブジェクトの executeUpdate() メソッドを呼び出している.このメソッドはUpdateされたレコード数が返されるので,それを変数cで受け取っている.

レコードの削除

同様に以下のように書き換えよう.

Connection conn = DriverManager.getConnection(url);
Statement stmt = conn.createStatement();
int c = stmt.executeUpdate("DELETE 酒 FROM 製造元='石本酒造'");
System.out.println(c + " records were deleted.");
conn.close();

解説

int c = stmt.executeUpdate("DELETE FROM 酒 WHERE 製造元='石本酒造'");
  • このSQL文は「酒テーブルのレコードのうち,製造元がが石本酒造であるようなものの削除せよ」という意味である.
  • Statementオブジェクトの executeUpdate() メソッドを呼び出している.このメソッドはDeleteされたレコード数が返されるので,それを変数cで受け取っている.

PreparedStatement の使い方

これまで見てきたJDBCの使い方は,毎回文字列としてSQL文をデータベースに送りつけていた. データベースはそのたびにSQL文をコンパイル(解釈して実行プランを作成し,それを最適化)しなければならない. 同じような内容のSQLを繰り返し実行する必要がある場合には,このような方法は極めて効率が悪い. そこで,PreparedStatement という仕組みが用意されていて,データベースがSQL文を一度解釈したら,それを何度も使いまわして,パラメータを変えて実行させることができる.

ここではINSERT文を例にとって説明する.Sample.javaのmain関数を次のように書き換えよう.

...前略...
Connection conn = DriverManager.getConnection(url);
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
PreparedStatement stmt1 = conn.prepareStatement("INSERT INTO 酒 VALUES(?, ?)");
for (;;) {
    System.out.print("酒名 = ");
    System.out.flush();
    String name = in.readLine();
    if (name.length() == 0)
	break;
    System.out.print("製造元 = ");
    System.out.flush();
    String make = in.readLine();
    stmt1.setString(1, name);
    stmt1.setString(2, make);
    stmt1.execute();
}
Statement stmt2 = conn.createStatement();
ResultSet rs = stmt2.executeQuery("SELECT 名前, 製造元 FROM 酒");
while (rs.next()) {
    String name = rs.getString(1);
    String make = rs.getString(2);
    System.out.println(name + ", " + make);
}			
conn.close();
...後略...
  • エラーがないことを確認したら,緑三角ボタンを押して実行してみよう.
    • 画面下の Console view に酒名の入力を促す文字が現れるはずなので,その後ろに適当な(しかしまだテーブルには存在しない)酒の名前を入力してエンター.
    • 続いて製造元を聞いてくるので,製造元を入れてエンター.この繰り返し.
    • 酒名に対して,何もいれずにエンターを押すと,データの入力は終了し,その時点でテーブルに入っている酒とその製造元がすべてリストアップされる.

解説

PreparedStatement stmt1 = conn.prepareStatement("INSERT INTO 酒 VALUES(?, ?)");
for (;;) {
    ...中略...
    stmt1.execute();
}
  • データベース接続オブジェクトの prepareStatement()メソッドを使って,INSERT文を準備 (prepare) し,その結果を表す PreparedStatementオブジェクトを入手している.
  • そのPreparedStatementオブジェクト (変数 stmt1) はその後の for文の中で繰り返し実行されている.
  • INSERT文中の ? はパラメータで,prepareした時点では値が決まっていない.実行する (PreparedStatementのexecute()メソッドを呼び出す)前に値をセットする.

BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
...中略...
for (;;) {
    ...中略...
    System.out.print("酒名 = ");
    System.out.flush();
    String name = in.readLine();
    if (name.length() == 0)
        break;
    System.out.print("製造元 = ");
    System.out.flush();
    String make = in.readLine();
    ...中略...
}
  • データベースに挿入する値を手に入れている.この部分はJDBCプログラミングには関係ない.
  • readLine() でタイプインされる酒名,製造元の文字列を入手している.
  • for (;;) で無限ループを作っている.繰り返しの中で空の酒名が入力されると(if (...) break;により) ループから脱出するようになっている.

stmt1.setString(1, name);
stmt1.setString(2, make);
stmt1.execute();
  • PreparedStatementのパラメータの値として,酒名(変数 name)と製造元(変数 make)をセットしている.
    • setXxx(parIdx, val) (Xxx は String, Int, Double など型名, parIdx は1から始まるパラメータ番号,val はパラメータにセットする値) を呼び出すと,prepareStatement()メソッドで与えたSQL文中の?で表わされるパラメータに値を設定することができる.
    • ここでは,1番目のパラメータに酒名,2番目のパラメータに製造元を文字列として与えている.

Statement stmt2 = conn.createStatement();
ResultSet rs = stmt2.executeQuery("SELECT 名前, 製造元 FROM 酒");
while (rs.next()) {
    String name = rs.getString(1);
    String make = rs.getString(2);
    System.out.println(name + ", " + make);
}

最後にレコードの検索・取得で示した方法で,酒テーブルのすべてのレコードを列挙してプリントアウトしている.