zl程序教程

您现在的位置是:首页 >  后端

当前栏目

从架构角度看MyBatis及foreach用法小结

mybatis架构 用法 小结 foreach 角度看
2023-06-13 09:15:36 时间
from DD

真正的能力一定是在不确定中,可以找到确定性的能力。 “作为清华的学生,不应该仅仅是学一门谋生赚钱的技术,让自己衣食无忧,而应该具备家国情怀,否则我们的毕业生和蓝翔技校有什么区别?” 看明白和能掌握使用,中间还隔着千百次的练习。 “同学们,咱们蓝翔技校就是实打实的学本领,咱们不玩虚的,你学挖掘机就把地挖好,你学厨师就把菜炒好,你学裁缝就把衣服做好。咱们蓝翔如果不踏踏实实学本事,那跟清华北大又有什么区别呢?”

从架构角度看MyBatis解决了啥问题

关注点分离[Separation of Concerns,简称SOC]是最经典的架构设计原则之一,在许多架构设计中被广泛使用。 关注点分离原则为我们的架构设计提出了三点要求。

  1. 架构中需要变化的部分,一定要能够非常清晰地被识别出来。
  2. 若架构中某部分发生变化,则变化不会影响到其它部分。
  3. 若架构中某部分需要扩展,则该扩展也不会影响到其它部分。架构能做到关注点分离,才能做到真正意义上的解耦,这是架构师们需要努力实现的目标。譬如"前后端分离"就是一个关注点分离的一个最有效的落地实践。

简单的拿手机这个系统做例子,手机的外壳和屏幕上的膜属于高频被DIY的地方,属于需要变化的部分。 设计手机时要给这两个变化的部分预留好扩展。不能因为换个壳或换个膜,影响到手机其它功能的使用。 即手机换壳或换膜时,不要影响到打电话、拍照、充电、换SD卡等其它手机功能的正常使用。

SOC的经典案例:前后端分离

Web应用刚出现时,使用的是服务端渲染技术,前端和后端的代码写在同一个代码文件中,譬如JSP、ASP就是这个时期的代表。 随后开发者逐渐意识到前后端主要解决的问题是不同的。在随后兴起的ASP.net和Struts框架,就有意识地对前端代码和后端代码就进行分离,前端和后端的代码写在不同的代码文件中。

前端需要考虑界面展现、数据展现和交互问题,后端需要考虑业务逻辑与数据逻辑问题。

这种情况下,就应该在架构上将它们进行分离。 同时,在团队协作上也能将前端和后端这两部分工作进行分离,因此出现了前端工程师和后端工程师这两个不同岗位。 这种分绝不是偶然的,它不仅让架构变得更加解耦,还能显著地提升团队的开发效率。

这一次分离,不仅解决了技术架构的问题,也满足了老板提升开发效率的问题。

SOC的经典案例:业务逻辑和数据逻辑分离与MyBatis的关系

业务逻辑对于我们开发而言不言自明,但数据逻辑包含包含哪些呢?最直接的就是一个个对应于数据库中每张数据表的实体对象:数据访问对象,即Data Access Object,简称DAO。

这一层数据直接和数据库打交道,我们将它们从业务逻辑中分离出来,并进行封装。也就是说,没必要为每一个DAO对象初始化的过程去编写大量的代码,这些代码应该封装到一个框架中。 我们只需要编写相应的SQL语句,并将这些SQL语句从业务代码中分离出来,最终将执行SQL语句所得到的结果集映射到DAO对象中即可。

MyBatis就是这样的框架:它能帮助我们将业务逻辑与数据逻辑进行分离 ,让开发应用程序的过程变得高效。

MyBatis的动态SQL特性

我们一直在不停地寻找避免重复的方法。文档的重复、设计的重复、编码的重复。MyBatis的动态SQL特性最大化地消除了应用层中拼SQL的重复。

如果你有使用JDBC或其他类似框架的经历,你就能体会到根据不同条件拼接SQL语句有多么痛苦。拼接的时候要确保不能忘了必要的空格,还要注意去掉列名最后的逗号。

Mybatis框架的动态SQL技术实现了一种根据特定条件动态拼装SQL语句的功能,它抽象并封装了根据条件拼接SQL语句这件事,让应用层和数据层的边界更清晰,极大地提升了代码的可读性和开发效率。

实现层面上,Mybatis是通过标签来实现动态SQL的。MyBatis3开始采用了强大的OGNL(Object-Graph Navigation Language)表达式语言让动态SQL更加容易使用、容易理解。

动态SQL:在编译时无法确定,只有等到程序运行起来,在执行的过程中才能确定,这种SQL叫做动态SQL。 譬如一个多条件筛选的列表查看功能,搜索时,可能会输入全部筛选条件、填入部分筛选条件或一个筛选条件都没有。这种情况下查询数据库的SQL都不尽相同。

MyBatis支持以下几种动态SQL:

  • 条件判断
    • where语句中,通过判断参数值来决定是否使用某个查询条件
    • update语句中,判断是否更新某一个字段
    • insert语句中,用来判断是否插入某个字段的值
    • if。if标签的使用场景:
    • choose (when otherwise)。if标签提供了基本的条件判断,但是它无法很好支持if... else...if ... else ... 的场景。choose就是弥补这个空缺的。choose元素包含when和otherwise两个标签,一个choose中至少有一个when,有0个或1个otherwise。
  • trim(where,set)
    • where标签的作用:如果该标签包含的元素中有返回值,就插入一个where;如果where后面的字符是以and或or开头的,就将开头的这个and或or去掉【封装了这部分重复】。
    • set标签用在update场景。这个标签的作用:如果set标签包含的元素中有返回值,就插入一个set;如果set后面的字符串是以逗号结尾,则将这个逗号去掉。需要应用自己处理set标签中没有值的情况。
    • where和set标签的功能都可以用trim来实现,并且在底层就是通过TrimSqlNode实现的。
  • foreach
    • SQL语句中使用in关键字,例如sql id in (1,2,3)。可以使用${ids}方式直接写入sql 1,2,3 ,但这种写法不能防止SQL注入。想避免SQL注入就需要用sql #{}的方式,这时就要配合使用sql foreach标签来满足需求。
  • bind
    • bind标签可以使用OGNL表达式创建一个变量并将其绑定到上下文中。

foreach

foreach标签可以对数组、Map或实现了Iterable接口(如List、Set)的对象进行遍历。数组在处理时会转换为List对象,因此foreach遍历的对象可以分为两类:Iterable类型和Map类型。

foreach标签概述

foreach标签包含以下属性:

  • collection:必填,值为要迭代循环的属性名。这个属性值的类型有很多。
  • item:变量名,值为从迭代对象中取出的每一个值。当迭代循环的对象是Map类型时,这个值为Map的value。
  • index:索引的属性名,在集合数组情况下值为当前索引值,当迭代循环的对象是Map类型时,这个值为Map的key(键值)。
  • open:整个循环内容开头的字符串。
  • close:整个循环内容结尾的字符串。
  • separator:每次循环的分隔符。

foreach使用场景小结:

foreach实现in集合 场景1:只对一个属性进行批量匹配

foreach实现in集合(或数组)是最简单和常用的一种情况。

public interface SysAccountDOMapper {
    List<SysAccountDO> selectByIdList(@Param("idList") List<Long> idList);
}
<?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.tangcheng.mybatis.introduction.rbac.mapper.SysAccountDOMapper">
    <sql id="Base_Column_List">
        <!--
          WARNING - @mbg.generated
          This element is automatically generated by MyBatis Generator, do not modify.
        -->
        id, nick_name, login_name, pwd, email, avatar, profile, creator_by
    </sql>

    <select id="selectByIdList"
            resultType="com.tangcheng.mybatis.introduction.rbac.domain.entity.SysAccountDO">
        select
        <include refid="Base_Column_List"/>
        from sys_account
        where id in
        <foreach collection="idList" open="(" close=")" separator="," item="id">
            #{id}
        </foreach>
    </select>
</mapper>

测试基类:

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.BeforeClass;

import java.io.IOException;
import java.io.Reader;

/**
 * mybatis-subject
 *
 * @Auther: cheng.tang
 * @Date: 2022/5/5 7:26 AM
 * @Description:
 */
@Slf4j
public class BaseMapperTest {

    protected static SqlSessionFactory sqlSessionFactory;

    @BeforeClass
    public static void init() {
        try {
            Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        }
    }

    public SqlSession getSqlSession() {
        return sqlSessionFactory.openSession();
    }

}

测试类:

import com.tangcheng.mybatis.introduction.BaseMapperTest;
import com.tangcheng.mybatis.introduction.rbac.domain.entity.SysAccountDO;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSession;
import org.junit.Assert;
import org.junit.Test;

import java.util.Arrays;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@Slf4j
public class SysAccountDOMapperTest extends BaseMapperTest {

    @Test
    public void test_selectByIdList() {
        SqlSession sqlSession = getSqlSession();
        try {
            SysAccountDOMapper sysAccountDOMapper = sqlSession.getMapper(SysAccountDOMapper.class);
            List<SysAccountDO> sysAccountDOList = sysAccountDOMapper.selectByIdList(Arrays.asList(1L, 2L));
            log.info("{} ", sysAccountDOList);
            assertThat(sysAccountDOList.size()).isEqualTo(2);
        } finally {
            sqlSession.close();
        }
    }


}

小贴士: 传的参数都要使用@Param注解指定名称,方便在XML文件中引用,这种显式声明的规范可以提升代码的可读性,减少引入错误的风险。 给参数添加@Param注解后,MyBatis就会自动将参数封装为Map类型,@Param注解的value中的值会作为Map中的key,这样SQL部分就可以通过配置的注解值来使用参数。 Mybatis有一套自己的规则来处理参数名,但不建议使用。 【推荐】in 操作能避免则避免,若实在避免不了,需要仔细评估 in 后边的集合元素数量,控 制在100个之内。

执行结果:

相关源码剖析: org.apache.ibatis.reflection.ParamNameResolver.wrapToMapIfCollection

package org.apache.ibatis.reflection;

import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.binding.MapperMethod.ParamMap;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

public class ParamNameResolver {

    public static final String GENERIC_NAME_PREFIX = "param";

    /**
     * Wrap to a {@link ParamMap} if object is {@link Collection} or array.
     *
     * @param object a parameter object
     * @param actualParamName an actual parameter name
     *                        (If specify a name, set an object to {@link ParamMap} with specified name)
     * @return a {@link ParamMap}
     * @since 3.5.5
     */
    public static Object wrapToMapIfCollection(Object object, String actualParamName) {
        if (object instanceof Collection) {
            ParamMap<Object> map = new ParamMap<>();
            /**
             * Mybatis会为参数集合参数生成一个默认的变量名。不建议使用
             * 在Mapper接口文件中显式声明,可读性更好
             */
            map.put("collection", object);
            if (object instanceof List) {
                map.put("list", object);
            }
            Optional.ofNullable(actualParamName).ifPresent(name -> map.put(name, object));
            return map;
        } else if (object != null && object.getClass().isArray()) {
            ParamMap<Object> map = new ParamMap<>();
            map.put("array", object);
            Optional.ofNullable(actualParamName).ifPresent(name -> map.put(name, object));
            return map;
        }
        return object;
    }
}
foreach实现in集合 场景2:or场景,单条件非精确匹配场景

场景: 根据用户呢称,批量模糊查询符合条件的记录

Mapper接口文件:

public interface SysAccountDOMapper {
    /**
     * 同时对多个用户呢称进行模糊查询
     *
     * @param searchedNickNameList
     * @return
     */
    List<Long> selectAllLikeMatchedAccountIds(@Param("searchedNickNameList") List<String> searchedNickNameList);

}

Mapper XML文件中的配置:


<select id="selectAllLikeMatchedAccountIds" resultType="java.lang.Long">
    select id from sys_account
    where
    <foreach collection="searchedNickNameList" open="(" close=")" separator="or" item="searchedNickName">
        nick_name like CONCAT('%',#{searchedNickName},'%')
    </foreach>
</select>

@Slf4j
public class SysAccountDOMapperTest extends BaseMapperTest {

    @Test
    public void test_selectAllLikeMatchedAccountIds() {
        SqlSession sqlSession = getSqlSession();
        try {
            SysAccountDOMapper sysAccountDOMapper = sqlSession.getMapper(SysAccountDOMapper.class);
            List<Long> sysAccountIdList = sysAccountDOMapper.selectAllLikeMatchedAccountIds(Arrays.asList("管理", "普通", "用户"));
            log.info("{} ", sysAccountIdList);
            assertThat(sysAccountIdList.size()).isEqualTo(2);
        } finally {
            sqlSession.close();
        }
    }

}
执行结果:
foreach实现in集合 场景3:or,多个属性进行匹配查询

场景: 根据 登陆名 和 邮箱属性,批量查询符合条件的记录

/**
 * 查询条件
 */
@Builder
@Data
public class LoginNameAndEmailBO {
    /**
     * 登陆名
     */
    private String loginName;

    /**
     * 邮箱
     */
    private String email;

}

Mapper接口文件:

public interface SysAccountDOMapper {
    /**
     * 多条件foreach or的场景。多条件,无法用in
     *
     * @param loginNameAndEmailList
     * @return
     */
    List<SysAccountDO> selectByLoginNameAndEmailList(@Param("loginNameAndEmailList") List<LoginNameAndEmailBO> loginNameAndEmailList);
}

Mapper XML文件中的配置:


<select id="selectByLoginNameAndEmailList"
        resultType="com.tangcheng.mybatis.introduction.rbac.domain.entity.SysAccountDO">
    select
    <include refid="Base_Column_List"/>
    from sys_account
    where
    <foreach collection="loginNameAndEmailList" item="loginNameAndEmailBO" open="(" close=")" separator="or">
        (login_name=#{loginNameAndEmailBO.loginName} and email=#{loginNameAndEmailBO.email} )
    </foreach>
</select>

小帖士: 当使用对象内的对象时,使用属性.属性(集合和数组可以使用下标取值)的方式可以引用多层嵌套的属性值。 譬如loginNameAndEmailBO.loginName、loginNameAndEmailBO.email


@Slf4j
public class SysAccountDOMapperTest extends BaseMapperTest {

    @Test
    public void test_selectByLoginNameAndEmailList() {
        SqlSession sqlSession = getSqlSession();
        try {
            SysAccountDOMapper sysAccountDOMapper = sqlSession.getMapper(SysAccountDOMapper.class);
            List<LoginNameAndEmailBO> loginNameAndEmailList = new ArrayList<LoginNameAndEmailBO>();
            loginNameAndEmailList.add(LoginNameAndEmailBO.builder().loginName("admin").email("admin@chaojihao.net").build());
            loginNameAndEmailList.add(LoginNameAndEmailBO.builder().loginName("test").email("test@chaojihao.net").build());
            List<SysAccountDO> sysAccountDOList = sysAccountDOMapper.selectByLoginNameAndEmailList(loginNameAndEmailList);
            log.info("{} ", sysAccountDOList);
            assertThat(sysAccountDOList.size()).isEqualTo(2);
        } finally {
            sqlSession.close();
        }
    }

}

执行结果:

foreach实现批量插入

如果数据库支持批量插入,就可以通过foreach来实现。批量插入是SQL-92新增的特性,目前支持的数据库有DB2、SQL Server2008及以上版本、PostgreSQL8.2及以上版本、 MySQL、SQLite3.7.11及以上版本、H2。批量插入的语法如下:

INSERT INTO tablename (column-a, [ column -b, ...])
values ('value-a-1', ['value-b-1', ...]),
       ('value-a-2', ['value-b-2', ...]),
    ...

从待处理部分可以看出,后面是一个值的循环,因此可以通过foreach进行动态拼SQL。

场景:批量创建用户 Mapper接口文件:

public interface SysAccountDOMapper {
    /**
     * 多条件foreach批量插入
     *
     * @return
     */
    int batchInsert(@Param("accountList") List<SysAccountDO> accountList);
}

Mapper XML文件中的配置:


<insert id="batchInsert">
    insert into sys_account(nick_name, login_name, pwd, email, avatar, profile, creator_by,modified_by)
    values
    <foreach collection="accountList" item="account" separator=",">
        (
        #{account.nickName},#{account.loginName},#{account.pwd},#{account.email},#{account.avatar},
        #{account.profile},#{account.creatorBy},#{account.modifiedBy}
        )
    </foreach>
</insert>

测试类:


@Slf4j
public class SysAccountDOMapperTest extends BaseMapperTest {

    @Test
    public void test_batchInsert() {
        SqlSession sqlSession = getSqlSession();
        try {
            SysAccountDOMapper sysAccountDOMapper = sqlSession.getMapper(SysAccountDOMapper.class);
            List<SysAccountDO> accountList = new ArrayList<SysAccountDO>();
            int expectedInsertRecordCount = 10;
            for (int i = 0; i < expectedInsertRecordCount; i++) {
                SysAccountDO sysAccountDO = new SysAccountDO();
                sysAccountDO.setNickName("用户_" + i);
                sysAccountDO.setLoginName("user_" + i);
                sysAccountDO.setPwd("pwd_" + i);
                sysAccountDO.setEmail("email_" + i);
                sysAccountDO.setCreatorBy(1L);
                sysAccountDO.setModifiedBy(1L);
                accountList.add(sysAccountDO);
            }
            int actualInsertRecordsCount = sysAccountDOMapper.batchInsert(accountList);
            assertThat(actualInsertRecordsCount).isEqualTo(expectedInsertRecordCount);
        } finally {
            sqlSession.rollback(true);
            sqlSession.close();
        }
    }

}

小帖士: 批量Insert时,每批的数量要根据情况调整,MySQL每批建议200条

执行结果:
foreach实现动态update

当foreach处理的参数是Map类型时,foreach标签的index属性值对应的不是索引值,而是Map中的key,利用这个key可以实现动态UPDATE

此处只是聊下foreach的一个能力。不建议通过这种在业务代码中这样使用。因为不debug下,根据不知道这段代码具体的执行情况,可读性差。

场景:批量创建用户 Mapper接口文件:

public interface SysAccountDOMapper {
    /**
     * 根据Map类型来动态更新记录值
     *
     * @return
     */
    int updateByMap(@Param("mapParams") Map<String, Object> mapParams);
}

Mapper XML文件中的配置:


<update id="updateByMap">
    update sys_account set
    <foreach collection="mapParams" item="newValue" index="fieldName" separator=",">
        ${fieldName}=#{newValue}
    </foreach>
    where id=#{mapParams.id}
</update>

测试类:


@Slf4j
public class SysAccountDOMapperTest extends BaseMapperTest {

    @Test
    public void test_updateByMap() {
        SqlSession sqlSession = getSqlSession();
        try {
            SysAccountDOMapper sysAccountDOMapper = sqlSession.getMapper(SysAccountDOMapper.class);
            Map<String, Object> mapParams = new HashMap<String, Object>();
            mapParams.put("id", 1L);
            mapParams.put("nick_name", "newNickName");
            mapParams.put("avatar", "https://f.chaojihao.net/logo/wxmplogog.jpeg");
            int actualInsertRecordsCount = sysAccountDOMapper.updateByMap(mapParams);
            assertThat(actualInsertRecordsCount).isEqualTo(1);
        } finally {
            sqlSession.rollback(true);
            sqlSession.close();
        }
    }

}


执行结果:

https://gitee.com/baidumap/mybatis-subject/blob/master/mybatis-introduction/src/test/java/com/tangcheng/mybatis/introduction/rbac/mapper/SysAccountDOMapperTest.java

疫情隔离这段时间,真是一段特殊的经历,加深了对一些事情的理解。 在家里呆一段时间后,会觉得莫名的烦闷,全身上下莫名的不自在,站在窗口看外面的感觉还不够,把窗户打开还不够,要把窗纱也打开,感觉还不够。小朋友呆在家里烦躁哭闹大抵也是这个原因吧。 傍晚,听到窗户外小孩子们稚嫩的嬉笑打闹声,清脆地传过来,总能激起心里的一阵涟漪,总有一种特别的感觉,感觉与这个地方还是联系在一起的。 站在外面,看天高地阔,看蓝天白云,看绿树红花。站在人堆里,看稚子嬉闹,看三三两两聊天,就觉得莫名的心安,看着看着,听着听着,好像放松了下来,感觉到了困意,有些想睡个好觉。 心里头想点什么,嘴上没个没个把门的,全都给说了。哈哈哈哈

疫情在家里这段时间,听到旁边火车进行时发出的铛铛铛的声音了。以前一直觉得是一个假的铁路线

对于可控的事情,我们保持谨慎。 对于不可控的事情,我们保持乐观。乐观能点亮人,感染人,做一个乐观的吧 无论在什么情境下,真正的自由一定不是外在的自由,真正的自由一定是心灵的自由。 真正的能力一定是在不确定中,可以找到确定性的能力。 研究嘛,一定在确信有规则才去做。如果100%确定一定没有一个确定的规律,哈哈哈哈 真正的力量,则是能够从容的做当下事情的力量。