MyBatis(二)
MyBatis resultMap元素
resultMap是MyBatis中最复杂的元素,主要用于解决实体类属性名与数据库表中字段名不一致的情况,可以将查询结果映射成实体对象。下面我们先从最简单的功能开始介绍
现有的MyBatis版本只支持resultMap查询,不支持更新或者保存,更不必说级联的更新、删除和修改。
-
resultMap元素的构成
resultMap元素可以包含以下子元素,代码如下
<resultMap id = "" type = "" > <!--类在实例化时用来注入结果的构造方法--> <constructor> <!--ID是参数,结果为ID--> <idArg></idArg> <!--注入到构造方法的一个普通结果--> <arg></arg> </constructor> <!-- 用于表示哪个列是主键 --> <id></id> <!--注入到字段或JavaBean属性的普通结果--> <result></result> <!--用于一对一关联--> <association property = ""></association> <!--用于一对多、多对多关联--> <collection property = ""></collection> <!--使用结果值来决定使用哪个结果映射--> <discriminator javaType = ""> <!--基于某些值的结果映射--> <case value = ""></case> </discriminator> </resultMap>
其中:
<resultMap>
元素的 type 属性表示需要的 POJO,id 属性是 resultMap 的唯一标识。- 子元素
<constructor>
用于配置构造方法。当一个 POJO 没有无参数构造方法时使用。 - 子元素
<id>
用于表示哪个列是主键。允许多个主键,多个主键称为联合主键。 - 子元素
<result>
用于表示 POJO 和 SQL 列名的映射关系。 - 子元素
<association>
、<collection>
和<discriminator>
在级联的情况下使用
<id>
和<result>
元素都有以下属性- property: 映射到列结果的字段或属性。如果 POJO 的属性和 SQL 列名(column元素)是相同的,那么 MyBatis 就会映射到 POJO 上
- column:对应SQL列
- javaType:配置Java类型。可以是特定的类完全限定名或MyBatis上下文的别名
- jdbcType:配置数据库类型。这是JDBC类型,MyBatis已经为我们做了限定,基本支持所有常用的数据库类型
- typeHandler:类型处理器。允许你用特定的处理器来覆盖MyBatis默认的处理器。需要指定jdbcType和javaType相互转化的规则
一条 SQL 查询语句执行后会返回结果集,结果集有两种存储方式,即使用 Map 存储和使用 POJO 存储。
-
使用Map存储结果集
任何select语句都可以使用Map存储,代码如下
UserMapper.java
List<Map<String,Object>> selectUserById(int id);
UserMapper.xml
<select id="selectUserById" resultType="map"> select * from user where id = #{id} </select>
Map的key是select语句查询的字段名(必须完全一样),而Map的value是查询返回结果中字段对应的值,一条记录映射到一个Map对象中。
使用Map存储结果集很方便,但可读性稍差,所以一般推荐使用POJO的方式存储
-
使用POJO存储结果集
因为MyBatis提供了自动映射,所以使用POJO存储结果集是最常用的方式。但有时候需要更加复杂的映射或级联,这时就需要使用select元素的resultMap属性配置映射集合。
UserMapper.XML代码如下
<resultMap id="myResult" type="com.heng.pojo.User"> <id property="id" column="id"></id> <result property="name" column="name"></result> </resultMap>
resultMap 元素的属性 id 代表这个 resultMap 的标识,type 标识需要映射的 POJO。我们可以使用 MyBatis 定义好的类的别名或自定义类的全限定名。
这里使用 property 元素指定 Website 的属性名称 uname,column 表示数据库中 website 表的 SQL 列名 name,将 POJO 和 SQL 的查询结果一 一对应。
<select id="selectAllByResultMap" resultMap="myResult"> select id,name from user </select>
可以发现 SQL 语句的列名和 myResult 中的 column 一一对应。
-
resultType和resultMap的区别
MyBatis 的每一个查询映射的返回类型都是 resultMap,只是当我们提供的返回类型是 resultType 时,MyBatis 会自动把对应的值赋给 resultType 所指定对象的属性,而当我们提供的返回类型是 resultMap 时,MyBatis 会将数据库中的列数据复制到对象的相应属性上,可用于复制查询。
需要注意的是,resultMap 和 resultType 不能同时使用。
日志工厂
思考:当我们在测试SQL的时候,要是能够在控制台输出SQL的话,是不是就能够有更快的排错效率?
如果一个数据库相关的操作出现了问题,我们可以根据输出的SQL语句快速排查问题。
对于以往的开发过程,我们会经常使用debug模式来调节,跟踪我们的代码执行过程。但是现在使用MyBatis开发是基于接口的,配置文件的源代码执行过程。因此,我们必须选择日志工具来作为我们开发,调节程序的工具
MyBatis内置的日志工厂提供日志功能,具体的实现有以下几种工具:
-
STDOUT_LOGGING(标准日志实现)
-
SLF4J
-
Apache Commons Logging
-
Log4j 2
-
Log4j(最常用的日志工具)
-
JDK logging
标准日志实现
指定 MyBatis 应该使用哪个日志记录实现。如果此设置不存在,则会自动发现日志记录实现
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
测试,可以看到控制台有大量的输出!我们可以通过这些输出来判断程序到底哪里出了Bug
Log4J
简介:
- Log4j是Apache的一个开源项目
- 通过使用Log4j,我们可以控制日志信息输送的目的地:控制台,文本,GUI组件….
- 我们也可以控制每一条日志的输出格式;
- 通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。最令人感兴趣的就是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。
使用步骤:
-
导log4j的包
<!-- https://mvnrepository.com/artifact/log4j/log4j --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency>
-
配置文件编写
#将等级为DEBUG的日志信息输出到console和file这两个目的地,console和file的定义在下面的代码 log4j.rootLogger=DEBUG,console,file #控制台输出的相关设置 log4j.appender.console = org.apache.log4j.ConsoleAppender log4j.appender.console.Target = System.out log4j.appender.console.Threshold=DEBUG log4j.appender.console.layout = org.apache.log4j.PatternLayout log4j.appender.console.layout.ConversionPattern=[%c]-%m%n #文件输出的相关设置 log4j.appender.file = org.apache.log4j.RollingFileAppender log4j.appender.file.File=./log/heng.log log4j.appender.file.MaxFileSize=10mb log4j.appender.file.Threshold=DEBUG log4j.appender.file.layout=org.apache.log4j.PatternLayout log4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%c]%m%n #日志输出级别 log4j.logger.org.mybatis=DEBUG log4j.logger.java.sql=DEBUG log4j.logger.java.sql.Statement=DEBUG log4j.logger.java.sql.ResultSet=DEBUG log4j.logger.java.sql.PreparedStatement=DEBUG
-
setting设置日志实现
<settings> <setting name="logImpl" value="LOG4J"/> </settings>
-
在程序中使用Log4j进行输出
import org.apache.log4j.Logger; class Log4JTest{ static Logger logger = Logger.getLogger(MyTest.class); @Test public void testLog4j(){ logger.info("info:进入了testLog4j方法"); logger.debug("debug:进入了testLog4j方法"); logger.error("error:进入了testLog4j方法"); SqlSession session = MybatisUtils.getSession(); UserMapper mapper = session.getMapper(UserMapper.class); List<User> users = mapper.selectUser(); for (User user : users) { System.out.println(user); } session.close(); } }
-
测试,看控制台输出!
- 使用Log4j 输出日志
- 可以看到还生成了一个日志的文件 【需要修改file的日志级别】
MyBatis实现分页
为什么需要分页?
在学习MyBatis等持久层框架的时候,会经常对数据进行增、删、改、查操作,使用最多的是对数据库进行查询操作,在查询大量数据的时候,我们往往会使用分页进行查询,也就是每次处理小部分数据,这样对数据库压力就在可控范围。
-
使用Limit实现分页
#语法 #startIndex:分页开始的页码;pageSize:分页的大小(分多少页) select * from user limit startIndex,pageSize #查询第6到10行记录(检索行6-10) select * from user limit 5,5 #为了检索从某一个偏移量到记录集的结束所有的记录行,可以指定第二个参数为 -1 #检索第91行到最后一行的数据(91-last) select * from user limit 90,-1 #如果给定一个参数,它表示返回最大的记录行数目 #检索前5行记录(0-5) select * from user limit 5 #换句话说,LIMIT n 等价于 LIMIT 0,n。
步骤:
-
修改UserMapper接口,或使用注解
List<User> selectUserByLimit(Map<String, Integer> map);
使用注解
@Select("select * from user limit #{startIndex},#{pageSize}") @ResultMap(value = "userMapper") public List<User> seletUserByLimit(Map<String, Integer> map);
-
修改Mapper文件(使用注解不需要)
<select id="selectUserByLimit" resultMap="myResult" parameterType="map"> select * from user limit #{startIndex},#{pageSize} </select>
-
在测试类进行传参测试
@Test public void selectUserByLimit(){ SqlSession session = MybatisUtils.getSession(); UserMapper mapper = session.getMapper(UserMapper.class); HashMap<String, Integer> map = new HashMap<>(); map.put("startIndex",2); map.put("pageSize",2); List<User> users = mapper.selectUserByLimit(map); for (User user : users) { System.out.println(user); } session.close(); }
-
测试程序,输出结果
-
-
RowBounds分页
我们除了使用Limit在SQL层面实现分页,也可以使用RowBounds在Java代码层面实现分页,当然此种方式作为了解即可。我们来看下如何实现的!
步骤:
-
Mapper接口
//选择全部用户RowBounds实现分页 List<User> getUserByRowBounds();
-
mapper文件
<select id="getUserByRowBounds" resultType="user"> select * from user </select>
-
测试类
在这里,我们需要使用RowBounds类
@Test public void testUserByRowBounds() { SqlSession session = MybatisUtils.getSession(); int currentPage = 2; //第几页 int pageSize = 2; //每页显示几个 RowBounds rowBounds = new RowBounds((currentPage-1)*pageSize,pageSize); //通过session.**方法进行传递rowBounds,[此种方式现在已经不推荐使用了] List<User> users = session.selectList("com.heng.mapper.UserMapper.getUserByRowBounds", null, rowBounds); for (User user: users){ System.out.println(user); } session.close(); }
-
-
PageHelper实现分页
官方文档:https://pagehelper.github.io/
MyBatis注解
MyBatis注解本质是反射实现!底层为动态代理模式
为了简化XML的配置,MyBatis提供了注解。我们可以通过MyBatis的jar包查看注解,如下图所示
以上注解主要分为三大类,即SQL语句映射、结果集映射和关系映射。下面将分别进行讲解
-
SQL语句映射
-
@Insert:实现新增功能
@Insert("insert into user(id,name) values(#{id},#{name})") public int insert(User user);
-
@Select:实现查询功能
@Select("Select * from user") @Results({ @Result(id = true,column = "id" , property = "id"), @Result(id = true,column = "name" , property = "name"), @Result(id = true,column = "pwd" , property = "pwd") }) List<User> queryAllUser();
-
@SelectKey:插入后,获取id的值
以MySQL为例,MySQL在插入一条数据后,使用select last_insert_id()可以获取到自增id的值
@Insert("insert into user(id,name) values(#{id},#{name})") @SelectKey(statement = "select last_insert_id",keyProperty = "id",KeyColumn = "id",resultType = int,before = false) public int insert(User user);
@SelectKey各个属性含义如下
- statement:表示要运行的SQL语句
- keyProperty:可选项,表示将查询结果赋值给数据表中的哪一列;
- keyColumn:可选项,表示将查询结果赋值给数据表中的哪一列;
- resultType:指定SQL语句的返回值;
- before:默认值为true,在执行插入语句之前,执行select last_insert_id()。值为false,则在执行插入语句之后,执行select last_insert_id()。
-
@Update:实现更新功能
@Update("Update user set name = #{name},pwd = #{pwd} where id = #{id}") public void updateUserById(User user);
-
@delete:实现删除功能
@ddelete("delete from user where id = #{id}") public void deleteUserById(Integer id);
-
@Param:映射多个参数
@Param用于在Mapper接口中映射多个参数
int saveUser(@Param(value="user") User user,@Param("name") String name,@Param("pwd") String pwd);
@Param 中的 value 属性可省略,用于指定参数的别名 。
关于@Param注解:
- 基本类型的参数或者String类型的参数都需要加上这个注解
- 引用类型不需要加
-
-
结果集映射
@Result、@Results、@ResultMap 是结果集映射的三大注解。
声明结果集映射关系代码:
@Select({"select id, name, class_id from student"}) @Results(id="studentMap", value={ @Result(column="id", property="id", jdbcType=JdbcType.INTEGER, id=true), @Result(column="name", property="name", jdbcType=JdbcType.VARCHAR), @Result(column="class_id ", property="classId", jdbcType=JdbcType.INTEGER) }) List<Student> selectAll();
下面为 @Results 各个属性的含义。
- id:表示当前结果集声明的唯一标识;
- value:表示结果集映射关系;
- @Result:代表一个字段的映射关系。其中,column 指定数据库字段的名称,property 指定实体类属性的名称,jdbcType 数据库字段类型,id 为 true 表示主键,默认 false。
可使用 @ResultMap 来引用映射结果集,其中 value 可省略。
@Select({"select id, name, class_id from student where id = #{id}"}) @ResultMap(value="studentMap") Student selectById(Integer id);
这样不需要每次声明结果集映射时都复制冗余代码,简化开发,提高了代码的复用性。
-
关系映射
-
@one:用于一对一关系映射
@Select("select * from student") @Results({ @Result(id=true,property="id",column="id"), @Result(property="name",column="name"), @Result(property="age",column="age"), @Result(property="address",column="address_id",one=@One(select="net.biancheng.mapper.AddressMapper.getAddress")) }) public List<Student> getAllStudents();
-
@many:用于一对多关系映射
@Select("select * from t_class where id=#{id}") @Results({ @Result(id=true,column="id",property="id"), @Result(column="class_name",property="className"), @Result(property="students", column="id", many=@Many(select="net.biancheng.mapper.StudentMapper.getStudentsByClassId")) }) public Class getClass(int id);
-
MyBatis关联(级联)查询
级联关系是一个数据库实体的概念,有3种级联关系,分别是一对一级联、一对多级联以及多对多级联。例如,一个角色可以分配给多个用户,也可以只分配给一个用户。大部分场景下,我们都需要获取角色信息和用户信息,所以会经常遇见一下SQL:
SELECT r.*,u.* FROM t_role r
INNER JOIN t_user_role ur ON r.id = ur.id
INNER JOIN t_user u ON ur.user_id = u.id
WHERE r.id = #{id}
在级联中存在三种对应的关系。
- 一对多的关系,如角色和用户的关系。通俗的理解就是,一家软件公司会存在许多软件工程师,公司和软件工程师就是一对多的关系。
- 一对一的关系。每个软件工程师都有一个编号(ID),这是他在公司的标识,它与工程师是一对一的关系。
- 多对多的关系,有一些公司一个角色可以对应多个用户,但是一个用户可以兼任多个角色。通俗的说,一个人既可以是总经理,同时也是技术总监,而技术总监这个职位可以对应多个人,这就是多对多的关系。
实际应用中,由于多对多的关系比较复杂,会增加理解和关联的复杂度,所以应用较少。推荐的方法是,用一对多的关系把它分解为双向关系,以降低关系的复杂度,简化程序。
级联的优点是获取关联数据十分便捷。但是级联过多会增加系统的复杂度,同时降低系统的性能,此增彼减。所以记录超过 3 层时,就不要考虑使用级联了,因为这样会造成多个对象的关联,导致系统的耦合、负载和难以维护。
MyBatis一对一关联查询
一对一级联关系在现实生活中是非常常见的,例如一个大学生只有一个学号,一个学号只属 于一个学生。同样,人与身份证也是一对一的级联关系。
在MyBatis中,通过<resultMap>
元素的子元素<association>
处理一对一级联关系。实例代码如下:
<association property="studentCard" column="cardId"
javaType="net.biancheng.po.StudentCard"
select="net.biancheng.mapper.StudentCardMapper.selectStuCardById"></association>
在<association>
元素中通常使用以下属性:
- property:指定映射到实体类的对象属性
- column:指定表中对应的字段(即查询返回的列名)。
- javaType:指定映射到实体对象属性的类型。
- select:指定引入嵌套查询的子SQL语句,该属性用于关联映射中的嵌套查询
一对一关联查询可采用以下两种方式:
- 单步查询,通过关联查询实现
- 分步查询,通过两次或多次查询,为一对一关系的实体Bean赋值
MyBatis多对一关联查询
多对一关联查询其实就是一对一关联查询的衍生;
例如:
-
多个学生对应一个老师
-
如果对于学生这边,就是一个多对一的现象,即从学生这边关联一个老师!
-
在数据库中关系图如下
示例
查询学生的所有信息,包括对应老师的姓名
基于查询嵌套处理关联
-
设计MySQL
CREATE TABLE `teacher` ( `id` INT(10) NOT NULL, `name` VARCHAR(30) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=INNODB DEFAULT CHARSET=utf8 INSERT INTO teacher(`id`, `name`) VALUES (1, '秦老师'); CREATE TABLE `student` ( `id` INT(10) NOT NULL, `name` VARCHAR(30) DEFAULT NULL, `tid` INT(10) DEFAULT NULL, PRIMARY KEY (`id`), KEY `fktid` (`tid`), CONSTRAINT `fktid` FOREIGN KEY (`tid`) REFERENCES `teacher` (`id`) ) ENGINE=INNODB DEFAULT CHARSET=utf8 INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('1', '小明', '1'); INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('2', '小红', '1'); INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('3', '小张', '1'); INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('4', '小李', '1'); INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('5', '小王', '1');
-
创建实体类Student以及Teacher
Srudent类
package com.heng.pojo; import lombok.Data; /** * @Author: minster * @Date: 2021/10/29 15:27 */ //使用lombok的Data注解可以帮我们自动配置构造器、getter和setter方法 @Data public class Student { private int id; private String name; private Teacher teacher; }
Teacher类
package com.heng.pojo; import lombok.Data; /** * @Author: minster * @Date: 2021/10/29 15:26 */ @Data public class Teacher { private int id; private String name; }
-
创建StudentMapper接口,在此接口增加方法
package com.heng.dao; import com.heng.pojo.Student; import org.apache.ibatis.annotations.Param; import java.util.List; /** * @Author: minster * @Date: 2021/10/29 15:28 */ @SuppressWarnings({"all"}) public interface StudentMapper { //查询所有的学生信息,以及对应的老师信息 List<Student> selectStudentAll(); }
-
编写对应的Mapper.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.heng.dao.StudentMapper"> <!-- 需求:获取所有学生及对应老师的信息 思路: 1.获取所有学生的信息 2.根据获取到的学生信息的tid->获取该老师的信息 3.查询出来的学生结果集中包含了老师,我们需要使用关联查询来处理结果集 3.1 做一个结果集映射TeacherResult 3.2 TeacherResult结果集的类型为Student 3.3 在学生表中,有一个Teacher类的属性teacher,让其对应数据库中的tid 3.4 利用association标签来处理一个复杂类型的关联;使用它来处理关联查询 --> <select id="selectStudentAll" resultMap="TeacherResult" > select * from student </select> <resultMap id="TeacherResult" type="com.heng.pojo.Student"> <result column="id" property="id"></result> <result column="name" property="name"></result> <association property="teacher" column="tid" javaType="com.heng.pojo.Teacher" select="selectTeacher"> </association> </resultMap> <select id="selectTeacher" resultType="com.heng.pojo.Teacher"> select * from teacher where id = #{tid} </select> </mapper>
association关联的属性:、
-
property属性名
-
javaType属性类型
-
column在多的一方的表中的列名
column多参数配置:
column="{key=value,key=value}" 其实就是键值对的形式,key是传给下个sql的取值名称,value是片段一中sql查询的字段名。
-
-
测试
@Test public void testSelectAllStudent(){ SqlSession session = MybatisUtils.getSession(); StudentMapper mapper = session.getMapper(StudentMapper.class); List<Student> studentList = mapper.selectStudentAll(); for (Student student : studentList) { System.out.println(student); } session.close(); }
-
运行结果
按结果嵌套处理
思路:直接查询出结果,进行结果集映射;查出来的数据需要起别名,不然两个实体类都会被同一个表数据映射
-
修改StudentMapper接口
package com.heng.dao; import com.heng.pojo.Student; import org.apache.ibatis.annotations.Param; import java.util.List; /** * @Author: minster * @Date: 2021/10/29 15:28 */ @SuppressWarnings({"all"}) public interface StudentMapper { //查询所有的学生信息,以及对应的老师信息 List<Student> selectStudentAll2(); }
-
修改Mapper配置文件
<?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.heng.dao.StudentMapper"> <!--基于查询结果嵌套处理--> <select id="selectStudentAll2" resultMap="teacherResult"> select s.id sid,s.name sname,t.name tname from student s,teacher t where s.tid = t.id; </select> <resultMap id="teacherResult" type="com.heng.pojo.Student"> <result property="id" column="sid"></result> <result property="name" column="sname"></result> <association property="teacher" javaType="com.heng.pojo.Teacher"> <result property="name" column="tname"></result> </association> </resultMap> </mapper>
-
编写测试类
@Test public void testSelectAllStudent2(){ SqlSession session = MybatisUtils.getSession(); StudentMapper mapper = session.getMapper(StudentMapper.class); List<Student> studentList = mapper.selectStudentAll2(); for (Student student : studentList) { System.out.println("Student = "+student.getName()+" Teacher = "+student.getTeacher().getName()); } session.close(); }
-
运行结果:
MyBatis一对多关联查询(多对多关系的拆分理解)
示例:查询一个老师的多个学生信息
按查询结果嵌套处理
- 思路:
-
从学生表和老师表中查出学生id,学生姓名,老师姓名
-
对查询出来的操作做结果集映射 1. 集合的话,使用collection! 2. JavaType和ofType都是用来指定对象类型的 3. JavaType是用来指定pojo中属性的类型 4. ofType指定的是映射到list集合属性中pojo的类型
-
修改TeacherMapper接口
package com.heng.dao; import com.heng.pojo.Teacher; import org.apache.ibatis.annotations.Param; /** * @Author: minster * @Date: 2021/10/29 15:28 */ @SuppressWarnings({"all"}) public interface TeacherMapper { public Teacher getTeacher(@Param("tid") int id); }
-
修改TeacherMapper配置文件
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.heng.dao.TeacherMapper"> <!--先把查询结果查询出来,再根据结果映射--> <select id="getTeacher" resultMap="TeacherStudent"> select s.id sid,s.name sname,t.id tid,t.name tname from student s,teacher t where s.tid=t.id and tid = #{tid} </select> <resultMap id="TeacherStudent" type="com.heng.pojo.Teacher"> <result property="id" column="tid"></result> <result property="name" column="tname"></result> <collection property="students" ofType="com.heng.pojo.Student"> <result property="id" column="sid"></result> <result property="name" column="sname"></result> <result property="tid" column="tid"></result> </collection> </resultMap> </mapper>
-
测试
@Test public void getCourse(){ SqlSession session = MybatisUtils.getSession(); StudentMapper mapper = session.getMapper(StudentMapper.class); List<Student> studentList = mapper.getStudentCourse(2); for (Student student : studentList) { System.out.println(student); System.out.println(student.getCourse().getName()); } session.close(); }
-
输出结果
按查询嵌套处理
-
编写接口方法
package com.heng.dao; import com.heng.pojo.Teacher; import org.apache.ibatis.annotations.Param; /** * @Author: minster * @Date: 2021/10/29 15:28 */ @SuppressWarnings({"all"}) public interface TeacherMapper { public Teacher getTeacher2(@Param("tid") int id); }
-
修改配置文件
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.heng.dao.TeacherMapper"> <select id="getTeacher2" resultMap="TeacherStudent2"> select * from teacher where id = #{tid} </select> <!--column是一对多的外键 , 写的是一的主键的列名--> <resultMap id="TeacherStudent2" type="com.heng.pojo.Teacher"> <result property="id" column="id"></result> <result property="name" column="name"></result> <collection property="students" javaType="ArrayList" ofType="com.heng.pojo.Student" select="getStudent" column="id"> </collection> </resultMap> <select id="getStudent" resultType="com.heng.pojo.Student"> select * from student where tid = #{tid} </select> </mapper>
-
测试
@Test public void getTeacher2(){ SqlSession session = MybatisUtils.getSession(); TeacherMapper mapper = session.getMapper(TeacherMapper.class); Teacher teacher = mapper.getTeacher2(1); System.out.println(teacher.getName()); System.out.println(teacher.getStudents()); session.close(); }
小结
-
关联-association
-
集合-collection
-
所以association是用于一对一和多对一,而collection是用于一对多的关系
- JavaType和ofType都是用来指定对象类型的
- JavaType是用来指定pojo中属性的类型
- ofType指定的是映射到list集合属性中pojo的类型。
注意说明:
-
保证SQL的可读性,尽量通俗易懂
-
根据实际要求,尽量编写性能更高的SQL语句
-
注意属性名和字段不一致的问题
-
注意一对多和多对一 中:字段和属性对应的问题
-
尽量使用Log4j,通过日志来查看自己的错误
动态SQL
什么是动态SQL:动态SQL指的是根据不同的查询条件,生成不同的Sql语句。
MyBatis的强大特性之一便是它的动态SQL。如果你有使用JDBC或其它类似框架的经验,你就能体会到根据不同条件拼接SQL语句的痛苦。例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。
利用SQL并非一件易事,但正是MyBatis提供了可以被用在任意SQL映射语句中的强大的动态SQL语言得以改进这种情形。
动态SQL元素和JSTL或基于类似XML的文本处理器相似。在MyBatis之前的版本中,有很多元素需要花时间了解。MyBatis3替换了之前的大部分元素,大大精简了元素种类,现在学习的元素种类比原来的一半还要少。
- if
- choose (when, otherwise)
- trim (where, set)
- foreach
——《MyBatis官方文档》
我们之前写的 SQL 语句都比较简单,如果有比较复杂的业务,我们需要写复杂的 SQL 语句,往往需要拼接,而拼接 SQL ,稍微不注意,由于引号,空格等缺失可能都会导致错误。
那么怎么去解决这个问题呢?这就要使用 mybatis 动态SQL,通过 if, choose, when, otherwise, trim, where, set, foreach等标签,可组合成非常灵活的SQL语句,从而在提高 SQL 语句的准确性的同时,也大大提高了开发人员的效率。
-
搭建环境:新建表Blog,并插入数据
CREATE TABLE `blog` ( `id` varchar(50) NOT NULL COMMENT '博客id', `title` varchar(100) NOT NULL COMMENT '博客标题', `author` varchar(30) NOT NULL COMMENT '博客作者', `create_time` datetime NOT NULL COMMENT '创建时间', `views` int(30) NOT NULL COMMENT '浏览量' ) ENGINE=InnoDB DEFAULT CHARSET=utf8
@Test public void addInitBlog(){ SqlSession session = MybatisUtils.getSession(); BlogMapper mapper = session.getMapper(BlogMapper.class); Blog blog = new Blog(); blog.setId(IDUtil.genId()); blog.setTitle("Mybatis如此简单"); blog.setAuthor("狂神说"); blog.setCreateTime(new Date()); blog.setViews(9999); mapper.addBlog(blog); blog.setId(IDUtil.genId()); blog.setTitle("Java如此简单"); mapper.addBlog(blog); blog.setId(IDUtil.genId()); blog.setTitle("Spring如此简单"); mapper.addBlog(blog); blog.setId(IDUtil.genId()); blog.setTitle("微服务如此简单"); mapper.addBlog(blog); session.close(); }
-
IF语句
需求:根据作者名字和博客名字来查询博客!如果作者名字为空,name只根据博客名字查询,反之,则根据作者名来查询
<!--需求1: 根据作者名字和博客名字来查询博客! 如果作者名字为空,那么只根据博客名字查询,反之,则根据作者名来查询 select * from blog where title = #{title} and author = #{author} --> <select id="queryBlogIf" parameterType="map" resultType="blog"> select * from blog where <if test="title != null"> title = #{title} </if> <if test="author != null"> and author = #{author} </if> </select>
这样写我们可以看到,如果 author 等于 null,那么查询语句为 select * from user where title=#{title},但是如果title为空呢?那么查询语句为 select * from user where and author=#{author},这是错误的 SQL 语句,如何解决呢?请看下面的 where 语句!
-
Where
<select id="queryBlogIf" parameterType="map" resultType="blog"> select * from blog <where> <if test="title != null"> title = #{title} </if> <if test="author != null"> and author = #{author} </if> </where> </select>
这个“where”标签会知道如果它包含的标签中有返回值的话,它就插入一个‘where’。此外,如果标签返回的内容是以AND 或OR 开头的,则它会剔除掉。
-
Set
同理,上面的对于查询 SQL 语句包含 where 关键字,如果在进行更新操作的时候,含有 set 关键词,我们怎么处理呢?
<!--注意set是用的逗号隔开--> <update id="updateBlog" parameterType="map"> update blog <set> <if test="title != null"> title = #{title}, </if> <if test="author != null"> author = #{author} </if> </set> where id = #{id}; </update>
-
choose语句
有时候,我们不想用到所有的查询条件,只想选择其中的一个,查询条件有一个满足即可,使用 choose 标签可以解决此类问题,类似于 Java 的 switch 语句
<select id="queryBlogChoose" parameterType="map" resultType="blog"> select * from blog <where> <choose> <when test="title != null"> title = #{title} </when> <when test="author != null"> and author = #{author} </when> <otherwise> and views = #{views} </otherwise> </choose> </where> </select>
注意:这里的When会由上到下按顺序执行,只要有一个成功执行,后面的都不会执行了!
@Test public void selectByChoose(){ SqlSession session = MybatisUtils.getSession(); BlogMapper mapper = session.getMapper(BlogMapper.class); Map map = new HashMap(); map.put("author","jack"); map.put("title","如何让富婆爱上我"); map.put("views","1000"); List<Blog> blogs = mapper.selectByChoose(map); for (Blog blog : blogs) { System.out.println(blog); } session.close(); }
所谓的动态SQL,本质上还是SQL语句,只是我们可以在SQL层面,去执行逻辑代码
-
Foreach
foreach
元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以及集合项迭代之间的分隔符。这个元素也不会错误地添加多余的分隔符,看它多智能!- collection:指定输入对象中的集合属性
- item:每次遍历生成的对象
- open:开始遍历时的拼接字符串
- close:结束时拼接的字符串
- separator:遍历对象之间需要拼接的字符串
示例代码
<select id="queryBlogByForEach" parameterType="map" resultType="com.heng.pojo.Blog"> select * from blog <where> <foreach collection="ids" item="id" open="and (" close=")" separator="or"> id = #{id} </foreach> </where> </select>
测试
@Test public void queryBlogByForEach(){ SqlSession session = MybatisUtils.getSession(); BlogMapper mapper = session.getMapper(BlogMapper.class); Map map = new HashMap(); List list = new ArrayList(); list.add(1); list.add(2); list.add(3); map.put("ids",list); List<Blog> blogs = mapper.queryBlogByForEach(map); for (Blog blog : blogs) { System.out.println(blog); } session.close(); }
SQL片段
有时候可能某个SQL语句我们用的特别多,为了增加代码的重用性,简化代码,我们需要将这些代码抽取出来,然后使用时直接调用。
提取SQL片段
<sql id="if-title-author">
<if test="title != null">
title = #{title}
</if>
<if test="author != null">
and author = #{author}
</if>
</sql>
引用SQL片段
<select id="queryBlogIf" parameterType="map" resultType="blog">
select * from blog
<where>
<!-- 引用 sql 片段,如果refid 指定的不在本文件中,那么需要在前面加上 namespace -->
<include refid="if-title-author"></include>
<!-- 在这里还可以引用其他的 sql 片段 -->
</where>
</select>
注意:
- 最好基于单表来定义SQL片段,提高片段的可重用性
- 在SQL片段中不要包括where
小结:
其实动态 sql 语句的编写往往就是一个拼接的问题,为了保证拼接准确,我们最好首先要写原生的 sql 语句出来,然后在通过 mybatis 动态sql 对照着改,防止出错。多在实践中使用才是熟练掌握它的技巧。
缓存
-
概括
什么是缓存[Cache]
- 存在内存中的临时数据。
- 将用户经常查询的数据放在缓存(内存)中,用户去查询数据就不用从硬盘上(关系型数据库/数据文件)查询,从缓存中查询,从而提高了查询效率,解决了高并发系统的性能问题。
为什么使用缓存
- 减少和数据库交互次数,减少系统开销
什么样的数据能使用缓存
- 经常查询并且不经常改变的数据
MyBatis缓存
- MyBatis包含一个非常强大的查询缓存特性,它可以非常方便地定制和配置缓存。缓存可以极大的提升查询效率。
- MyBatis系统中默认定义了两级缓存:一级缓存和二级缓存
- 默认情况下,只有一级缓存开启(SqlSession级别的缓存,也称为本地缓存)
- 二级缓存需要手动开启和配置,它是基于namespace级别的缓存。
- 为了提高扩展性,MyBatis定义了缓存接口Cache。我们可以通过实现Cache接口来自定义二级缓存
-
一级缓存
一级缓存也叫本地缓存
- 与数据库同一次会话期间查询到的数据会妨碍本地缓存中
- 以后如果需要获取相同的数据,直接从缓存中拿,没必要再去查询数据库
代码示例
-
在MyBatis中开启日志,方便测试结果
-
编写接口方法
//根据id查询用户 User queryUserById(@Param("id") int id);
-
接口对应的Mapper文件
<select id="queryUserById" resultType="user"> select * from user where id = #{id} </select>
-
测试
@Test public void testQueryUserById(){ SqlSession session = MybatisUtils.getSession(); UserMapper mapper = session.getMapper(UserMapper.class); User user = mapper.queryUserById(1); System.out.println(user); User user2 = mapper.queryUserById(1); System.out.println(user2); System.out.println(user==user2); session.close(); }
-
结果分析
一级缓存失效的四种情况
一级缓存是SqlSession级别的缓存,是一直开启的,我们关闭不了它;
一级缓存失效情况:程序没有使用到当前的一级缓存,还需要再向数据库发起一次查询请求!
-
SqlSession不同
@Test public void testQueryUserById(){ SqlSession session = MybatisUtils.getSession(); SqlSession session2 = MybatisUtils.getSession(); UserMapper mapper = session.getMapper(UserMapper.class); UserMapper mapper2 = session2.getMapper(UserMapper.class); User user = mapper.queryUserById(1); System.out.println(user); User user2 = mapper2.queryUserById(1); System.out.println(user2); System.out.println(user==user2); session.close(); session2.close(); }
观察结果:发现请求了两条SQL语句!
结论:每个SqlSession中的缓存相互独立
-
SqlSession相同,查询条件不同
@Test public void queryUserById(){ SqlSession session = MybatisUtils.getSession(); UserMapper mapper = session.getMapper(UserMapper.class); User user1 = mapper.queryUserById(1); System.out.println("user1:"+user1); User user2 = mapper.queryUserById(2); System.out.println("user2:"+user2); System.out.println(user1==user2); session.close(); }
观察结果:发现发送了两条SQL语句!很正常的理解
结论:当前缓存中,不存在这个数据
-
SqlSession相同,两次查询之间执行了增、删、改操作
@Test public void testQueryUserById(){ SqlSession session = MybatisUtils.getSession(); UserMapper mapper = session.getMapper(UserMapper.class); User user = mapper.queryUserById(1); System.out.println(user); HashMap map = new HashMap(); map.put("name","kuangshen"); map.put("id",4); mapper.updateUser(map); User user2 = mapper.queryUserById(1); System.out.println(user2); System.out.println(user==user2); session.close(); }
观察结果:查询在中间执行了增删改操作后,从新执行了(增删改回刷新缓存!)
结论:因为增删改操作可能会对当前数据产生影响
-
SqlSession相同,手动清除了一级缓存
@Test public void queryUserById(){ SqlSession session = MybatisUtils.getSession(); UserMapper mapper = session.getMapper(UserMapper.class); User user1 = mapper.queryUserById(1); System.out.println("user1:"+user1); session.clearCache(); User user2 = mapper.queryUserById(1); System.out.println("user2:"+user2); System.out.println(user1==user2); session.close(); }
一级缓存就是一个map
-
二级缓存
-
二级缓存也叫全局缓存,一级缓存作用域太低了,所以诞生了二级缓存
-
基于namespace级别的缓存;一个名称空间,对应一个二级缓存
-
工作机制
- 一个会话查询了一条数据,这个会话就会被放在当前会话的一级缓存中;
- 如果当前会话关闭了,这个会话对应的一级缓存就没了;但是我们想要的是,会话关闭了,一级缓存中的数据就会被保存到二级缓存中;
- 新的会话查询信息,就可以从二级缓存中获取内容
- 不同的mapper查出的数据会仿真自己对应的缓存(map)中
使用步骤
-
开启全局缓存
<setting name="cacheEnabled" value="true"/>
-
去每个mapper.xml中配置使用二级缓存,这个配置非常简单;
<cache></cache> <!--等价于--> <cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/> <!-- FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。-->
-
测试
- 所有的实体类需要先实现序列化接口!
@Test public void queryUserById(){ SqlSession session1 = MybatisUtils.getSession(); UserMapper mapper1 = session1.getMapper(UserMapper.class); SqlSession session2 = MybatisUtils.getSession(); UserMapper mapper2 = session2.getMapper(UserMapper.class); User user1 = mapper1.queryUserById(1); System.out.println("user1:"+user1); System.out.println("从一级缓存读取数据"); User user2 = mapper1.queryUserById(1); System.out.println("user2:"+user2); session1.close(); System.out.println("====Session1关闭!===="); System.out.println("从二级缓存读取数据"); User user3= mapper2.queryUserById(1); System.out.println("user3:"+user3); System.out.println(user1==user2&&user2==user3); session2.close(); }
输出结果
结论:
- 只要开启了二级缓存,我们在同一个Mapper中的查询,可以在二级缓存中拿到数据
- 查出的数据都会被默认先放在一级缓存中
- 只有会话提交或者关闭以后,一级缓存中的数据才会转到二级缓存中
-
-
缓存原理图