この記事では、DBMS ストアド プロシージャの使用方法について説明します。 ResultSet を返すなど、ストアド プロシージャを使用する基本機能と高度な機能について説明しました。この記事では、読者がすでに DBMS と JDBC に精通しており、他の言語 (つまり Java 以外の言語) で書かれたコードを何の障害もなく読めることを前提としています。ストアド プロシージャ プログラミングの経験があることが必要です。
ストアド プロシージャとは、データベースに保存され、データベース側で実行されるプログラムを指します。特別な構文を使用して、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 テーブルの行にマッピングするのは非常に簡単ですが、通常は、ID を見つけるために SELECT を実行し、その後、指定された ID を持つデータを挿入するために INSERT を実行する必要があります。高度に正規化されたデータベース スキーマでは、複数のテーブルの更新が必要になる場合があり、そのためさらに多くのステートメントが必要になります。 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(); } }
为了确保可维护性,建议使用像这儿这样的static方法。这也使得调用存储过程的代码集中在一个简单的模版代码中。如果你用到许多存储过程,就会发现仅需要拷贝、粘贴就可以创建新的方法。因为代码的模版化,甚至也可以通过脚本自动生产调用存储过程的代码。
Functions
存储过程可以有返回值,所以CallableStatement类有类似getResultSet这样的方法来获取返回值。当存储过程返回一个值时,你必须使用registerOutParameter方法告诉JDBC驱动器该值的SQL类型是什么。你也必须调整存储过程调用来指示该过程返回一个值。
下面接着上面的例子。这次我们查询Dylan Thomas逝世时的年龄。这次的存储过程使用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对象,当我们完成应用后,可以重新设计更好的模式。
• 存储过程通过更好的SQL嵌入使得复杂的SQL更容易理解。
• 编写存储过程比在Java中编写嵌入的SQL拥有更好的工具--大部分编辑器都提供语法高亮!
• 存储过程可以在任何SQL命令行中测试,这使得调试更加容易。
并不是所有的数据库都支持存储过程,但是存在许多很棒的实现,包括免费/开源的和非免费的,所以移植并不是一个问题。Oracle、PostgreSQL和DB2都有类似的存储过程语言,并且有在线的社区很好地支持。
存储过程工具很多,有像TOAD或TORA这样的编辑器、调试器和IDE,提供了编写、维护PL/SQL或pl/pgsql的强大的环境。
存储过程确实增加了你的代码的开销,但是它们和大多数的应用服务器相比,开销小得多。如果你的代码复杂到需要使用DBMS,我建议整个采用存储过程的方式。
以上就是在Java中调用存储过程(详细)的内容,更多相关内容请关注PHP中文网(www.php.cn)!