[jdbctemplate+POSTGRESQL+儲存過程]jdbc呼叫儲存過程順便勘誤同時給出幾個較好的配合方式
前言
首先採用jdbc呼叫儲存過程是因為,整合mybatis的話,對於以儲存過程為主的系統沒有多大的幫助,反而多了一個分層。
本文將給出常見的儲存過程呼叫方式。
閱讀前可以先參考一下:
關於postgresql的多結果集,或者遊標返回儲存過程請檢視上篇文章:
【轉載】postgresql儲存過程中返回型別
必要資料及程式碼交代
資料表:
地區區域表一張,資料量大約30000條,不過不是重點,表結構如下:
"id" int4 DEFAULT nextval('common_region_id_seq'::regclass) NOT NULL, "parent_id" int4 NOT NULL, "name" varchar(30) COLLATE "default" NOT NULL, "level" int2 NOT NULL, "code" char(6) COLLATE "default" NOT NULL, "pingyin" varchar(40) COLLATE "default", "name_en" varchar(60) COLLATE "default", CONSTRAINT "common_region_pkey" PRIMARY KEY ("id") ) WITH (OIDS=FALSE) ; ALTER TABLE "public"."common_region" OWNER TO "dbuser"; COMMENT ON TABLE "public"."common_region" IS '地區表'; COMMENT ON COLUMN "public"."common_region"."code" IS '地區碼'; CREATE INDEX "common_region_id_pk" ON "public"."common_region" USING btree ("id"); CREATE INDEX "common_region_parent_id" ON "public"."common_region" USING btree ("parent_id"); CREATE INDEX "common_region_region_type" ON "public"."common_region" USING btree ("level");
前面部分記錄如下:
常見模式,儲存過程output遊標,jdbc直接呼叫。
這是網上常見的模式,有很多文章都是這樣寫的,想必在postgresql下面也能這樣用,文章一搜一大堆,例如:
對了,還搜到一篇postgresql的儲存過程遊標呼叫方式:
好了,我們有這麼多資料,那麼肯定可以照搬不誤了。
下面是測試程式碼:
儲存過程:
/**兩個輸出函式測試1**/ CREATE OR REPLACE FUNCTION "public"."sp_test_multi_cursors2"(IN "id" int4, OUT "records_cursor_01" refcursor, OUT "records_cursor_02" refcursor) RETURNS "record" AS $BODY$ declare tmpId integer; begin open records_cursor_01 for select * from common_region limit 3; open records_cursor_02 for select * from common_region limit 2; end; $BODY$ LANGUAGE 'plpgsql' VOLATILE;
java端程式碼:
工具類:
package net.w2p.Shared.common.DB; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.util.ArrayList; import java.util.HashMap; public class DataTableHelper { public static ArrayList<HashMap<String,Object>> rs2MapList(ResultSet rsList) { ArrayList<HashMap<String,Object>> mytable=new ArrayList<>(); if (rsList == null) { return mytable; } try { ResultSetMetaData mdata = rsList.getMetaData(); int columnCount=mdata.getColumnCount(); boolean firstGetColumnName = true; while (rsList.next()) { HashMap<String,Object> item=new HashMap<>(); for (int j = 0; j < columnCount; j++) { item.put(mdata.getColumnName(j+1),rsList.getObject(j+1)); } mytable.add(item); } return mytable; } catch (Exception e) { e.printStackTrace(); } return mytable; } }
java端呼叫程式碼:
採用的是spring,在配置檔案裡請先定義jdbc一下:
<bean id="jdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate">
<constructor-arg ref="dataSource" />
</bean>
然後正式呼叫程式碼是:
package common;
import com.alibaba.fastjson.JSONObject;
import main.BaseTest;
import net.w2p.DevBase.service.common.RegionService;
import net.w2p.DevBase.service.common.RegionServiceCase1;
import net.w2p.DevBase.vo.common.Region;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.List;
public class RegionTester1 extends BaseTest {
@Autowired
private RegionServiceCase1 case1;
@Test
public void multiCursor2(){
case1.multiCursors2();
}
}
好了,寫寫測試程式碼:
package common;
import com.alibaba.fastjson.JSONObject;
import main.BaseTest;
import net.w2p.DevBase.service.common.RegionService;
import net.w2p.DevBase.service.common.RegionServiceCase1;
import net.w2p.DevBase.vo.common.Region;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.List;
public class RegionTester1 extends BaseTest {
@Autowired
private RegionServiceCase1 case1;
@Test
public void multiCursor2(){
case1.multiCursors2();
}
}
注意,spring的單元測試請參考:
執行以後結果是:
報錯如下:
org.springframework.jdbc.UncategorizedSQLException: CallableStatementCallback; uncategorized SQLException; SQL state [34000]; error code [0]; ERROR: cursor "<unnamed portal 1>" does not exist; nested exception is org.postgresql.util.PSQLException: ERROR: cursor "<unnamed portal 1>" does not exist
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:89)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
at org.springframework.jdbc.core.JdbcTemplate.translateException(JdbcTemplate.java:1414)
是不是我們的儲存過程錯了?直接執行一下看看:
不,執行的結果是正確的。。。
這裡要勘誤一下。。
我不知道oracle,mysql,db2等等返回的遊標是不是也會報這種錯,不過,postgresql的一定會有這個錯,而大部分文章也沒提及,然而,這個錯不是第一天了,很久之前已經有了:
我PostgreSQL8.1.0中建立瞭如下Function
CREATE OR REPLACE FUNCTION getres(a "varchar")
RETURNS refcursor AS
$BODY$declare membercur refcursor;
begin
open membercur for select * from member where membercode=$1;
return membercur;
end; $BODY$
LANGUAGE 'plpgsql' VOLATILE;
在Java中呼叫:
String driver = "org.postgresql.Driver";
String url = "jdbc:postgresql://localhost:5432/nop";
String user = "nop";
String passwd = "nop";
Class.forName(driver);
Connection conn = DriverManager.getConnection(url, user, passwd);
conn.setAutoCommit(false); // return refcursor must within a transaction
CallableStatement proc = conn.prepareCall("{ ? = call getres('') }");
proc.registerOutParameter(1, Types.OTHER);
proc.execute(); //在此處出錯。
ResultSet result = (ResultSet) proc.getObject(1);
System.out.println(result);
while(result.next())
{
System.err.println("Name : " + result.getString(2));
}
conn.commit();
錯誤資訊如下:
org.postgresql.util.PSQLException: ERROR: cursor "<unnamed portal 1>" does not exist
at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:1501)
at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:1286)
at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:177)
at org.postgresql.jdbc2.AbstractJdbc2Statement.execute(AbstractJdbc2Statement.java:430)
at org.postgresql.jdbc2.AbstractJdbc2Statement.executeWithFlags(AbstractJdbc2Statement.java:332)
at org.postgresql.jdbc2.AbstractJdbc2Connection.execSQLQuery(AbstractJdbc2Connection.java:198)
at org.postgresql.jdbc2.AbstractJdbc2ResultSet.internalGetObject(AbstractJdbc2ResultSet.java:176)
at org.postgresql.jdbc3.AbstractJdbc3ResultSet.internalGetObject(AbstractJdbc3ResultSet.java:39)
at org.postgresql.jdbc2.AbstractJdbc2ResultSet.getObject(AbstractJdbc2ResultSet.java:2322)
at org.postgresql.jdbc2.AbstractJdbc2Statement.executeWithFlags(AbstractJdbc2Statement.java:367)
at org.postgresql.jdbc2.AbstractJdbc2Statement.execute(AbstractJdbc2Statement.java:339)
at PostgreSqlHelper.executeResultSet(PostgreSqlHelper.java:125)
at PostgreSqlHelperTest.testConnectDB(PostgreSqlHelperTest.java:14)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at junit.framework.TestCase.runTest(TestCase.java:154)
at junit.framework.TestCase.runBare(TestCase.java:127)
at junit.framework.TestResult$1.protect(TestResult.java:106)
at junit.framework.TestResult.runProtected(TestResult.java:124)
at junit.framework.TestResult.run(TestResult.java:109)
at junit.framework.TestCase.run(TestCase.java:118)
at junit.framework.TestSuite.runTest(TestSuite.java:208)
at junit.framework.TestSuite.run(TestSuite.java:203)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:478)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:344)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:196)
這哥們十幾年前提的問題。。嗯。。。
好了,來勘誤了。
返回遊標呼叫勘誤
這位兄弟也遇到了這問題:
而有人回答:
好了,我們改一下程式碼,再來:
package net.w2p.DevBase.service.common;
import com.alibaba.fastjson.JSONObject;
import net.w2p.DevBase.vo.common.Region;
import net.w2p.Shared.common.DB.DataTableHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.CallableStatementCallback;
import org.springframework.jdbc.core.CallableStatementCreator;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.sql.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@Service
public class RegionServiceCase1 {
@Autowired
JdbcTemplate jdbcTemplate;
public void multiCursors2(){
jdbcTemplate.execute(new CallableStatementCreator() {
@Override
public CallableStatement createCallableStatement(Connection con) throws SQLException {
String sql="{ call \"sp_test_multi_cursors2\"(?,?,?)}";
CallableStatement st=con.prepareCall(sql);
st.setInt(1,1);
st.registerOutParameter(2, Types.REF_CURSOR);
st.registerOutParameter(3,Types.REF_CURSOR);
return st;
}
},new CallableStatementCallback<List<Region>>(){
@Override
public List<Region> doInCallableStatement(CallableStatement cs) throws SQLException, DataAccessException {
List<Region> res=new ArrayList<>();
cs.execute();
ResultSet rs1=(ResultSet)cs.getObject(2);
ResultSet rs2=(ResultSet)cs.getObject(3);
//
ArrayList<HashMap<String,Object>> mapList1= DataTableHelper.rs2MapList(rs1);
ArrayList<HashMap<String,Object>> mapList2=DataTableHelper.rs2MapList(rs2);
System.out.println(JSONObject.toJSONString(mapList1));
System.out.println(JSONObject.toJSONString(mapList2));
rs1.close();
rs2.close();
return res;
}
});
}
public void multiCursors2_no_auto_commit(){
jdbcTemplate.execute(new CallableStatementCreator() {
@Override
public CallableStatement createCallableStatement(Connection con) throws SQLException {
con.setAutoCommit(false);//-------看到沒有?加上這一句。。
String sql="{ call \"sp_test_multi_cursors2\"(?,?,?)}";
CallableStatement st=con.prepareCall(sql);
st.setInt(1,1);
st.registerOutParameter(2, Types.REF_CURSOR);
st.registerOutParameter(3,Types.REF_CURSOR);
return st;
}
},new CallableStatementCallback<List<Region>>(){
@Override
public List<Region> doInCallableStatement(CallableStatement cs) throws SQLException, DataAccessException {
List<Region> res=new ArrayList<>();
cs.execute();
ResultSet rs1=(ResultSet)cs.getObject(2);
ResultSet rs2=(ResultSet)cs.getObject(3);
//
ArrayList<HashMap<String,Object>> mapList1= DataTableHelper.rs2MapList(rs1);
ArrayList<HashMap<String,Object>> mapList2=DataTableHelper.rs2MapList(rs2);
System.out.println(JSONObject.toJSONString(mapList1));
System.out.println(JSONObject.toJSONString(mapList2));
rs1.close();
rs2.close();
return res;
}
});
}
}
測試程式碼:
package common;
import com.alibaba.fastjson.JSONObject;
import main.BaseTest;
import net.w2p.DevBase.service.common.RegionService;
import net.w2p.DevBase.service.common.RegionServiceCase1;
import net.w2p.DevBase.vo.common.Region;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.List;
public class RegionTester1 extends BaseTest {
@Autowired
private RegionServiceCase1 case1;
@Test
public void multiCursor2(){
case1.multiCursors2();
}
@Test
public void multiCursor2_no_auto_commit(){
case1.multiCursors2_no_auto_commit();
}
}
然後看結果:
好了,能正常運行了。
所以,正確呼叫方式是這樣子的:
jdbcTemplate.execute(new CallableStatementCreator() {
@Override
public CallableStatement createCallableStatement(Connection con) throws SQLException {
con.setAutoCommit(false);//-------看到沒有?加上這一句。。
String sql="{ call \"sp_test_multi_cursors2\"(?,?,?)}";
CallableStatement st=con.prepareCall(sql);
st.setInt(1,1);
st.registerOutParameter(2, Types.REF_CURSOR);
st.registerOutParameter(3,Types.REF_CURSOR);
return st;
}
},new CallableStatementCallback<List<Region>>(){
@Override
public List<Region> doInCallableStatement(CallableStatement cs) throws SQLException, DataAccessException {
List<Region> res=new ArrayList<>();
cs.execute();
ResultSet rs1=(ResultSet)cs.getObject(2);
ResultSet rs2=(ResultSet)cs.getObject(3);
//
ArrayList<HashMap<String,Object>> mapList1= DataTableHelper.rs2MapList(rs1);
ArrayList<HashMap<String,Object>> mapList2=DataTableHelper.rs2MapList(rs2);
System.out.println(JSONObject.toJSONString(mapList1));
System.out.println(JSONObject.toJSONString(mapList2));
rs1.close();
rs2.close();
return res;
}
});
話說這個問題是太細了還是怎麼了,我在資料搜尋時候也有不少人遇到這問題,然而在呼叫儲存過程中沒人提及,這樣太浪費時間了。
儲存過程+多遊標優化版
直接用out形式的引數返回遊標在java的呼叫中可以完美獲取,但是,假如我們在sql裡面直接引用會發現。。額。。
頂多會有unnamed portal這種形式的程式碼。。很難顯示裡面的資料,參照之前的文章,可以得到解決方案:
out變更為inout。
儲存過程程式碼:
CREATE OR REPLACE FUNCTION "public"."sp_test_multi_cursors"(IN "id" int4, INOUT "records_cursor_01" refcursor, INOUT "records_cursor_02" refcursor)
RETURNS "record" AS $BODY$
declare tmpId integer;
begin
open records_cursor_01 for select * from common_region limit 3;
open records_cursor_02 for select * from common_region limit 2;
end;
$BODY$
LANGUAGE 'plpgsql' VOLATILE;
sql直接呼叫且顯示資料:
java程式碼中引用:
public void multiCursors_enhance(){
jdbcTemplate.execute(new CallableStatementCreator() {
@Override
public CallableStatement createCallableStatement(Connection con) throws SQLException {
con.setAutoCommit(false);//-------out引數中有遊標時請加上這一句。
String sql="{ call \"sp_test_multi_cursors\"(?,?::refcursor,?::refcursor)}";
CallableStatement st=con.prepareCall(sql);
st.setInt(1,1);
st.setObject(2,"crs_001");
st.registerOutParameter(2, Types.REF_CURSOR);
st.setObject(3,"crs_002");
st.registerOutParameter(3,Types.REF_CURSOR);
return st;
}
},new CallableStatementCallback<List<Region>>(){
@Override
public List<Region> doInCallableStatement(CallableStatement cs) throws SQLException, DataAccessException {
List<Region> res=new ArrayList<>();
cs.execute();
ResultSet rs1=(ResultSet)cs.getObject(2);
ResultSet rs2=(ResultSet)cs.getObject(3);
//
ArrayList<HashMap<String,Object>> mapList1= DataTableHelper.rs2MapList(rs1);
ArrayList<HashMap<String,Object>> mapList2=DataTableHelper.rs2MapList(rs2);
System.out.println(JSONObject.toJSONString(mapList1));
System.out.println(JSONObject.toJSONString(mapList2));
rs1.close();
rs2.close();
cs.getConnection().setAutoCommit(true);//--恢復為自動提交。
return res;
}
});
}
測試函式:
@Test
public void multiCursor_enhance(){
case1.multiCursors_enhance();
}
執行結果:
這種形式完美滿足sql裡面的呼叫以及jdbc裡面的呼叫。
儲存過程+json
為了返回多個數據,我們可以用遊標,然而,我們也是可以利用pg裡面的json來處理的,這種方式比直接返回遊標更友好,而且沒有auto commit的痛苦。好了,請先參考下面文章:
row_to_json、array_to_json都是很有用的函式。
話說這文章我也是抄別人的。。。竟然還有其他網站直接爬我的來用,怎麼不直接去爬原作者的。。。。。
儲存過程:
CREATE OR REPLACE FUNCTION "sp_test_multi_json"(IN "id" int4, out json_result_01 text,out json_result_02 text)
RETURNS record AS $BODY$
declare tmpId integer;
begin
select json_result::text into json_result_01 from (select array_to_json(array_agg(row_to_json(t))) as json_result
from (select * from common_region cr limit 3) t) middle
;
select array_to_json(array_agg(row_to_json(t)))::text into json_result_02 from (
select * from common_region cr limit 8
) t;
end;
$BODY$
LANGUAGE 'plpgsql' VOLATILE;
select sp_test_multi_json(1);
直接呼叫儲存過程的結果:
java端程式碼:
public void multiJson(){
jdbcTemplate.execute(new CallableStatementCreator() {
@Override
public CallableStatement createCallableStatement(Connection con) throws SQLException {
String sql="{ call \"sp_test_multi_json\"(?,?,?)}";
CallableStatement st=con.prepareCall(sql);
st.setInt(1,1);
st.registerOutParameter(2, Types.VARCHAR);
st.registerOutParameter(3,Types.VARCHAR);
return st;
}
},new CallableStatementCallback<List<Region>>(){
@Override
public List<Region> doInCallableStatement(CallableStatement cs) throws SQLException, DataAccessException {
List<Region> res=new ArrayList<>();
cs.execute();
String json1=cs.getString(2);
String json2=cs.getString(3);
System.out.println(json1);
System.out.println(json2);
return res;
}
});
}
測試用程式碼:
@Test
public void multiJson(){
case1.multiJson();
}
測試結果:
儲存過程+json 優化增強
對於java的呼叫來說,out json_1 varchar 這種形式很容易直接拿到值,不過,對於sql裡面呼叫函式以後要拿到值也是不容易的,不過這次也不能用inout的形式。。因為。。。cursor的呼叫和生成變數跟傳統的不一樣,為了方便起見,直接返回setof varchar之類的。
儲存過程程式碼:
CREATE OR REPLACE FUNCTION "sp_test_multi_json3"(IN "id" int4)
RETURNS setof varchar AS $BODY$
declare tmpId integer;
declare json_result_01 varchar;
declare json_result_02 varchar;
begin
select json_result::text into json_result_01 from (select array_to_json(array_agg(row_to_json(t))) as json_result
from (select * from common_region cr limit 3) t) middle
;
select array_to_json(array_agg(row_to_json(t)))::text into json_result_02 from (
select * from common_region cr limit 8
) t;
return next json_result_01;
return next json_result_02;
end;
$BODY$
LANGUAGE 'plpgsql' VOLATILE;
sql呼叫:
java程式碼呼叫:
public void multiJsonEnhance(){
jdbcTemplate.execute(new CallableStatementCreator() {
@Override
public CallableStatement createCallableStatement(Connection con) throws SQLException {
String sql="{ call \"sp_test_multi_json3\"(?)}";
// sql="select * from \"sp_test_multi_json3\"(?)"; --兩種寫法都可以。
CallableStatement st=con.prepareCall(sql);
st.setInt(1,1);
return st;
}
},new CallableStatementCallback<List<Region>>(){
@Override
public List<Region> doInCallableStatement(CallableStatement cs) throws SQLException, DataAccessException {
List<Region> res=new ArrayList<>();
cs.execute();
ResultSet rs = cs.getResultSet();
while (rs.next()){
System.out.println(rs.getString(1));//--注意,從1開始
}
return res;
}
});
}
測試程式碼:
@Test
public void multiJsonEnhance(){
case1.multiJsonEnhance();
}
結果如下:
結論
對於簡單的儲存過程,要返回多個結果的,請遵循:
1、不要用inout和out引數;
2、統一返回setof varchar 字串型別,然後由java端解碼處理資料。
3、包括json格式的也請轉成字串,畢竟,jdbc,沒有對json的原生型別支援。
對於多個複雜結果集的,請遵循:
1、使用inout形式輸出遊標
2、返回型別是record。