背景:

        在通常的业务处理中,需要前端展示一个表单,后端通常要写配套的返回实体类 VO好将Mybatis查询的结构映射为实体类,即便是简单的单表查询也需要写常用的三层结构。如果返回的结构需要变动,例如加上一列,前后端几乎每个层级都需要改动添加上新的返回字段。

        本文主要是为了业务大量存放于存储过程的项目服务,如果是单纯为了方便可以稍作修改,将Mybatis调用存储过程,修改为普通的SQL。其余的思路也是相通的。

springboot mybatis调用存储过程 获取返回对象 mybatis调用存储过程返回游标_sql

如下,Java通常的三层结构对于SQL较为简单或者业务写在存储过程的查询实在是太繁重了!! 

//实体类
public class SysUser extends BaseEntity
{
    private static final long serialVersionUID = 1L;

    /** 用户ID */
    @Excel(name = "用户序号", cellType = ColumnType.NUMERIC, prompt = "用户编号")
    private Long userId;
....
//Service接口
public interface ISysUserService
{
    /**
     * 根据条件分页查询用户列表
....
//imp
@Service
public class SysUserServiceImpl implements ISysUserService
{
    private static final Logger log = LoggerFactory.getLogger(SysUserServiceImpl.class);
    @Autowired
    private SysUserMapper userMapper;
....
//Controller
@RestController
@RequestMapping("/system/user")
public class SysUserController extends BaseController
{
    @Autowired
    private ISysUserService userService;
    @GetMapping("/list")
    public TableDataInfo list(SysUser user)
    {
        startPage();
        List<SysUser> list = userService.selectUserList(user);
        return getDataTable(list);
    }
....
//Mapper
public interface SysUserMapper
    public List<SysUser> selectUserList(SysUser sysUser);
//XML
	<sql id="selectUserVo">
        select * from sys_user u
    </sql>

存储过程 

将SQL写入存储过程中,Oracle版本为11g,注释为测试存储过程多参数的情况

create or replace package pkg_package

as

type type_cursor is ref cursor;

end;
create or replace procedure p_temp_procedure

(
--v_a varchar,
--v_b varchar,
cur_out_arg out pkg_package.type_cursor

)

is

begin

open cur_out_arg for select 
--v_a,v_b  ,
a.* from sys_user a;

end;

JDBC

        先使用JDBC进行测试,注释部分为存储过程单参数的情况。使用JDBC预编译,将需要的变量填入?之中 。将获取的游标转换为List<Map> 的形式,和平常的List<User>一样,只不过将User干掉修改为Map类型。

public static void main(String[] args) throws SQLException, ClassNotFoundException {
        List<Map<String, Object>> tableData = new ArrayList<Map<String, Object>>();
        List<String> columnName = new ArrayList<>();
        JSONObject res =new JSONObject();
        String driver = "oracle.jdbc.driver.OracleDriver";
        String url = "jdbc:oracle:thin:@127.0.0.1:1521/orcl";
        String username = "yam";
        String password = "******";
        Class.forName(driver);
        Connection con = DriverManager.getConnection(url, username, password);
//        CallableStatement callableStatement = con.prepareCall("{call p_temp_procedure(?)}");
        CallableStatement callableStatement = con.prepareCall("{call p_temp_procedure(?,?,?)}");
            callableStatement.setInt(1, 0);
            callableStatement.setInt(2, 10);
        callableStatement.registerOutParameter(3, OracleTypes.CURSOR);
//        callableStatement.registerOutParameter(1, OracleTypes.CURSOR);
        callableStatement.execute();
//        ResultSet rs = ((OracleCallableStatement) callableStatement).getCursor(1);
        ResultSet rs = ((OracleCallableStatement) callableStatement).getCursor(3);
        int cl = rs.getMetaData().getColumnCount();
        //columnName 可以配合前端如vantd之类的框架,进行表单的展示,不用前端再去写动态表单。根据实际情况进行改造
        for (int i = 0; i < cl; i++) {
            String key = rs.getMetaData().getColumnName(i + 1);
            columnName.add(key);
        }
        res.put("columnName",columnName);
        while (rs.next()) {
            Map<String, Object> each = new HashMap<String, Object>();
            for (int i = 0; i < cl; i++) {
                String key = rs.getMetaData().getColumnName(i + 1);
                Object val = rs.getObject(key);
                each.put(key, val);
            }
            tableData.add(each);
        }
        res.put("tableData",tableData);
        System.out.println("输出的结果是:" + res);
    }

      输出的结果是: {
    "columnName": [":B2", ":B1", "USER_ID", "DEPT_ID", ...],
    "tableData": [{
        "DEPT_ID": 103,
        "PHONENUMBER": "15888888888",
        "SEX": "1",
        ":B1": "10",
        "USER_ID": 1...
    },{
        "SEX": "0",
        "UPDATE_BY": "admin",
        ":B1": "10",
        "USER_ID": 100...
    }]

        上面输出字段不一样是因为推测是HashMap的value不能为空,所以在put的时候需要判断一下val是否为空,如果为空put “”即可。

        实践之后发现Map中是可以有存有null的value,而转换成json的时候json中也存在。原因是fastjson生成json后他的toString方法中Null属性不显示。

springboot mybatis调用存储过程 获取返回对象 mybatis调用存储过程返回游标_存储过程_02

        解决方案有两个,一个是刚刚说的给value赋空字符串

if (val ==null){
                    val="";
                }

        另一个是在toString时候指定参数为空也要写入。这个方法只能fastJson使用,fastJson2无效。

String str = JSONObject.toJSONString(res,SerializerFeature.WriteMapNullValue);
        System.out.println("输出的结果是:" + str);

         如果不要toString的话,可以无视这些,直接debug看。

 

        

 Mybatis

        Mapper ,返回和输入的参数请务必定义为Map<String, Object>,不要@parm。本质是对输入的Map进行修改,添加新的Key-value。所以必须要有输入的Map!

public Map<String, Object> getTable(Map<String, Object> result);

        XML,定义一个resultMap类型为HashMap进行映射,因为靠变量拼接存储过程名和变量名,所以只能用${},但是因为存储过程是已经在数据库中编译过的,所以不必担心SQL注入 。输出游标基本是定死,请不要修改。

<resultMap id="cursorMap" type="java.util.HashMap">
    </resultMap>    
    <select id="getTable" statementType="CALLABLE" parameterType="map" >
        {call ${procedureName}(
                ${procedureVar},
                #{cur_out_arg, jdbcType=CURSOR, mode=OUT, javaType=ResultSet, resultMap=cursorMap}
            )}
    </select>

         Controller层,将存储过程的过程名字和变量数组传入,

@GetMapping("/list")
    public R<JSONObject> userList() throws ClassNotFoundException, SQLException {
        JSONObject res =new JSONObject();
        Map tableData=new HashMap();
//    procedureName 为需要调用的存储过程名
        tableData.put("procedureName","p_temp_procedure");
//    procedureVar为需要调用的存储过程的输入变量
        tableData.put("procedureVar","09492,9512");
//    调用写好的通用Mapper,通过Mybatis的映射将结果写入输入的tableData中
        autoDBtoJSONMapper.getTable(tableData);
        res.put("tableData",tableData);
        return R.ok(res);
    }

        但是返回的Map怎么size不一样? 有东西什么东西漏了?

        

springboot mybatis调用存储过程 获取返回对象 mybatis调用存储过程返回游标_List_03

         和前面JDBC的转换一样,Mybatis也同样有问题,但是问题原因不一样,他在处理结果的Map中就直接缺少了空的字段。

        Mybatis查询数据映射数据类型使用的是Map,当字段值为null时,mybatis映射返回字段的时候会忽略。为了解决这个问题,先修改Mybatis的配置文件。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- 全局参数 -->
    <settings>
        <!-- 使全局的映射器启用或禁用缓存 -->
        <setting name="cacheEnabled"             value="true"   />
        <!-- 允许JDBC 支持自动生成主键 -->
        <setting name="useGeneratedKeys"         value="false"   />
        <!-- 配置默认的执行器.SIMPLE就是普通执行器;REUSE执行器会重用预处理语句(prepared statements);BATCH执行器将重用语句并执行批量更新 -->
        <setting name="defaultExecutorType"      value="SIMPLE" />
		<!-- 指定 MyBatis 所用日志的具体实现 -->
        <setting name="logImpl"                  value="SLF4J"  />
        <!-- 使用驼峰命名法转换字段 -->
		<!-- <setting name="mapUnderscoreToCamelCase" value="true"/> -->
        <!-- 修改映射Map,返回所有值,即便是Null -->
        <setting name="callSettersOnNulls" value="true"/>
	</settings>

</configuration>

   经过Mybatis的配置后如果查询结果为空则传入null。如果要直接toString 的话也会遇到上面JDBC输出的问题,不过这里是放在controller中,直接把json返回给前端所以并无影响。

springboot mybatis调用存储过程 获取返回对象 mybatis调用存储过程返回游标_sql_04

springboot mybatis调用存储过程 获取返回对象 mybatis调用存储过程返回游标_存储过程_05