1. 程式人生 > >[jdbctemplate+POSTGRESQL+儲存過程]jdbc呼叫儲存過程順便勘誤同時給出幾個較好的配合方式

[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。