一、实现原理
参考文档
- Using table-valued parameters
- System requirements for the JDBC driver
- Microsoft JDBC Driver for SQL Server
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 性能,请尽量避免使用