复杂的返回值
关于存储过程的知识,很多人好像就熟悉我们所讨论的这些。如果这是存储过程的全部功能,那么存储过程就不是其它远程执行机制的替换方案了。存储过程的功能比这强大得多。
当你执行一个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 pl/pgsql;
下面是调用该存储过程的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,我建议整个采用存储过程的方式。