回顾
首先来梳理一下市面上常见的教程,还有介绍mybatis的书籍的一个思路,别让我们在一堆教程和练习中蒙圈了;那我们先回顾以下上篇笔记的内容吧,温故而知新!
官方的用户开发手册→简单了解全局配置文件和SQL映射文件→固定SQL感受增删改查→然后是重点部分:MyBatis获取参数值的两种方式#{}、${} ,注意#{}不是绝对的取代了${},${}也有不得不用的场景→获取参数值的集中情况:单个参数获取、多个参数获取、map类型获取、集合类型获取、@Param注解方式获取→最后看了@Param注解方式的源码,主要涉及到的是设置参数和执行SQL并封装结果集的部分,如果对前面是怎样创建SqlSessionFactory的、怎样获取XML配置的等等,可以看尚硅谷在2018年发布的MyBatis教程视频:
【尚硅谷MyBatis实战教程全套完整版(初学者零基础从入门到精通,好评如潮,资料齐全)】 https://www.bilibili.com/video/BV1mW411M737/?share_source=copy_web&vd_source=b94d76ca220e819adedd722fa97178f0
那我们接着学MyBatis吧,学不死的只会让我们更强!
MyBatis各种查询功能
查询返回单个数据值
<!--int getUserCount();-->
<select id="getUserCount" resultType="java.lang.Integer">
select count(*) from t_user
</select>
查询返回一个实体类对象
<!--User getUserById(Integer id);-->
<select id="getUserById" resultType="com.coffeeship.pojo.User">
select * from t_user where id = #{id}
</select>
查询返回一个List集合
<!--List<User> getAllUsers();-->
<select id="getAllUsers" resultType="com.coffeeship.pojo.User">
select * from t_user
</select>
查询返回一个Map集合
<!--Map<String, Object> getUserMap(@Param("username") String username, @Param("password") String password);-->
<select id="getUserMap" resultType="java.util.Map">
<!--select * from t_user where username = #{username} and password = #{password}-->
select id, username, email from t_user where username = #{username} and password = #{password}
</select>
查询返回多个Map集合
<!--List<Map<String, Object>> getAllUserMap();-->
<select id="getAllUserMap" resultType="java.util.Map">
select * from t_user
</select>
<!--Map<String, Object> getAllUserMaps();-->
<select id="getAllUserMaps" resultType="java.util.Map">
select * from t_user
</select>
最后附上最尊敬的Mapper接口和测试类
public interface UserMapper {
/**
* 查询用户的总记录数
* @return
* 在MyBatis中,对于Java中常用的类型都设置了类型别名
* 例如:java.lang.Integer-->int|integer
* 例如:int-->_int|_integer
* 例如:Map-->map,List-->list
*/
int getUserCount();
/**
* 根据用户id查询用户信息
* @param id
* @return
*/
User getUserById(Integer id);
/**
* 查询所有用户信息
* @return
*/
List<User> getAllUsers();
/**
* 根据用户名称和密码查询用户信息为map集合
* @param username, password
* @return
*/
Map<String, Object> getUserMap(@Param("username") String username, @Param("password") String password);
/**
* 查询所有用户信息为map集合
* @return
* 将表中的数据以map集合的方式查询,一条数据对应一个map;若有多条数据,就会产生多个map集合,此时可以将这些map放在一个list集合中获取
*/
List<Map<String, Object>> getAllUserMap();
/**
* 查询所有用户信息为map集合
* @return
* 将表中的数据以map集合的方式查询,一条数据对应一个map;
* 若有多条数据,就会产生多个map集合,并且最终要以一个map的方式返回数据,
* 此时需要通过@MapKey注解设置map集合的键,值是每条数据所对应的map集合
*/
@MapKey("id")
Map<String, Object> getAllUserMaps();
}
public class MyBatisTest {
@Test
public void testForSingle() {
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
int userCount = mapper.getUserCount();
System.out.println("用户总数为:" + userCount);
}
@Test
public void testForSingleUser() {
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.getUserById(2);
System.out.println(user);
}
@Test
public void testForAllUser() {
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> allUsers = mapper.getAllUsers();
allUsers.forEach(user -> System.out.println(user));
}
@Test
public void testForUserMap() {
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
Map<String, Object> userMap = mapper.getUserMap("admin", "123456");
System.out.println(userMap);
}
@Test
public void testForUserMapList() {
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<Map<String, Object>> allUserMap = mapper.getAllUserMap();
allUserMap.forEach(map -> System.out.println(map));
}
@Test
public void testForUserMaps() {
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
Map<String, Object> allUserMaps = mapper.getAllUserMaps();
System.out.println(allUserMaps);
}
}
特殊SQL的处理
这时候就可以用到很少使用到的获取参数法宝${}了,它有着非常惊人的字符串拼接能力,但也因此有了自己的软肋——SQL依赖注入问题,因此,大部分程序员都不经常使用这件法宝。
模糊匹配
<!--List<User> getUserByName(@Param("username") String username);-->
<select id="getUserByName" resultType="com.coffeeship.pojo.User">
<!--select * from t_user where username like '%${username}%'-->
<!--select * from t_user where username like concat('%', #{username}, '%')-->
select * from t_user where username like "%"#{username}"%"
</select>
批量删除(其实是关键字in的使用)
<!--int deleteBatch(String ids);-->
<delete id="deleteBatch">
delete from t_user where id in (${ids})
</delete>
动态设置表名(可以理解为表名也是形参)
<!--List<User> getUsers(String tableName);-->
<select id="getUsers" resultType="com.coffeeship.pojo.User">
select * from ${tableName}
</select>
添加功能获取自增主键
简单解释一下,这个处理说的是添加一条数据后,获取对应的自增主键的值,一般是为了某些添加功能的场景,添加数据后,需要马上根据这个主键添加其他的数据,“马上”的意思就是前面添加的数据还没来得及写库呢,所以我就需要添加之后就获取这个主键值,然后用于其他数据的添加。
好吧,好像罗里吧嗦被我说复杂了~
t_clazz(clazz_id,clazz_name)
t_student(student_id,student_name,clazz_id)
1、添加班级信息
2、获取新添加的班级的id
3、为班级分配学生,即将某学的班级id修改为新添加的班级的id
<!--int insertUser(User user);-->
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
insert into t_user values(null, #{username}, #{password}, #{age}, #{sex}, #{email})
</insert>
同样赋上Mapper接口和测试类,重点注意最后获取自增主键id那个测试方法哦
public interface UserMapper {
/**
* 测试模糊查询
* @param username
* @return
*/
List<User> getUserByName(@Param("username") String username);
/**
* 批量删除
* @param ids
* @return
*/
int deleteBatch(String ids);
/**
* 动态设置表名,查询所有的用户信息
* @param tableName
* @return
*/
List<User> getUsers(String tableName);
/*
* t_clazz(clazz_id,clazz_name)
* t_student(student_id,student_name,clazz_id)
* 1、添加班级信息
* 2、获取新添加的班级的id
* 3、为班级分配学生,即将某学的班级id修改为新添加的班级的id
* */
/**
* 添加用户信息
* @param user
* @return
* useGeneratedKeys:设置使用自增的主键
* keyProperty:因为增删改有统一的返回值是受影响的行数,因此只能将获取的自增的主键放在传输的参数user对象的某个属性中
*/
int insertUser(User user);
}
public class MyBatisTest {
@Test
public void testForMohu() {
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> userList = mapper.getUserByName("admin");
userList.forEach(user -> System.out.println(user));
}
@Test
public void testForDeleteBatch() {
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
int deleteBatch = mapper.deleteBatch("1, 2, 3");
System.out.println(deleteBatch);
}
@Test
public void testForTableName() {
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> tUser = mapper.getUsers("t_user");
tUser.forEach(user -> System.out.println(user));
}
@Test
public void testForInsertUser() {
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = new User(null, "coffee", "123456", 25, "女", "123456@163.com");
int insertUser = mapper.insertUser(user);
System.out.println(insertUser);//返回操作数量
System.out.println(user.getId());//返回用户主键id
}
}
自定义映射resultMap
总结一下,感觉所有的框架都是这样,有一些我写好的提供给你用,但是总有些特殊情况需要你自己处理的,这里就是,上面我们都一直在用resultType写固定的数据类型,像java.lang包下的基础数据类型啊、java.util包下的集合类型啊,还有自定义的类。
resultMap主要是处理实体类的属性和表字段的映射关系;
特殊情况就是①属性和字段名不匹配,通过驼峰映射也不能处理;②实体类多对一或一对多关系
属性和字段名不匹配
这里我仅仅是把实体类属性名更改了,并没有修改get set方法名称,实体类返回封装结果都没有问题,这就说明了,mybatis查询到结果后封装结果、设置参数时是通过set方法名称去匹配的,比如说,从数据库中查询到字段名为username,就会去实体类中去找setUsername这个方法,去注入属性值。
当我修改setUsername方法为setNickname后,属性值就为null了,因为注入属性值失败了。
这里有点像IOC容器里,注入属性那种意思,其实底层原理确实是类似的,XML配置文件解析,动态代理mapperProxy,反射。。。
欸欸欸,回到正题,这里表字段是username,set方法是setNickname怎么办呢?那就用一下自定义映射关系吧~
<resultMap id="userMap" type="com.coffeeship.pojo.User">
<id property="id" column="id"/>
<result property="nickname" column="username"/>
</resultMap>
<select id="getUserById" resultMap="userMap">
select * from t_user where id = #{id}
</select>
注意这里实体类属性我写的是nickName,set方法是setNickname,那么映射关系中的property写nickName或nickname都是可以的,但不是绝对不区分大小写,比如nicknAme或Nickname都是不可以的。经验证,只能写属性名或set方法去掉set的部分首字母小写。
若字段名和实体类中的属性名不一致,但是字段名符合数据库的规则(使用 _ ),实体类中的属性 名符合Java 的规则(使用驼峰)此时也可通过以下两种方式处理字段名和实体类中的属性的映射关系a> 可以通过为字段起别名的方式,保证和实体类中的属性名保持一致b> 可以在 MyBatis 的核心配置文件中设置一个全局配置信息 mapUnderscoreToCamelCase ,可以在查询表中数据时,自动将_ 类型的字段名转换为驼峰例如:字段名 user_name ,设置了 mapUnderscoreToCamelCase ,此时字段名就会转换为userName
实体类属性多对一
经典中的经典!查询员工信息以及员工所对应的部门信息,多对一哈
但其实在我现在这个公司,这个假设不成立,因为有好多领导或者优秀的同事是属于多个部门或组织的,典型的拿着一份钱,干多份活。
级联属性
这是一个联合查询,肯定没法用一个实体类来接收,那就可以自定义映射关系了
<resultMap id="empMap" type="com.coffeeship.pojo.Emp">
<id property="eId" column="e_id"/>
<result property="eName" column="e_name"/>
<result property="age" column="age"/>
<result property="email" column="email"/>
<result property="dept.dId" column="d_id"/>
<result property="dept.dName" column="d_name"/>
</resultMap>
<select id="getEmpById" resultMap="empMap">
select e.*, d.* from t_emp e left join t_dept d on e.d_id = d.d_id where e_id = #{id}
</select>
这里跟IOC注入内部bean有一点区别的地方是!IOC那边如果用dept.dId这种,要求Emp类中必须有dept属性的get方法,但mybatis这里没有这个要求,直接就可以用,因为我在Emp类中仅仅生成了所有属性的set方法。
使用association处理映射关系
<resultMap id="empMap" type="com.coffeeship.pojo.Emp">
<id property="eId" column="e_id"/>
<result property="eName" column="e_name"/>
<result property="age" column="age"/>
<result property="email" column="email"/>
<association property="dept" javaType="com.coffeeship.pojo.Dept">
<id property="dId" column="d_id"/>
<result property="dName" column="d_name"/>
</association>
</resultMap>
<select id="getEmpById" resultMap="empMap">
select e.*, d.* from t_emp e left join t_dept d on e.d_id = d.d_id where e_id = #{id}
</select>
分步查询
这个对于我来说,是一个新知识点哈哈, 意思就是先根据员工id查询员工信息,再根据员工信息里的部门id查询部门信息。
这里我第一次执行,报错了,原因是全局配置文件中没有扫描到对应的DeptMapper.xml
加上映射文件后,
<!--引入映射文件 来自源根的路径-->
<mappers>
<mapper resource="mappers/UserMapper.xml"/>
<mapper resource="mappers/EmpMapper.xml"/>
<mapper resource="mappers/DeptMapper.xml"/>
<!--以包为单位引入映射文件
要求: 1.mapper接口所在的包要和映射文件所在的包一致
2.mapper接口要和映射文件的名字一致
-->
<!--<package name="com.coffeeship.mapper"/>-->
</mappers>
后面是dept属性没获取到,是null,我猜测应该是dept属性和表字段不匹配导致的,而且也没开启驼峰映射。
开启一下映射,果然就有了
<settings>
<!--将表中字段的下划线自动转换为驼峰-->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
再开启延迟加载,修改Emp的toString
<settings>
<!--将表中字段的下划线自动转换为驼峰-->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!--开启延迟加载-->
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
分步查询的优点:可以实现延迟加载,但是必须在核心配置文件中设置全局配置信息:lazyLoadingEnabled :延迟加载的全局开关。当开启时,所有关联对象都会延迟加载aggressiveLazyLoading :当开启时,任何方法的调用都会加载该对象的所有属性。 否则,每个属性会按需加载此时就可以实现按需加载,获取的数据是什么,就只会执行相应的 sql 。此时可通过 association 和collection中的 fetchType 属性设置当前的分步查询是否使用延迟加载, fetchType="lazy( 延迟加载)|eager( 立即加载 )"
这里还是执行了两条sql,为什么呢?因为这里打印了emp对象,尽管toString中不打印dept属性内容,但系统还是觉得用到了dept。
不打印emp对象,则只执行了一条sql。
<settings>
<!--将表中字段的下划线自动转换为驼峰-->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!--开启延迟加载-->
<setting name="lazyLoadingEnabled" value="true"/>
<!--aggressiveLazyLoading改不改false都行,重点是lazyLoadingEnabled得设置为true-->
<!--<setting name="aggressiveLazyLoading" value="false"/>-->
</settings>
经测试,<setting name="lazyLoadingEnabled" value="true"/>全局配置不开启,也可以单个开启
<resultMap id="empMapStep" type="com.coffeeship.pojo.Emp">
<id property="eId" column="e_id"/>
<result property="eName" column="e_name"/>
<result property="age" column="age"/>
<result property="email" column="email"/>
<!--
select:设置分步查询,查询某个属性的值的sql的标识(namespace.sqlId)
column:将sql以及查询结果中的某个字段设置为分步查询的条件
-->
<!-- <association property="dept" column="d_id" javaType="com.coffeeship.pojo.Dept" fetchType="lazy" select="com.coffeeship.mapper.DeptMapper.getDeptById" />-->
<association property="dept" column="d_id" fetchType="lazy" select="com.coffeeship.mapper.DeptMapper.getDeptById" />
</resultMap>
association中的fetchType属性设置为lazy即可实现延迟加载
实体类属性一对多
collection标签处理
<resultMap id="deptMap" type="com.coffeeship.pojo.Dept">
<id property="dId" column="d_id"/>
<result property="dName" column="d_name"/>
<!--ofType:设置collection标签所处理的集合属性中存储数据的类型-->
<collection property="emps" ofType="com.coffeeship.pojo.Emp">
<id property="eId" column="e_id"/>
<result property="eName" column="e_name"/>
<result property="age" column="age"/>
<result property="email" column="email"/>
</collection>
</resultMap>
<select id="getDeptById" resultType="com.coffeeship.pojo.Dept">
select d.*, e.* from t_dept d left join t_emp e on d.d_id = e.d_id where d.d_id = #{id}
</select>
分步查询
<resultMap id="deptMap" type="com.coffeeship.pojo.Dept">
<id property="dId" column="d_id"/>
<result property="dName" column="d_name"/>
<collection property="emps" column="d_id" select="com.coffeeship.mapper.EmpMapper.getEmpByDid"/>
</resultMap>
<select id="getDeptById" resultMap="deptMap">
select * from t_dept where d_id = #{id}
</select>
<select id="getEmpByDid" resultType="com.coffeeship.pojo.Emp">
select * from t_emp where d_id = #{id}
</select>
体验延迟加载
好吧,再来记录最后一个部分,本笔记就结束吧
MyBatis动态SQL
简单来说,就是传参不一样,SQL就不一样。
Mybatis 框架的动态 SQL 技术是一种根据特定条件动态拼装 SQL 语句的功能,它存在的意义是为了解决拼接SQL 语句字符串时的痛点问题。
先说下都有哪些标签吧,if、where、trim、choose/when/otherwise、foreach、sql 熟悉吧?靠猜应该都能猜一半
if标签
if 标签可通过 test 属性的表达式进行判断,若表达式的结果为 true ,则标签中的内容会执行;反之标签中的内容不会执行
<select id="getUserDynamic" resultType="com.coffeeship.pojo.User">
select * from t_user where 1=1
<if test="username != null and username != ''">
and username = #{username}
</if>
<if test="password != null and password != ''">
and password = #{password}
</if>
<if test="age != null and age != ''">
and age = #{age}
</if>
<if test="email != null and email != ''">
and email = #{email}
</if>
</select>
where标签
细心的童鞋应该注意到了上面where关键词后有1=1, 这是为了避免紧跟着where后面的那个参数如果不传可能导致的SQL语法问题。
这也因此有了where标签,加上where标签,可以自动识别是否添加where关键字。
where 和 if 一般结合使用:a> 若 where 标签中的 if 条件都不满足,则 where 标签没有任何功能,即不会添加 where 关键字b> 若 where 标签中的 if 条件满足,则 where 标签会自动添加 where 关键字,并将条件最前方多余的and去掉注意: where 标签不能去掉条件最后多余的 and
<select id="getUserDynamic" resultType="com.coffeeship.pojo.User">
select * from t_user
<where>
<if test="username != null and username != ''">
and username = #{username}
</if>
<if test="password != null and password != ''">
and password = #{password}
</if>
<if test="age != null and age != ''">
and age = #{age}
</if>
<if test="email != null and email != ''">
and email = #{email}
</if>
</where>
</select>
trim标签
注意这里不要跟数据库那个trim函数去除首尾空格搞混了,但也有一个共同点就是都是在首尾操作:添加或删除前缀和后缀,注意prefixOverrides或suffixOverrides去掉内容可以是可选的,比如and|or意味着去掉多余的and或or。
trim 用于去掉或添加标签中的内容常用属性:prefix :在 trim 标签中的内容的前面添加某些内容prefixOverrides :在 trim 标签中的内容的前面去掉某些内容suffix :在 trim 标签中的内容的后面添加某些内容suffixOverrides :在 trim 标签中的内容的后面去掉某些内容
<!-- <select id="getUserDynamic" resultType="com.coffeeship.pojo.User">
select * from t_user
<!–and在前,去掉前面的and–>
<trim prefix="where" prefixOverrides="and">
<if test="username != null and username != ''">
and username = #{username}
</if>
<if test="password != null and password != ''">
and password = #{password}
</if>
<if test="age != null and age != ''">
and age = #{age}
</if>
<if test="email != null and email != ''">
and email = #{email}
</if>
</trim>
</select>-->
<select id="getUserDynamic" resultType="com.coffeeship.pojo.User">
select * from t_user
<!--and在后,去掉最后的and-->
<trim prefix="where" suffixOverrides="and|or">
<if test="username != null and username != ''">
username = #{username} and
</if>
<if test="password != null and password != ''">
password = #{password} and
</if>
<if test="age != null and age != ''">
age = #{age} and
</if>
<if test="email != null and email != ''">
email = #{email} and
</if>
</trim>
</select>
choose、when、otherwise标签
类似于if-else标签
<select id="getUserDynamic" resultType="com.coffeeship.pojo.User">
select * from t_user
<where>
<choose>
<when test="username != null and username != ''">
and username = #{username}
</when>
</choose>
<choose>
<when test="password != null and password != ''">
and password = #{password}
</when>
</choose>
<choose>
<when test="age != null and age != ''">
and age = #{age}
</when>
</choose>
<choose>
<when test="email != null and email != ''">
and email = #{email}
</when>
</choose>
</where>
</select>
这里相当于写了很多if。
<select id="getUserDynamic" resultType="com.coffeeship.pojo.User">
select * from t_user
<where>
<choose>
<when test="username != null and username != ''">
username = #{username}
</when>
<when test="password != null and password != ''">
password = #{password}
</when>
<when test="age != null and age != ''">
age = #{age}
</when>
<when test="email != null and email != ''">
email = #{email}
</when>
<otherwise>
1=1
</otherwise>
</choose>
</where>
</select>
这样把<when>标签都写在<choose>中才算是if-else-else-else;otherwise就相当于default。
foreach标签
这个比较熟悉,批量的增删改都需要用到这个标签,但这样的话,如果是SQL拼接会有一个SQL过长,数据库报错的问题;标签中应该有一个属性可以设置批量BATCH还是SINGLE,BATCH情况下好像就没有因为SQL过长的异常了。
属性:collection :设置要循环的数组或集合item :表示集合或数组中的每一个数据separator :设置循环体之间的分隔符open :设置 foreach 标签中的内容的开始符close :设置 foreach 标签中的内容的结束符
注意List中每个元素都是一个item,引用字段必须用item.字段名,比如这里只能用user.username,不能只写username。
当我们传递一个 List 实例或者数组作为参数对象传给 MyBatis。
MyBatis 会自动将它包装在一个 Map 中,用名称在作为键。List 实例将会以“list” 作为键,而数组实例将会以“array”作为键。
collection名称如果是List列表类型,可写arg0、collection、list,mybatis默认放到了Map中,当然也可以通过@Param指定列表类对象名称。
注意!!!Oracle数据库中不支持批量插入时这么写,insert into table values(?, ?, ?), (?, ?, ?); 可以通过sql嵌套,内层用select 字段, 字段, 字段 from dual去处理,或者写整个SQL,用分号分隔。比如:
报错Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException
不要慌,这是由于目前不支持多SQL执行导致的,就像SQLyog软件界面的这俩的区别:
只需要在数据库配置处加上allowMultiQueries=true即可;
不过这里,它把操作数量认为是1了。
删除和更新操作与此类似,就不赘述了。
最后一个标签:
SQL片段标签
sql 片段,可以记录一段公共 sql 片段,在使用的地方通过 include 标签进行引入。
看这意思,就是抽取一些常用的字段列,作为引用。
<!--Map<String, Object> getUserNamePwd(Integer id);-->
<sql id="userNamePwd">
username, password
</sql>
<select id="getUserNamePwd" resultType="java.util.Map">
select <include refid="userNamePwd"></include> from t_user where id = #{id}
</select>
好啦,今天的笔记就到这里结束吧,欢迎查看下篇笔记,预告:重点!难点!面试可能会问到的MyBatis缓存机制和分页插件。