批量测试Mybatis项目中Sql是否正确

本文介绍了一种批量测试Mybatis Mapper的方法,通过反射执行Mapper中的所有方法,为参数赋予默认值,记录成功和失败的执行情况,适用于Oracle到Mysql数据库迁移场景。

去Oracle行动

最近公司要发展海外项目,所以要将现有的系统全部平移过去,另外数据库也要从原来的Oracle变为Mysql。公司的数据库交互层面使用的是Mybatis,而OracleMysql也有一些语法上的不同。所以在项目中的Sql要改动,但是多个项目中涉及到的Sql非常多,如果仅凭人工一条一条辨别的话,工作量有点大。所以就萌发出了直接将数据源变为Mysql,利用反射批量执行Mapper中的方法,然后如果有参数的话,就设置为默认的初始值,然后记录下来成功的数据和失败的数据,这样就可以根据失败原因进行修改。能够节省很大的时间。

执行效果

代码介绍

总体思路就三步

  1. 通过反射获得要执行的Mapper类的所有方法
  2. 获得方法中的参数,并赋值
  3. 执行
AutoTestMapper autoTestMapper = new AutoTestMapper("存放Mapper全路径名");
autoTestMapper.openSqlSession(sqlSessionFactory);

复制代码

在构造函数中传入全路径名后,进行解析,解析出包名和所有的文件名并存储起来

    public AutoTestMapper(String path) throws IOException, ClassNotFoundException {
        String mapperContent = getFileContent(path);
        String pathPattern = "import [a-z,A-Z,/.]+;";
        String[] pathArr = matchMethod(pathPattern, mapperContent).split(";");
        for (int i = 0; i < pathArr.length; i++) {
            pathArr[i] = pathArr[i].replaceAll("import ", "");
            Class cls = Class.forName(pathArr[i]);
            if (!cls.isInterface()) {
                TYPE_ARRAY.add(cls);
            }
        }
        //获得全路径名的前缀
        String packPattern = "package [a-z,A-Z,/.]+;";
        String[] packPathArr = matchMethod(packPattern, mapperContent).split(";");
        String packPath = packPathArr[0].replaceAll("package ", "").replaceAll(";", "");
        this.PACK_PATH = packPath;
    }
复制代码

然后调用openSqlSession的方法,传入SqlSessionFactory参数

        List<Map<Class, Object>> list = new ArrayList<>();
        List<String> invokeSuccess = new ArrayList<>();
        List<String> invokeFail = new ArrayList<>();
        for (String fileName : FILE_NAME) {
            Class cls = Class.forName(PACK_PATH + "." + fileName);
            //添加Mapper
            if (!sqlSessionFactory.getConfiguration().hasMapper(cls)){
                sqlSessionFactory.getConfiguration().addMapper(cls);
            }
            //获得Mapper
            Object mapper = sqlSessionFactory.openSession().getMapper(cls);
            //反射执行Mapper的方法
            Map<String, List<String>> resultMap = autoTestInvoke(cls, mapper);
            invokeSuccess.addAll(resultMap.get(SUCCESS_FLG));
            invokeFail.addAll(resultMap.get(FAIL_FLG));
        }

复制代码

然后通过Mybatyis提供的方法getMapper()传入类名获得所要Mapper类。核心方法就是autoTestInvoke()方法了

  	private Map<String, List<String>> autoTestInvoke(Class c, Object o)
     {
        Method[] declaredMethods = c.getDeclaredMethods();
        String fileName = c.getName().substring(c.getName().lastIndexOf("."));
        List<String> invokeSuccess = new ArrayList<>();
        List<String> invokeFail = new ArrayList<>();
        Map<String, List<String>> resultMap = new HashMap<>();
        //给参数赋初始值
        for (Method method : declaredMethods) {
            List<Object> list = new ArrayList<>();
            for (Class cls : method.getParameterTypes()) {
                Object par = new Object();
                if (TYPE_ARRAY.contains(cls)) {
                    if (cls.equals(String.class)) {
                        par = "1";
                    } else {
                        try {
                            par = cls.newInstance();
                            assignment(cls, par);
                        } catch (InstantiationException e) {
                            if (cls.isPrimitive()) {
                                cls = primitiveClazz.get(cls.getName());
                            }
                            try {
                                par = cls.getDeclaredConstructor(String.class).newInstance("1");

                            }catch (NoSuchMethodException e1){
                                System.out.println(cls.getName()+e);
                            }
                        }
                    }
                }else if ("java.util.Map".equals(cls.getName())){
                    par = getMapData(c.getName()+"."+method.getName());
                }
                list.add(par);
            }
            try {
                method.invoke(o, list.toArray());
                invokeSuccess.add("Success: " + fileName + "." + method.getName());
            } catch (Exception e) {
                invokeFail.add("Error:" + method.getName() + "   Error Info:" + e);
            }
        }
        resultMap.put(SUCCESS_FLG, invokeSuccess);
        resultMap.put(FAIL_FLG, invokeFail);
        return resultMap;
    }

复制代码

这里面完成为参数赋初始值,和执行的逻辑。

使用说明

自动测试Mapper除了传参为List和Set,其余都能测到。在xml中所有的if条件都会拼接到。

  • AutoTestMapper拷贝到测试模块中。如图所示

AutoTestMapper文件存放在github

  • resources模块中加入mybatis-config.xml文件,如图所示

mybatis-config.xml内容如下

	<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <environments default="dev">
        <environment id="dev">
            <transactionManager type="JDBC"></transactionManager>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="连接地址"/>
                <property name="username" value="账号"/>
                <property name="password" value="密码"/>
            </dataSource>
        </environment>
    </environments>
</configuration>
	
复制代码
  • 在根目录创建lib文件夹,并将测试的Mybatis版本放入其中,并在Gradle中引入此包

compile files('../lib/mybatis-3.5.0-hupengfeiTest.jar')此处路径填写相对路径

如果目录结构如下,那么就compile files('lib/mybatis-3.5.0-hupengfeiTest.jar')

mybatis-3.5.0-hupengfeiTest.jargithub下面的lib目录中

-lib
	-- mybatis-3.5.0-hupengfeiTest.jar
-build.gradle
复制代码

如果目录结构如下,那么就compile files('../lib/mybatis-3.5.0-hupengfeiTest.jar')

-lib
	-- mybatis-3.5.0-hupengfeiTest.jar
-service
	-- build.gradle
复制代码

  • 在单元测试中编写代码,进行测试
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = { AirApplication.class })//此处AirApplication.class为项目中的启动类,自行修改
public class TestMapper {

    @Test
    public void testCeshi()
            throws IllegalAccessException, IntrospectionException, InvocationTargetException, NoSuchMethodException,
            InstantiationException, IOException, ClassNotFoundException {
        //读取Mybatis配置
        Reader resourceAsReader = Resources.getResourceAsReader("mybatis-config.xml");
        //生成SqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsReader);
        resourceAsReader.close();
        AutoTestMapper autoTestMapper = new AutoTestMapper(存放Mapper的Java文件夹的全路径名);
        //执行测试方法
        autoTestMapper.openSqlSession(sqlSessionFactory);
    }
}
复制代码

就会在控制台中打印出执行失败的Mapper以及其原因。如下图所示

github地址:github.com/modouxiansh…

<think>我们正在使用MyBatis进行批量插入操作,并希望从SQL Server数据库中获取所有插入记录的主键值。在MyBatis中,我们通常使用`useGeneratedKeys`和`keyProperty`来获取自增主键。但在批量插入时,情况会复杂一些,因为需要为多个记录获取主键。根据引用[3]的内容,SQLServer的JDBC驱动在执行批量操作时,如果要获取自动生成的主键,通常需要在INSERT语句中使用OUTPUT子句。但是,MyBatis默认生成的批量插入语句并不包含OUTPUT子句,而且直接修改SQL语句来添加OUTPUT子句可能会很复杂,并且如果用户语句中已经包含OUTPUT子句,则无法再添加。然而,我们可以考虑以下方法:1.使用MyBatis的`@Options`注解或`<insert>`标签的`useGeneratedKeys`和`keyProperty`属性,并配合`@InsertProvider`或`<insert>`标签编写包含OUTPUT子句的SQL语句。2.使用SQLServer的SCOPE_IDENTITY()函数只能获取最后插入的标识值,不适用于批量插入。因此,我们需要使用OUTPUT子句来返回插入的多个主键。3.在MyBatis中,我们可以使用`<foreach>`标签来构建批量插入语句,并在其中使用OUTPUT子句。但是,需要注意的是,在SQL Server中,使用OUTPUT子句返回结果集时,不能直接将结果集映射到keyProperty。因此,我们需要调整Mapper接口和XML配置。具体步骤:步骤1:在Mapper接口中定义插入方法,返回值为插入的主键列表(或影响的行数,但我们需要主键)。步骤2:在XML映射文件中编写INSERT语句,使用OUTPUT子句返回插入的主键值。步骤3:由于MyBatis不支持直接将OUTPUT返回的结果集赋值给keyProperty,我们可以将结果集作为查询结果返回,然后在Mapper接口中接收。示例:假设我们有一个表`t_user`,主键为`id`(自增),其他字段为`name`。Mapper接口: ```javapublicinterface UserMapper {@Insert({"INSERT INTOt_user(name)","OUTPUT INSERTED.id","VALUES(#{name})"})@Options(useGeneratedKeys =true,keyProperty= "id")IntegerinsertUser(User user);//批量插入@InsertProvider(type =UserSqlProvider.class, method= "insertUsers")@Options(useGeneratedKeys =true,keyProperty= "ids")voidinsertUsers(@Param("list") List<User>users);} ```但是,上述单个插入可以工作,批量插入则不行。因为MyBatis默认的批量插入foreach并不支持OUTPUT子句返回多个主键并赋值给每个对象的属性。另一种做法是使用单独的SQL语句,在批量插入中返回所有生成的主键,然后手动设置到对象中。但是,这需要我们在一个事务中执行,并且需要确保顺序。我们可以这样写批量插入:```xml<insert id="insertUsers"useGeneratedKeys="true"keyProperty="id">INSERTINTO t_user (name)OUTPUTINSERTED.idVALUES<foreachcollection="list"item="item"separator=",">(#{item.name})</foreach></insert> ```然而,在SQL Server中,上述语句在批量插入时使用OUTPUT子句并配合VALUES列表是允许的,它会返回所有插入的id。但是,MyBatis需要能够将这些id设置到每个对象的keyProperty(即id)属性中。但是,MyBatis在处理批量插入的keyProperty时,要求数据库驱动支持返回多个自增主键。SQLServer的JDBC驱动支持通过OUTPUT子句返回多个主键,而MyBatis从3.3.0版本开始支持批量插入返回自增主键(需要数据库驱动支持)。因此,我们可以尝试以下配置:在MyBatis的XML映射文件中:```xml<insert id="insertUsers"useGeneratedKeys="true"keyProperty="id">INSERT INTOt_user(name)VALUES<foreach collection="list" item="item" separator=",">(#{item.name})</foreach> </insert>```注意:这里并没有显式使用OUTPUT子句,因为MyBatis会自动处理?实际上,对于SQLServer,MyBatis会使用JDBC的getGeneratedKeys方法来获取主键,而JDBC驱动在内部可能会使用OUTPUT子句或SCOPE_IDENTITY(),但批量时通常使用OUTPUT。但是,根据引用[3]中的讨论,SQLServer的JDBC驱动在执行批量插入时,如果要获取生成的主键,需要在连接字符串中设置`useBulkCopyForBatchInsert=false`(默认是false),因为批量复制操作(BulkCopy)不支持返回生成的主键。因此,我们需要确保连接字符串中没有设置`useBulkCopyForBatchInsert=true`。同时,我们还需要注意,使用`<foreach>`拼接的批量插入在数据量很大时可能会超出SQL语句的长度限制,此时可以考虑分批次插入。另外,如果使用MyBatis执行批量插入,还可以使用ExecutorType.BATCH来执行批量操作,但这种方式下,获取主键可能会遇到问题,因为JDBC的批处理操作在返回主键方面支持有限。所以,总结如下:方案1:使用foreach拼接单个INSERT语句(适用于数据量不是特别大的情况)```xml<insertid="insertUsers" useGeneratedKeys="true" keyProperty="id">INSERTINTO t_user (name)VALUES<foreach collection="list" item="item" separator=",">(#{item.name})</foreach> </insert>```方案2:使用ExecutorType.BATCH,并配合在INSERT语句中使用OUTPUT子句返回主键,然后在Mapper方法中返回主键列表,并手动设置到对象中(较为复杂)。由于方案1简单且常用,我们优先尝试方案1。但是,请注意:SQL Server对单个INSERT语句插入多行记录时,使用VALUES列表是有限制的(最多1000行)。如果超过,需要分批次插入。另外,确保SQLServer的JDBC驱动版本支持此功能(较新的驱动都支持)。如果使用方案1,在插入后,传入的List<User>中的每个User对象的id属性会被自动设置为对应的主键值。示例代码:UserMapper.xml: ```xml<mappernamespace="com.example.mapper.UserMapper"><insertid="insertUsers" useGeneratedKeys="true" keyProperty="id">INSERTINTO t_user (name)VALUES<foreachcollection="list"item="user" separator=",">(#{user.name})</foreach></insert> </mapper>```在Java中调用:```javaList<User>users =new ArrayList<>(); //添加多个User对象userMapper.insertUsers(users);//此时,每个User对象的id已经被设置```如果遇到问题,可能需要检查:1.数据库连接字符串中是否设置了`useBulkCopyForBatchInsert=true`,如果设置为true,则不会返回主键,需要改为false(或删除,因为默认是false)。2. SQLServer版本是否支持(SQLServer2008及以上支持一次插入多行)。3.主键必须是自增列。如果数据量很大,可以分批次插入,例如每次插入1000条。因此,对于用户的需求,推荐使用foreach拼接的方式,并设置useGeneratedKeys和keyProperty,同时确保JDBC驱动能够返回生成的主键(SQLServer JDBC驱动默认支持通过OUTPUT子句返回,但需要避免使用批量复制模式)。引用说明:引用[3]提到,直接修改用户语句添加OUTPUT子句是困难的,但MyBatis通过JDBC驱动的getGeneratedKeys方法内部处理了这个问题,因此我们不需要显式添加OUTPUT子句(除非特殊需求)。在MyBatis中,我们只需要配置useGeneratedKeys即可。但是,根据SQLServer JDBC驱动的文档,当使用批量插入(多个VALUES)时,驱动会自动使用OUTPUT子句来返回生成的主键。因此,我们按照方案1即可。注意:在MyBatis官方文档中,对于批量插入获取主键,有类似的示例:https://mybatis.org/mybatis-3/zh/sqlmap-xml.html#insert_update_and_delete文档中说明:“对于支持自动生成记录主键的数据库(如 MySQLSQL Server),设置useGeneratedKeys=”true”,然后把keyProperty设置为目标属性就 OK了。”因此,我们可以放心使用。但是,如果使用ExecutorType.BATCH(即SqlSession的批处理模式),则不能使用foreach拼接,而需要循环执行单个插入,此时也可以获取主键,但性能可能不如方案1(因为需要多次执行SQL)。所以,在数据量允许的情况下,方案1是首选。如果数据量非常大,可能需要考虑使用SQLServer的Bulk Copy,但BulkCopy不返回主键,所以需要其他方式生成主键(例如使用UUID,或者提前从序列获取一批主键值)。综上所述,我们给出如下解决方案:1.使用MyBatis的foreach标签拼接批量插入语句。2.在insert标签中设置useGeneratedKeys="true"和keyProperty="id"(假设对象的主键属性为id)。3.确保SQLServer JDBC驱动连接字符串中没有设置useBulkCopyForBatchInsert=true(因为这会禁用返回主键的功能)。4.如果数据量超过1000条,分批次插入(每批1000条)。代码示例:分批次插入的Java代码: ```javapublic voidbatchInsert(List<User> users) {intbatchSize=1000;for (int i=0;i <users.size(); i+= batchSize){List<User> batchList= users.subList(i,Math.min(i +batchSize, users.size()));userMapper.insertUsers(batchList);} }```这样,每个批次插入1000条,并且每个User对象的主键id会被正确设置。因此,回答用户问题:在使用MyBatis进行批量插入操作时,可以通过上述方案获取所有插入记录的主键值。</think>在使用 MyBatis 进行批量插入操作时,从 SQL Server 获取所有插入记录的主键值,可通过以下方案实现: ### 解决方案(基于 SQL Server 特性) #### 核心方法:使用 `OUTPUT` 子句 ```xml <insert id="batchInsert" parameterType="java.util.List"> INSERT INTO target_table (column1, column2) OUTPUT INSERTED.id <!-- 返回插入的主键值 --> VALUES <foreach collection="list" item="item" separator=","> (#{item.value1}, #{item.value2}) </foreach> </insert> ``` #### 关键步骤: 1. **Mapper 接口定义**: ```java List<Long> batchInsert(List<Entity> list); // 返回主键列表 ``` 2. **SQL 优化**: - 使用 `OUTPUT INSERTED.id` 子句直接返回主键 - 通过 `<foreach>` 拼接批量 VALUES 语句 - 单次插入量建议 ≤ 1000 条(避免超长 SQL) 3. **JDBC 连接配置**: ```properties jdbc.url=jdbc:sqlserver://...;useBulkCopyForBatchInsert=false ``` *必须关闭批量复制模式(否则无法返回主键)[^3]* #### 执行效果: ```java List<Long> ids = mapper.batchInsert(entityList); // ids 包含所有新生成的主键值 ``` ### 备选方案(分批次插入) ```java // 手动分批次提交(每批1000条) try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) { for (int i = 0; i < list.size(); i++) { mapper.insertSingle(list.get(i)); if (i % 1000 == 0 || i == list.size() - 1) { session.commit(); // 触发主键获取 } } } ``` *需在单条插入语句中配置 `useGeneratedKeys="true"`* ### 注意事项: 1. **主键要求**: - 仅适用于 `IDENTITY` 自增主键或 `OUTPUT` 支持的列 - 非自增主键需提前生成主键值 2. **性能平衡**: - 大批量数据(>10万条)建议用 SQL Server 原生 `BULK INSERT` - 中等批量(1k~10万)用 `OUTPUT` 方案最优 3. **事务控制**: - 确保操作在事务内执行 - 批量失败时需回滚 > **SQL Server 限制**:`OUTPUT` 子句在包含 `TRIGGER` 的表上可能返回逻辑删除的伪记录,需测试验证[^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值