MyBatis + SQL Server Using Table-Valued Parameters

一、实现原理

参考文档

1、微软官方封装了 JDBC 驱动 jar 包,提供 SQLServerDataTable 类;

2、Mybatis 官方提供自定义类型处理接口 TypeHandler ,可实现自定义类型与参数的绑定关系;

3、通过实现 Collection 类装载数据记录,如定义 List<User> users 来装载 User 表中的记录,那么 users 相当于一张中间表;

4、在实现 TypeHandler 接口时,对 mapper.xml 中 SQL 模板的参数进行赋值,此处可以注入 SQLServerDataTable 类作为执行参数;

5、在 mapper.xml 中的 SP 语句指定第 N 个参数处理器为自定义的 TypeHandler ,在 mybatis 解析 SQL 模板时不再使用基础类型处理器 BaseTypeHandler ,否则会抛出 UNKNOW 异常。

二、开发环境

  • java 1.8
  • docker mcr.mocrosoft.com/mssql/server:2019-latest

三、Pom 依赖

主要添加 Spring MVC、Mybatis、JDBC、Jackson 依赖

 <!-- https://mvnrepository.com/artifact/com.microsoft.sqlserver/mssql-jdbc -->
        <dependency>
            <groupId>com.microsoft.sqlserver</groupId>
            <artifactId>mssql-jdbc</artifactId>
            <version>6.2.0.jre8</version>
        </dependency>

        <!-- java.time.LocalDateTime 支持,并在 mapper 中启用-->
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
            <version>2.13.0</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus</artifactId>
            <version>3.4.3.1</version>
        </dependency>

        <!-- sqlserver -->
        <dependency>
            <groupId>com.microsoft.sqlserver</groupId>
            <artifactId>sqljdbc4</artifactId>
            <version>4.0</version>
        </dependency>

        <!-- 注解 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!-- jackson 2 databind -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.13.3</version>
        </dependency>

        <!-- jackson 2 core -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.13.3</version>
        </dependency>

        <!-- jackson 2 annotations -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.13.3</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

四、Yml 配置

配置数据源和 mybatis 映射

spring:
  datasource:
    url: jdbc:sqlserver://127.0.0.1:1434;DatabaseName=TestDB
    username: root
    password: root
    driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
mybatis:
  mapper-locations: classpath:mapper/*.xml

五、自定义 Type

CREATE TYPE [dbo].[MyTableType] AS TABLE(
        [MyKey] [VARCHAR](50) NOT NULL,
        [MyValue] [VARCHAR](50) NOT NULL
)

六、自定义存储过程

SET QUOTED_IDENTIFIER ON
SET ANSI_NULLS ON
GO

CREATE PROCEDURE [dbo].[MybatisTestPro] 
@MyTable MyTableType READONLY,
@Code varchar(10) OUT,
@Msg varchar(10) OUT

AS
SET NOCOUNT ON;

SELECT MyKey,MyValue FROM @MyTable

SELECT @Code = '200', @Msg = 'Success'

GO

七、Xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.dao.TestDao">

    <resultMap id="Map1" type="java.util.HashMap">
    </resultMap>

    <select id="proTest" resultType="java.util.List" resultMap="Map1" parameterType="Map" statementType="CALLABLE">
        {CALL MyBatisTestPro(#{MyTable,mode=IN,jdbcType=OTHER,typeHandler=com.example.demo.utils.MyTableTypeHandler},
                            #{Code,mode=OUT,jdbcType=VARCHAR}, #{Msg,mode=OUT,jdbcType=VARCHAR})}
    </select>

</mapper>

八、TestDao

package com.example.demo.dao;

import org.apache.ibatis.annotations.Mapper;

import java.util.List;
import java.util.Map;


@Mapper
public interface TestDao {

    List<Map<String,Object>> proTest(Map<String,Object> map);

}


九、Implement TypeHandler

package com.example.demo.utils;

import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.apache.ibatis.type.TypeHandler;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;

/**
 * 实现处理器:表变量传参
 * <p>
 * 不需要实现 get 方法,返回的结果集就是表数据
 */

@MappedJdbcTypes({JdbcType.OTHER})  // 对应数据库类型
@MappedTypes({ArrayList.class})     // java 数据类型
public class MyTableTypeHandler implements TypeHandler<ArrayList<?>> {

    @Override
    public void setParameter(PreparedStatement ps, int i, ArrayList<?> parameter, JdbcType jdbcType) throws SQLException {
        ps.setObject(i, SQLServerDataTableFactory.getSqlServerDataTableInstance(parameter));
    }

    @Override
    public ArrayList<?> getResult(ResultSet rs, String columnName) throws SQLException {
        return null;
    }

    @Override
    public ArrayList<?> getResult(ResultSet rs, int columnIndex) throws SQLException {
        return null;
    }

    @Override
    public ArrayList<?> getResult(CallableStatement cs, int columnIndex) throws SQLException {
        return null;
    }
}

十、SQLServerDataTableFactory

package com.example.demo.utils;

import com.example.demo.data.MyTable;
import com.microsoft.sqlserver.jdbc.SQLServerDataTable;
import com.microsoft.sqlserver.jdbc.SQLServerException;

import java.sql.Types;
import java.util.ArrayList;


/**
 * SQL 表值参数工厂
 *
 */
public class SQLServerDataTableFactory {

    private final static String DOT_SPLIT = "\\.";

    private final static String MY_TABLE = "MyTable";

    private final static String MY_DATA_FIELD_KEY = "MyKey";

    private final static String MY_DATA_FIELD_VALUE = "MyValue";

    /**
     * 返回临时表实例
     *
     * @param dataList 泛型对象数组
     * @return SQLServerDataTable
     * @throws SQLServerException SQL 异常
     */
    public static SQLServerDataTable getSqlServerDataTableInstance(ArrayList<?> dataList) throws SQLServerException {

        if (dataList != null && dataList.size() > 0) {

            String className = dataList.get(0).getClass().getName();
            String[] split = className.split(DOT_SPLIT);
            className = split[split.length - 1];

            switch (className) {
                case MY_TABLE: {
                    return getMyTableInstance(dataList);
                }
                default: {
                    return null;
                }
            }
        }
        return null;
    }

    /**
     * 返回 MyData 类型的临时表
     *
     * @param dataList 泛型对象数组
     * @return SQLServerException
     * @throws SQLServerException SQL 异常
     */
    private static SQLServerDataTable getMyTableInstance(ArrayList<?> dataList) throws SQLServerException {
        // 定义临时表
        SQLServerDataTable serverDataTable = new SQLServerDataTable();
        serverDataTable.addColumnMetadata(MY_DATA_FIELD_KEY, Types.VARCHAR);
        serverDataTable.addColumnMetadata(MY_DATA_FIELD_VALUE, Types.VARCHAR);

        // 使用并行流往表中写入数据
        dataList.stream().parallel().forEach(
                p -> {
                    MyTable data = (MyTable) p;
                    try {
                        serverDataTable.addRow(data.getMyKey(), data.getMyValue());
                    } catch (SQLServerException e) {
                        e.printStackTrace();
                    }
                }
        );
        // 返回临时表
        return serverDataTable;
    }

}

十一、MyTable

package com.example.demo.data;

import lombok.Data;

/**
 * 自定义数据类型
 *
 * @author Jiansheng Ma
 * @since 2022/11/17 16:28
 */
@Data
public class MyTable {

    private String MyKey;
    
    private String MyValue;
}

十二、模拟数据

 private ArrayList<MyTable> getMyDataArrayList(int cap) {
        System.out.println("生成模拟数据");
        ArrayList<MyTable> list = new ArrayList<MyTable>(cap);
        for (int i = 0; i < cap; i++) {
            MyTable data = new MyTable();
            data.setMyKey(UUID.randomUUID().toString());
            data.setMyValue(UUID.randomUUID().toString());
            list.add(data);
        }
        return list;
    }
public List<Map<String, Object>> testPro() throws JsonProcessingException {

        Map<String, Object> map = new HashMap<String, Object>(3);

        map.put("MyTable", getMyDataArrayList(100));

        // 调用  SP
        System.out.println("===> 开始执行存储过程:" + LocalDateTime.now().toString());
        List<Map<String, Object>> res = testDao.proTest(map);
        System.out.println("===> 执行存储过程结束:" + LocalDateTime.now().toString());

        // 获取响应结果集
        System.out.println("===> 结果集大小:" + res.size());
        System.out.println(JacksonUtil.toJsonString(res.get(0)));
        // 获取响应参数
        System.out.println("===> 通用响应参数");
        System.out.println("Code :" + map.get("Code").toString());
        System.out.println("Msg :" + map.get("Msg").toString());

        return res;
    }

十三、JacksonUtil

package com.example.demo.utils;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.io.InputStream;

/**
 * jackson 封装
 *
 * @author yushanma
 * @since 2022/8/13 10:54
 */

@Component("jacksonUtil")
public class JacksonUtil {

    private static final Logger logger = LogManager.getLogger(JacksonUtil.class.getName());

    private static ObjectMapper mapper = new ObjectMapper();;

    /**
     * 初始化 jackson 配置
     */
    @PostConstruct
    private static void init() {
        // 如果 json 中有新增的字段并且是实体类类中不存在的,不报错
        mapper.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false);
        // 在反序列化时忽略在 json 中存在但 Java 对象不存在的属性
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        // 在序列化时日期格式默认为 yyyy-MM-dd'T'HH:mm:ss.SSSZ
        mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        // java.time.LocalDateTime 支持
        mapper.registerModule(new JavaTimeModule());
        // 在序列化时忽略值为 null 的属性
        //mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        // 忽略值为默认值的属性
        //mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_DEFAULT);
        mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
        logger.debug("JacksonUtil Component Init Done.");
    }

    /**
     * Obj 转 JsonString
     *
     * @param o obj
     * @return obj 的 json 序列化字符串
     * @throws JsonProcessingException json 处理异常
     */
    public static String toJsonString(Object o) throws JsonProcessingException {
        if (o == null) {
            logger.warn("Obj 对象为空!");
            return "";
        } else {
            return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(o);
        }
    }

    /**
     * JsonString 转 Obj
     *
     * @param jsonString Json 字符串
     * @param valueType Obj 类型
     * @param <T> 泛型
     * @return T 对象
     * @throws JsonProcessingException json 处理异常
     */
    public static <T> T toObject(String jsonString, Class<T> valueType) throws JsonProcessingException {
        if (jsonString == null || jsonString.isEmpty()) {
            logger.warn("Json String 为空!");
            return null;
        } else {
            return mapper.readValue(jsonString, valueType);
        }
    }

    /**
     * InputStream 转 Obj
     * @param inputStream 输入流
     * @param valueType Obj 类型
     * @param <T> 泛型
     * @return T 对象
     * @throws IOException IO 异常
     */
    public static <T> T toObject(InputStream inputStream, Class<T> valueType) throws IOException {
        if (inputStream == null) {
            logger.warn("输入流为空!");
            return null;
        } else {
            return mapper.readValue(inputStream, valueType);
        }
    }

}

十四、测试

十五、特别说明

使用表值参数严重影响 SQL 性能,请尽量避免使用

猜你喜欢

转载自blog.csdn.net/weixin_47560078/article/details/127937997