ストアド プロシージャは、データベースに保存され、データベース側で実行されるプログラムを指します。特別な構文を使用して、Java クラスからストアド プロシージャを呼び出すことができます。呼び出されると、ストアド プロシージャの名前と指定されたパラメータが JDBC 接続を通じて DBMS に送信され、ストアド プロシージャが実行され、接続を通じて結果が返されます (存在する場合)。
ストアド プロシージャを使用すると、EJB または CORBA ベースのアプリケーション サーバーを使用するのと同じ利点があります。違いは、ストアド プロシージャは多くの一般的な DBMS から無料で入手できるのに対し、アプリケーション サーバーはほとんどが非常に高価であることです。ライセンス料だけの問題ではありません。アプリケーション サーバーを使用する際の管理コストとコーディング コスト、およびクライアント プログラムの複雑さはすべて、DBMS のストアド プロシージャに置き換えることができます。
ストアド プロシージャは Java、Python、Perl、または C で作成できますが、通常は DBMS で指定された特定の言語を使用します。 Oracle は PL/SQL を使用し、PostgreSQL は pl/pgsql を使用し、DB2 は手続き型 SQL を使用します。これらの言語はすべて非常に似ています。ストアド プロシージャ間でのストアド プロシージャの移行は、Sun の EJB 仕様の異なる実装間でセッション Bean を移行するのと同じくらい難しいことではありません。さらに、ストアド プロシージャは SQL を埋め込むように設計されているため、Java や C などの言語よりもデータベース メカニズムを表現しやすい方法になります。
ストアド プロシージャは DBMS 自体で実行されるため、アプリケーションでの待機時間を短縮できます。 Java コードで 4 つまたは 5 つの SQL ステートメントを実行する代わりに、サーバー側で実行する必要があるストアド プロシージャは 1 つだけです。ネットワーク上でのデータの往復回数を減らすと、パフォーマンスが大幅に向上します。
ストアド プロシージャの使用
単純な古い JDBC は、CallableStatement クラスを介したストアド プロシージャの呼び出しをサポートしています。このクラスは実際には PreparedStatement のサブクラスです。詩人のデータベースがあるとします。データベースには詩人の死亡年齢を設定するストアド プロシージャがあります。以下は、オールド ソーク ディラン トーマスを呼び出すための詳細なコードです (オールド ソーク ディラン トーマス、暗示または文化に関連しているかどうかは指定しません。批判して修正してください。翻訳):
try{ int age = 39; String poetName = "dylan thomas"; CallableStatement proc = connection.prepareCall("{ call set_death_age(?, ?) }"); proc.setString(1, poetName); proc.setInt(2, age); cs.execute(); }catch (SQLException e){ // ....}
The string returns to the prepareCall メソッドはストアド プロシージャの呼び出しの記述規則です。ストアド プロシージャの名前を指定します。指定する必要があるパラメータを表します。 JDBC との統合は、ストアド プロシージャにとって非常に便利です。アプリケーションからストアド プロシージャを呼び出すには、スタブ クラスや構成ファイルは必要なく、DBMS の JDBC ドライバ以外は何も必要ありません。
このコードが実行されると、データベースのストアド プロシージャが呼び出されます。ストアド プロシージャが結果を返さないため、結果を取得できませんでした。実行の成功または失敗は例外によってわかります。失敗とは、ストアド プロシージャの呼び出しの失敗 (間違った型のパラメータを指定した場合など)、またはアプリケーションの失敗 (詩データベースに "Dylan Thomas" が存在しないことを示す例外をスローした場合など) を意味する場合があります。
SQL 操作とストアド プロシージャの組み合わせ
Java オブジェクトを SQL テーブル内の行にマッピングするのは非常に簡単ですが、通常は複数の SQL ステートメント (ID を検索するための SELECT など) を実行する必要があります。次に、INSERT は指定された ID を持つデータを挿入します。高度に正規化されたデータベース スキーマでは、複数のテーブルの更新が必要になる場合があり、そのためさらに多くのステートメントが必要になります。 Java コードは急速に増大する可能性があり、各ステートメントのネットワーク オーバーヘッドが急速に増加する可能性があります。
これらの SQL ステートメントをストアド プロシージャに移動すると、コードが大幅に簡素化され、ネットワーク呼び出しが 1 つだけ必要になります。関連する SQL 操作はすべてデータベース内で実行できます。また、PL/SQL などのストアド プロシージャ言語では、Java コードよりも自然な SQL 構文の使用が可能です。以下は、Oracle の PL/SQL 言語で書かれた初期のストアド プロシージャです。
create procedure set_death_age(poet VARCHAR2, poet_age NUMBER) poet_id NUMBER; begin SELECT id INTO poet_id FROM poets WHERE name = poet; INSERT INTO deaths (mort_id, age) VALUES (poet_id, poet_age); end set_death_age;
これは固有のものですか?いいえ。詩人のテーブルに更新があるのを期待していたと思います。これは、ストアド プロシージャを使用した実装がいかに簡単であるかを示しています。 set_death_age はほぼ確実に悪い実装です。詩人のテーブルに死亡年齢を保存する列を追加する必要があります。 Java コードはストアド プロシージャを呼び出すだけなので、データベース スキーマがどのように実装されるかは関係ありません。後でデータベース スキーマを変更してパフォーマンスを向上させることはできますが、コードを変更する必要はありません。
上記のストアド プロシージャを呼び出す Java コードは次のとおりです。
public static void setDeathAge(Poet dyingBard, int age) throws SQLException{ Connection con = null; CallableStatement proc = null; try { con = connectionPool.getConnection(); proc = con.prepareCall("{ call set_death_age(?, ?) }"); proc.setString(1, dyingBard.getName()); proc.setInt(2, age); proc.execute(); } finally { try { proc.close(); } catch (SQLException e) {} con.close(); } }
保守性を確保するために、このような静的メソッドを使用することをお勧めします。これにより、ストアド プロシージャを呼び出すコードも単純なテンプレート コードに集約されます。多くのストアド プロシージャを使用する場合は、コピーして貼り付けるだけで新しいメソッドを作成できることがわかります。コードのテンプレート化により、スクリプトを通じてストアド プロシージャを呼び出すコードを自動的に生成することも可能です。
関数
ストアド プロシージャは戻り値を持つことができるため、CallableStatement クラスには戻り値を取得する getResultSet などのメソッドがあります。ストアド プロシージャが値を返すときは、registerOutParameter メソッドを使用して、値の SQL 型を JDBC ドライバーに伝える必要があります。また、ストアド プロシージャの呼び出しを調整して、プロシージャに値を返すように指示する必要があります。
以下は上記の例に続きます。今回はディラン・トーマスの死亡時の年齢を調べてみました。今回のストアド プロシージャは PostgreSQL の pl/pgsql を使用します:
create function snuffed_it_when (VARCHAR) returns integer 'declare poet_id NUMBER; poet_age NUMBER; begin --first get the id associated with the poet. SELECT id INTO poet_id FROM poets WHERE name = $1; --get and return the age. SELECT age INTO poet_age FROM deaths WHERE mort_id = poet_id; return age; end;' language 'pl/pgsql';
另外,注意pl/pgsql参数名通过Unix和DOS脚本的$n语法引用。同时,也注意嵌入的注释,这是和Java代码相比的另一个优越性。在Java中写这样的注释当然是可以的,但是看起来很凌乱,并且和SQL语句脱节,必须嵌入到Java String中。
下面是调用这个存储过程的Java代码:
connection.setAutoCommit(false); CallableStatement proc = connection.prepareCall("{ ? = call snuffed_it_when(?) }"); proc.registerOutParameter(1, Types.INTEGER); proc.setString(2, poetName); cs.execute(); int age = proc.getInt(2);
如果指定了错误的返回值类型会怎样?那么,当调用存储过程时将抛出一个RuntimeException,正如你在ResultSet操作中使用了一个错误的类型所碰到的一样。
复杂的返回值
关于存储过程的知识,很多人好像就熟悉我们所讨论的这些。如果这是存储过程的全部功能,那么存储过程就不是其它远程执行机制的替换方案了。存储过程的功能比这强大得多。
当你执行一个SQL查询时,DBMS创建一个叫做cursor(游标)的数据库对象,用于在返回结果中迭代每一行。ResultSet是当前时间点的游标的一个表示。这就是为什么没有缓存或者特定数据库的支持,你只能在ResultSet中向前移动。
某些DBMS允许从存储过程中返回游标的一个引用。JDBC并不支持这个功能,但是Oracle、PostgreSQL和DB2的JDBC驱动器都支持在ResultSet上打开到游标的指针(pointer)。
设想列出所有没有活到退休年龄的诗人,下面是完成这个功能的存储过程,返回一个打开的游标,同样也使用PostgreSQL的pl/pgsql语言:
create procedure list_early_deaths () return refcursor as 'declare toesup refcursor; begin open toesup for SELECT poets.name, deaths.age FROM poets, deaths -- all entries in deaths are for poets. -- but the table might become generic. WHERE poets.id = deaths.mort_id AND deaths.age < 60; return toesup; end;' language 'plpgsql';
下面是调用该存储过程的Java方法,将结果输出到PrintWriter:
PrintWriter: static void sendEarlyDeaths(PrintWriter out){ Connection con = null; CallableStatement toesUp = null; try { con = ConnectionPool.getConnection(); // PostgreSQL needs a transaction to do this... con. setAutoCommit(false); // Setup the call. CallableStatement toesUp = connection.prepareCall("{ ? = call list_early_deaths () }"); toesUp.registerOutParameter(1, Types.OTHER); toesUp.execute(); ResultSet rs = (ResultSet) toesUp.getObject(1); while (rs.next()) { String name = rs.getString(1); int age = rs.getInt(2); out.println(name + " was " + age + " years old."); } rs.close(); } catch (SQLException e) { // We should protect these calls. toesUp.close(); con.close(); } }
因为JDBC并不直接支持从存储过程中返回游标,我们使用Types.OTHER来指示存储过程的返回类型,然后调用getObject()方法并对返回值进行强制类型转换。
这个调用存储过程的Java方法是mapping的一个好例子。Mapping是对一个集上的操作进行抽象的方法。不是在这个过程上返回一个集,我们可以把操作传送进去执行。本例中,操作就是把ResultSet打印到一个输出流。这是一个值得举例的很常用的例子,下面是调用同一个存储过程的另外一个方法实现:
public class ProcessPoetDeaths{ public abstract void sendDeath(String name, int age); } static void mapEarlyDeaths(ProcessPoetDeaths mapper){ Connection con = null; CallableStatement toesUp = null; try { con = ConnectionPool.getConnection(); con.setAutoCommit(false); CallableStatement toesUp = connection.prepareCall("{ ? = call list_early_deaths () }"); toesUp.registerOutParameter(1, Types.OTHER); toesUp.execute(); ResultSet rs = (ResultSet) toesUp.getObject(1); while (rs.next()) { String name = rs.getString(1); int age = rs.getInt(2); mapper.sendDeath(name, age); } rs.close(); } catch (SQLException e) { // We should protect these calls. toesUp.close(); con.close(); } }
这允许在ResultSet数据上执行任意的处理,而不需要改变或者复制获取ResultSet的方法:
static void sendEarlyDeaths(final PrintWriter out){ ProcessPoetDeaths myMapper = new ProcessPoetDeaths() { public void sendDeath(String name, int age) { out.println(name + " was " + age + " years old."); } }; mapEarlyDeaths(myMapper); }
这个方法使用ProcessPoetDeaths的一个匿名实例调用mapEarlyDeaths。该实例拥有sendDeath方法的一个实现,和我们上面的例子一样的方式把结果写入到输出流。当然,这个技巧并不是存储过程特有的,但是和存储过程中返回的ResultSet结合使用,是一个非常强大的工具。
以上がJavaでストアドプロシージャを呼び出す方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。