真正的能力一定是在不确定中,可以找到确定性的能力。 “作为清华的学生,不应该仅仅是学一门谋生赚钱的技术,让自己衣食无忧,而应该具备家国情怀,否则我们的毕业生和蓝翔技校有什么区别?” 看明白和能掌握使用,中间还隔着千百次的练习。 “同学们,咱们蓝翔技校就是实打实的学本领,咱们不玩虚的,你学挖掘机就把地挖好,你学厨师就把菜炒好,你学裁缝就把衣服做好。咱们蓝翔如果不踏踏实实学本事,那跟清华北大又有什么区别呢?”
关注点分离[Separation of Concerns,简称SOC]是最经典的架构设计原则之一,在许多架构设计中被广泛使用。 关注点分离原则为我们的架构设计提出了三点要求。
简单的拿手机这个系统做例子,手机的外壳和屏幕上的膜属于高频被DIY的地方,属于需要变化的部分。 设计手机时要给这两个变化的部分预留好扩展。不能因为换个壳或换个膜,影响到手机其它功能的使用。 即手机换壳或换膜时,不要影响到打电话、拍照、充电、换SD卡等其它手机功能的正常使用。
Web应用刚出现时,使用的是服务端渲染技术,前端和后端的代码写在同一个代码文件中,譬如JSP、ASP就是这个时期的代表。 随后开发者逐渐意识到前后端主要解决的问题是不同的。在随后兴起的ASP.net和Struts框架,就有意识地对前端代码和后端代码就进行分离,前端和后端的代码写在不同的代码文件中。
前端需要考虑界面展现、数据展现和交互问题,后端需要考虑业务逻辑与数据逻辑问题。
这种情况下,就应该在架构上将它们进行分离。 同时,在团队协作上也能将前端和后端这两部分工作进行分离,因此出现了前端工程师和后端工程师这两个不同岗位。 这种分绝不是偶然的,它不仅让架构变得更加解耦,还能显著地提升团队的开发效率。
这一次分离,不仅解决了技术架构的问题,也满足了老板提升开发效率的问题。
业务逻辑对于我们开发而言不言自明,但数据逻辑包含包含哪些呢?最直接的就是一个个对应于数据库中每张数据表的实体对象:数据访问对象,即Data Access Object,简称DAO。
这一层数据直接和数据库打交道,我们将它们从业务逻辑中分离出来,并进行封装。也就是说,没必要为每一个DAO对象初始化的过程去编写大量的代码,这些代码应该封装到一个框架中。 我们只需要编写相应的SQL语句,并将这些SQL语句从业务代码中分离出来,最终将执行SQL语句所得到的结果集映射到DAO对象中即可。
MyBatis就是这样的框架:它能帮助我们将业务逻辑与数据逻辑进行分离 ,让开发应用程序的过程变得高效。
我们一直在不停地寻找避免重复的方法。文档的重复、设计的重复、编码的重复。MyBatis的动态SQL特性最大化地消除了应用层中拼SQL的重复。
如果你有使用JDBC或其他类似框架的经历,你就能体会到根据不同条件拼接SQL语句有多么痛苦。拼接的时候要确保不能忘了必要的空格,还要注意去掉列名最后的逗号。
Mybatis框架的动态SQL技术实现了一种根据特定条件动态拼装SQL语句的功能,它抽象并封装了根据条件拼接SQL语句这件事,让应用层和数据层的边界更清晰,极大地提升了代码的可读性和开发效率。
实现层面上,Mybatis是通过标签来实现动态SQL的。MyBatis3开始采用了强大的OGNL(Object-Graph Navigation Language)表达式语言让动态SQL更加容易使用、容易理解。
动态SQL:在编译时无法确定,只有等到程序运行起来,在执行的过程中才能确定,这种SQL叫做动态SQL。 譬如一个多条件筛选的列表查看功能,搜索时,可能会输入全部筛选条件、填入部分筛选条件或一个筛选条件都没有。这种情况下查询数据库的SQL都不尽相同。
MyBatis支持以下几种动态SQL:
if... else...
、if ... else ...
的场景。choose就是弥补这个空缺的。choose元素包含when和otherwise两个标签,一个choose中至少有一个when,有0个或1个otherwise。sql id in (1,2,3)
。可以使用${ids}方式直接写入sql 1,2,3
,但这种写法不能防止SQL注入。想避免SQL注入就需要用sql #{}
的方式,这时就要配合使用sql foreach
标签来满足需求。foreach标签可以对数组、Map或实现了Iterable接口(如List、Set)的对象进行遍历。数组在处理时会转换为List对象,因此foreach遍历的对象可以分为两类:Iterable类型和Map类型。
foreach标签包含以下属性:
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;
}
}
场景: 根据用户呢称,批量模糊查询符合条件的记录
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();
}
}
}
场景: 根据 登陆名 和 邮箱属性,批量查询符合条件的记录
/**
* 查询条件
*/
@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来实现。批量插入是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处理的参数是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%确定一定没有一个确定的规律,哈哈哈哈 真正的力量,则是能够从容的做当下事情的力量。