MyBatis学习笔记
1 简介
1.1 什么是MyBatis
- 持久层框架
- 定制化SQL、存储过程和高级映射
- 使用简单的XML或注解配置,映射原生类型、接口和POJO为数据库中的记录
1.2 持久化
数据持久化
- 持久化就是将程序的数据在持久状态和瞬时状态转化的过程
- 数据库、IO文件持久化
1.3 持久层
Dao层
- 完成持久化工作的代码块
1.4 为什么需要MyBatis
-
原生JDBC的缺点
原生的JDBC操作复杂,需要获取连接、获取PreparedStatement、写入Sql、预编译参数、执行Sql、封装结果、释放连接等一系列操作,比较麻烦
并且JDBC中SQL语句是硬编码在程序中的,数据库层和Java编码耦合,维护修改Sql语句时比较麻烦 -
Hibernate框架的缺点
无法定制SQL,是一个全映射框架,做部分字段映射很难
而MyBatis将重要的步骤(即编写SQL语句)抽取出来,可以人工定制,其他步骤实现自动化
编写SQL语句是通过配置文件实现,好维护,能解决数据库的优化问题
MyBatis底层就是对原生JDBC的一个简单封装
既将Java编码与SQL语句分离开来,还不会失去自动化功能,是一个半自动的持久化框架
2 第一个MyBatis程序
搭建环境-->导入MyBatis-->编写代码-->测试
2.1 搭建环境
- 创建数据库
- 创建项目
- 导入JDBC驱动,MyBatis包
- 配置mybatis的核心xml文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<!--每个Mapper.xml都要在核心配置文件中注册-->
<mappers>
<mapper resource="com/hjc/dao/UserMapper.xml"/>
</mappers>
</configuration>
- 从xml文件中构建SqlSessionFactory,我们把构建SqlSessionFactory的方法放在工具类的静态方法内
public class MyBatisUtils {
private static SqlSessionFactory sqlSessionFactory;
static {
try {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
public static sqlSession getSqlSession() {
return sqlSessionFactory.openSession();
}
}
2.2 编写代码
- 编写实体类(pojo)
- 编写Dao接口
public interface UserDao {
List<User> getUserList();
}
- 编写UserMapper.xml文件(接口实现类由原来的UserDaoImpl转变为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">
<!--namespace = 绑定一个对应的Dao/Mapper接口-->
<mapper namespace="com.hjc.dao.UserDao">
<!--select查询语句-->
<select id="getUserList" resultType="com.hjc.pojo.User">
select * from user;
</select>
</mapper>
-
在核心配置文件中注册UserMapper.xml
<!--每个Mapper.xml都要在核心配置文件中注册--> <mappers> <mapper resource="com/hjc/dao/UserMapper.xml"/> </mappers>
- 在默认的Maven配置下,UserMapper.xml文件应该放在resources文件夹下,也就是resources/com/hjc/dao/UserMapper.xml路径
- 要想把UserDao和UserMapper.xml文件放在一起,需要更改Maven构建配置
- 如果不这样做,在构建时,Mapper文件无法被打包到target文件夹中
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
2.3 测试代码
public class UserDaoTest {
@Test
public void testGetUserList() {
//获得SqlSession对象
SqlSession sqlSession = MyBatisUtils.getSqlSession();
//方式一:getMapper
UserDao userDao = sqlSession.getMapper(UserDao.class);
List<User> userList = userDao.getUserList();
//方式二:不建议使用
//List<User> userList = sqlSession.selectList("com.hjc.dao.UserDao.getUserList");
for (User user : userList)
System.out.println(user);
//关闭SqlSession
sqlSession.close();
}
}
3 CRUD
3.1 namespace
namespace中的包名要和Dao/Mapper接口的包名一致
3.2 select
查询语句
- id:就是对应的namespace中的方法名
- resultType:sql语句执行的返回类型
- parameterType:sql语句的参数类型
- 编写接口
//根据id查询用户
User getUserById(int id);
- 编写对应Mapper.xml文件中的sql语句
<select id="getUserById" parameterType="Integer" resultType="com.hjc.pojo.User">
select * from user where id = #{id}
</selects>
- 测试
@Test
public void testGetUserById() {
SqlSession sqlSession = MyBatisUtils.getSqlSession();
UserDao userDao = sqlSession.getMapper(UserDao.class);
User user = userDao.getUserById(1);
System.out.println(user);
sqlSession.close();
}
3.3 insert
- 编写接口
//插入用户
void addUser(User user);
- 编写对应Mapper.xml文件中的sql语句
<insert id="addUser" parameterType="com.hjc.pojo.User">
insert into user (id, name, pwd) values (#{id}, #{name}, #{pwd})
</insert>
- 测试
@Test
public void testAddUser() {
SqlSession sqlSession = MyBatisUtils.getSqlSession();
UserDao userDao = sqlSession.getMapper(UserDao.class);
userDao.addUser(new User(5, "test", 123456));
//需要提交事务
sqlSession.commit();
sqlSession.close();
}
3.4 update
- 编写接口
//更新用户
void updateUse(User user);
- 编写对应Mapper.xml文件中的sql语句
<update id="updateUser" parameterType="com.hjc.pojp.User">
update user set name = #{name}, pwd = #{pwd} where id = #{id}
</update>
- 测试
@Test
public void testUpdateUser() {
SqlSession sqlSession = MyBatisUtils.getSqlSession();
UserDao userDao = sqlSession.getMapper(UserDao.class);
userDao.updateUser(new User(5, "test", 123456));
//需要提交事务
sqlSession.commit();
sqlSession.close();
}
3.5 delete
- 编写接口
//删除用户
void deleteUserById(int id);
- 编写对应Mapper.xml文件中的sql语句
<delete id="deleteUserById" parameterType="Integer">
delete from user where id = #{id}
</delete>
- 测试
@Test
public void testDeleteUserById() {
SqlSession sqlSession = MyBatisUtils.getSqlSession();
UserDao userDao = sqlSession.getMapper(UserDao.class);
userDao.deleteUserById(5);
//需要提交事务
sqlSession.commit();
sqlSession.close();
}
注意点:MyBatis默认不会自动提交事务,需要手动提交事务
3.6 Map
如果实体类或数据库中表的字段或参数过多,我们应当考虑使用Map作为sql的参数
void addUserByMap(Map<String, Object> map);
<insert id="addUserByMap" parameterType="Map">
insert into user (id, pwd) values (#{userId}, #{userPwd})
</insert>
@Test
public void testAddUserByMap() {
SqlSession sqlSession = MyBatisUtils.getSqlSession();
UserDao userDao = sqlSession.getMapper(UserDao.class);
Map<String, Object> map = new HashMap<>();
map.put("userId", 6);
map.put("userPwd", "123456");
userDao.addUserByMap(map);
//需要提交事务
sqlSession.commit();
sqlSession.close();
}
- Map传递参数,直接在sql中取出key即可
- 对象传递参数,直接在sql中取对象的属性即可
- 只有一个基本类型参数的情况下,可以直接在sql中取到
- 多个参数的情况,用Map,或者注解
3.7 模糊查询
List<User> getUserLike(String name);
<select id="getUserLike" parameterType="String" resultType="com.hjc.pojo.User">
select * from user where name like #{name}
</select>
@Test
public void testGetUserLike() {
SqlSession sqlSession = MyBatisUtils.getSqlSession();
UserDao userDao = sqlSession.getMapper(UserDao.class);
List<User> userList = userDao.getUserLike("%test%");
for (User user : userList)
System.out.println(user);
sqlSession.close();
}
- 在Java代码执行的时候,传递通配符"% %"
List<User> userList = userDao.getUserLike("%test%");
- 在sql拼接中使用通配符"% %"
select * from user where name like "%"#{name}"%"
3.8 总结参数传递的多种情况
在编写mapper映射文件的时候,parameterType属性其实是可有可无的,也就是说,不写这个属性,一般也会成功
一般在SQL语句中获取参数值使用#{}来获取,也可以使用${}来获取
{}是参数预编译的方式,参数的位置都是用?来替代,参数是后来预编译设置进去的
${}不是参数预编译的方式,而是直接和SQL语句进行拼串
两者相比,#{}更加安全,不存在SQL注入的问题,而${}不安全,会有SQL注入的问题
但是,如果数据库的表名也要作为参数传入的话,就只能使用${}获取,因为表名不支持参数预编译
- 传入单个参数
当方法传入单个参数的时候,我们在SQL语句中获得参数是用#{参数名}
来获取的,实际上,大括号内的参数名可以随便取
- 传入多个参数
当方法传入多个参数的时候,如果我们只是在SQL语句中使用#{参数名1}
、#{参数名2}
来获取多个参数,这样是不能成功的
因为传入多个参数,MyBatis会自动将这些参数封装在一个map中,map中的key是参数的索引,那么,我们就不能在大括号内写入参数名,
而是使用#{0}
、#{1}
或者#{param1}
、#{param2}
来获取第一个参数值和第二个参数值
另外,如果一定要用参数名来获取传入多个参数情况下的参数值,我们推荐在dao层的接口方法的参数前面加上@Param
注解,
比如int getUserByNameAndPassword(@Param("name") String name, @Param("pwd") String pwd)
,这样的话,我们在写SQL语句的时候就能直接使用#{参数名}
来获取参数值
<select id="getUserByIdAndName" resultType="com.hjc.pojo.User">
select * from user where name=#{name} and pwd=#{pwd}
</select>
- 传入pojo
如果方法的参数为pojo类,那么在SQL语句中直接使用#{pojo的属性名}
来获取对应属性的值
- 传入map
我们可以手动将传入的多个参数封装进一个map中,那么在SQL语句中可以使用#{key}
来获取对应的值,key为map的键
3.9 查询返回list
如果需要查询结果返回list,那么在写mapper映射文件的时候,resultType属性写入list集合内部元素的类型,MyBatis会自动将查询结果封装进list中
3.10 查询返回map
- 如果查询返回单条记录,要封装在map中返回,那么列名作为key,值作为value
<select id="getUserByIdReturnMap" resultType="map">
select * from user where id=#{id}
</select>
执行这个配置,返回的map是key为列名或者属性名,value为对应的值
- 如果查询返回多条记录,封装到map中返回,那么主键作为key,单条记录对象作为value
<select id="getUsersReturnMap" resultType="com.hjc.pojo.User">
select * from user
</select>
此外,在接口方法上还要指明作为key的属性
@MapKey("id")
Map<Integer, User> getUsersReturnMap();
注意点:如果返回多条记录封装到map中,那么返回类型resultType要写封装到map中value的类型,和查询返回list相同,另外,在接口方法上还要指定一个属性作为map的key
4 配置解析
4.1 核心配置文件
- mybatis-config.xml
- 配置文件会影响MyBatis行为的设置和属性信息
configuration(配置)
properties(属性)
settings(设置)
typeAliases(类型别名)
typeHandlers(类型处理器)
objectFactory(对象工厂)
plugins(插件)
environments(环境配置)
environment(环境变量)
transactionManager(事务管理器)
dataSource(数据源)
databaseIdProvider(数据库厂商标识,用来数据库移植,一般不用)
mappers(映射器)
4.2 环境配置(environments)
environments标签中定义了配置的环境,包括事务管理器和数据源
MyBatis可以配置成适应多种环境,但每个SqlSessionFactory实例只能选择一种环境
要切换其他环境,只需要将environments标签的default属性换成其他environment标签的id属性
MyBatis默认的事务管理器就是JDBC,连接池POOLED
在和Spring进行整合的时候,事务管理器和数据源交给Spring来做
4.3 属性(properties)
可以通过properties属性来实现引用外部配置文件,属性可以在Java属性文件(properties文件)中配置,也可以通过properties元素的子元素来传递
- 编写外部配置文件db.properties
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/test_demo?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
username=root
password=123456
- 在核心配置文件中引入properties文件
<properties resource="db.properties"/>
- 也可以在核心配置文件中增加属性
<properties resource="db.properties">
<property name="username" value="root"/>
<property name="password" value="111111"/>
</properties>
- 如果两个地方都配置了同一字段,优先使用外部文件配置的信息
4.4 类型别名(typeAliases)
在mappers的配置文件中,一般resultType属性要求我们写全限定类名,我们可以使用类型别名定义一个别名,那么在写resultType的时候就不需要写全限定类名了
- 类型别名是为Java类型设置一个短的名字,为了减少全限定类名的冗余,别名不区分大小写
<typeAliases>
<typeAlias type="com.hjc.pojo.User" alias="User"/>
</typeAliases>
- 指定一个包名,MyBatis会在包名下面搜索需要的JavaBean,默认别名就是这个类的类名,不区分大小写,也可以配合注解@Alias使用
<typeAliases>
<package name="com.hjc.pojo"/>
</typeAliases>
- 在实体类比较少的时候,使用第一种方式;实体类比较多,建议第二种
另外,MyBatis已经为常见的Java类型内建了相应的类型别名,比如int -> _int,String -> string,别名不区分大小写,我们在自己定义别名的时候注意不要与这些别名冲突
4.5 设置(settings)
这项属性可以改变MyBatis的运行时行为
- cacheEnabled:全局开启或关闭所有映射器配置文件中已配置的任何缓存
- lazyLoadingEnabled:开启或关闭延迟加载
- logImpl:指定MyBatis所有日志的具体实现
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="logImpl" value="LOG4J"/>
</settings>
4.6 映射器(mappers)
注册绑定Mapper文件
- 方式一
<mappers>
<mapper resource="com/hjc/dao/UserMapper.xml"/>
</mappers>
- 方式二:使用class文件绑定注册
<mappers>
<mapper class="com.hjc.dao.UserMapper"/>
</mappers>
注意点
使用此方法必须
- 接口和其Mapper配置文件必须同名
- 接口和其Mapper配置文件必须在同一个包下
- 方式三:使用扫描包进行绑定注册
<mappers>
<mapper package="com.hjc.dao"/>
</mappers>
注意点
- 接口和其Mapper配置文件必须同名
- 接口和其Mapper配置文件必须在同一个包下
或者 - Mapper配置文件的路径和接口的包路径一致
4.7 其他配置
- typeHandlers(类型处理器)
- objectFactory(对象工厂)
- plugins插件
- mybatis-generator-core
- mybatis-plus
- 通用mapper
4.8 生命周期和作用域
生命周期和作用域至关重要,错误使用会导致并发问题
SqlSessionFactoryBuilder
- 一旦创建了SqlSessionFactory,就不再需要SqlSessionFactoryBuilder了
- 局部变量
SqlSessionFactory
- 相当于数据库连接池
- 一旦被创建就在应用的运行期间一直存在,没有理由丢弃它或重新创建另一个实例
- SqlSessionFactory的最佳作用域是应用作用域
- 使用单例模式或者静态单例模式
SqlSesison
- 相当于连接到连接池的一个请求
- 不是线程安全的,不能被共享
- SqlSession的最佳作用域是请求或方法作用域
- 用完之后要立即关闭,否则资源被占用
图中的每个Mapper代表一个具体的业务
5 实体类属性名和数据库字段名不一致问题
MyBatis自动封装结果集,前提是数据库列名和JavaBean属性名一一对应(不区分大小写),如果不一一对应,可以开启驼峰命名规则(数据库aaa_bbb -> 属性aaaBbb),可以起别名,还可以自定义结果集映射
如果User类中的属性名依次为id,name和password,而数据库中的列名以此为id,name和pwd,不一一对应
解决方法:
5.1 起别名
原来是
<select id="getUserById" parameterType="int" resultType="com.hjc.pojo.User">
select * from user where id = #{id}
</select>
改为
<select id="getUserById" parameterType="int" resultType="com.hjc.pojo.User">
select id, name, pwd password from user where id = #{id}
</select>
5.2 使用resultMap
结果集映射
<!--结果集映射-->
<resultMap id="UserMap" type="User">
<!--id为主键列,result为非主键列-->
<!--column为数据库字段,property为实体类属性-->
<!--<id column="id" property="id"/>-->
<!--<result column="name" property="name"/>-->
<result column="pwd" property="password"/>
</resultMap>
<select id="getUserById" parameterType="int" resultMap="UserMap">
select * from user where id = #{id}
</select>
对于简单的语句不需要配置显示的结果集映射,而对于复杂一点的语句需要描述它们的关系
6 获取自增主键和非自增主键的值
6.1 获取自增主键的值
当数据库的主键是自增的情况下,插入一条用户信息是不需要给id属性赋值的,那么如何才能获取到刚插入的用户的主键id呢
我们可以使用useGeneratedKeys="true"
来获取自增主键的值
比如
<insert id="addUser" useGeneratedKeys="true" keyProperty="id">
insert into user (name, pwd) values (#{name}, #{pwd})
</insert>
useGeneratedKeys="true"
表示需要获取数据库自增的主键,keyProperty="id"
表示将需要获取的自增主键封装给id属性
User user = new User(null, "test", 123456);
userDao.addUser(user);
SqlSession.commit();
System.out.println(user.getId());
这样就能获取到新插入user对象的id
6.2 获取非自增主键的值
如果数据库的主键不是自增的,那么在插入一条用户信息的时候就要带上主键值,但是,我们可能不知道数据库中的主键值已经用了多少,也就是说,有可能插入的主键已经在数据库中存在了,会产生冲突
我们可以使用selectKey标签在核心SQL语句运行之前先运行一个查询主键的SQL语句,再将查到的主键值赋值给JavaBean的对应属性
比如
<insert id="addUser">
<selectKey order="BEFORE" resultType="integer" keyProperty="id">
select max(id) + 1 from user
</selectKey>
insert into user (id, name, pwd) values (#{id}, #{name}, #{pwd})
</insert>
其中,order="BEFORE"
表示在核心SQL语句之前运行,keyProperty="id"
表示将查询到的值封装给id属性
User user = new User(null, "test", 123456);
userDao.addUser(user);
SqlSession.commit();
System.out.println(user.getId());
这样就能在数据库没有设定自增主键的情况下,安全地插入数据
7 日志
7.1 日志工厂
如果数据库操作出现异常,需要排错,那么日志就是最好的助手
以前可以使用sout打印,debug,现在有日志工厂
MyBatis使用内置的日志工厂提供日志功能,可以设置具体日志实现
STDOUT_LOGGING
在mybatis核心配置文件中配置日志
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
可以直接使用
7.2 LOG4J
-
LOG4J可以控制日志信息输送的目的地是控制台,文件,GUI组件
-
可以控制每一条日志的输出格式
-
可以定义每一条日志信息的级别
-
LOG4J在配置完后不能直接使用
- 先导入LOG4J的包
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
- 在resource文件夹下创建log4j.properties文件
#将等级为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/hjc.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
- 配置log4j为日志实现
<settings>
<setting name="logImpl" value="LOG4J"/>
</settings>
- log4j的使用,测试运行
简单使用
- 在要使用log4j的类中,导入包 import org.apache.log4j.Logger;
- 日志对象,参数为当前类的class
private static Logger logger = Logger.getLogger(UserDaoTest.class);
- 日志级别
logger.info("info: 开始测试");
logger.debug("debug: 开始debug");
logger.error("error: 出现了错误");
8 分页
如果一次查询得到的数据量太大,会产生资源浪费的后果,所以要用到分页
- 减少数据的处理量
8.1 使用limit分页
语法:select * from user limit startIndex, pageSize;
例子:select * from user limit 1, 3;
MyBatis中实现分页
- 编写接口,传输参数为map,map内存放start Index和pageSize
//分页查询
List<User> getUserByLimit(Map<String, Integer> map);
- 编写对应的Mapper.xml文件
<select id="getUserByLimit" parameterType="map" resultType="com.hjc.pojo.User">
select * from user limit #{startIndex}, #{pageSize}
</select>
- 测试
@Test
public void testGetUserByLimit() {
SqlSession sqlSession = MyBatisUtils.getSqlSession();
UserDao userDao = sqlSession.getMapper(UserDao.class);
Map<String, Integer> map = new HashMap<>();
map.put("startIndex", 1);
map.put("pageSize", 2);
List<User> userList = userDao.getUserByLimit(map);
for (User user : userList)
System.out.println(user);
sqlSession.close();
}
8.2 使用RowBounds分页
不在sql中使用limit,使用面向对象思想
- 编写接口
List<user> getUserByRowBounds();
- 编写对应的Mapper.xml文件
<select id="getUserByRowBounds" resultType="com.hjc.pojo.User">
select * from user
</select>
- 测试
@Test
public void testGetUserByRowBounds() {
SqlSession sqlSession = MyBatisUtils.getSqlSession();
RowBounds rowBounds = new RowBounds(1, 2);
//不使用Mapper来查询,使用以前的selectList来查询
List<User> userList =
sqlSession.selectList("com.hjc.dao.UserDao.getUserByRowBounds", null, rowBounds);
for (User user : userList)
System.out.println(user);
sqlSession.close();
}
8.3 使用分页插件
MyBatis分页插件PageHelper,了解
9 使用注解
9.1 面向接口编程
在开发中,会选择面向接口编程
- 根本原因是解耦,可拓展,提高复用
- 分层开发中,上层不需要管具体的实现
- 规范性更好
关于接口的理解
- 从深层次的理解,接口是定义(规范、约束)与实现的分离
- 接口的本身反映了系统设计人员对系统的抽象理解
- 接口有两类
- 第一类是对一个个体的抽象,对应为一个抽象体(abstract class)
- 第二类是对一个个体某一方面的抽象,即形成一个抽象面(interface)
- 一个个体可能有多个抽象面
9.2 注解
- 注解在接口内方法名上标注
@Select("select * from user")
List<User> getUsers();
- 需要在核心配置文件中绑定接口
<mappers>
<mapper class="com.hjc.dao.UserDao"/>
</mappers>
- 测试
- 本质:反射机制实现
- 底层:动态代理
9.3 注解实现CRUD
可以在工具类内实现自动提交事务
public static sqlSession getSqlSession() {
return sqlSessionFactory.openSession(true);
}
- 编写接口
public interface UserDao {
@Select("select * from user where id = #{id}")
User getUserById(@param("id") int id);
@Insert("insert into user (id, name, pwd) values (#{id}, #{name}, #{pwd})")
int addUser(User user);
@Update("update user set name = #{name}, pwd = #{pwd} where id = #{id}")
int updateUser(User user);
@Delete("delete from user where id = #{uid}")
int deleteUser(@Param("uid") int id);
}
- 绑定接口
<mappers>
<mapper class="com.hjc.dao.UserDao"/>
</mappers>
- 测试
关于@Param注解
- 基本类型的参数或者String类型,需要加上
- 引用类型不需要加
- 如果只有一个基本类型参数的话,可以忽略
- 在sql语句中使用的参数就是注解内设定的属性名
10 MyBatis执行流程
11 Lombok
lombok是一个插件,通过注解来消除业务中冗长的代码,尤其对于POJO
使用步骤
- 在IDEA中安装Lombok插件
- 在项目中导入Lombok包
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>
- 在实体类上加注解
- @Data
- @AllArgsConstructor
- @NoArgsConstructor
- ……
12 多对一关系处理
mysql中多对一查询方式
- 子查询
- 联表查询
假设多个学生对应一个老师,那么先分别写对应实体类
@Data
public Student implements Serializable {
private int id;
private String name;
//学生需要关联一个老师
private Teacher teacher;
}
@Data
public Teacher implements Serializable {
private int id;
private String name;
}
要查出所有学生以及对应的老师,有两种方法
12.1 按照查询嵌套处理
思路:
- 查询所有的学生信息
- 根据查询出的学生tid,查询对应的老师,相当于子查询
<select id="getStudent" resultMap="StudentPlusTeacher">
select * from student
</select>
<resultMap id="StudentPlusTeacher" type="Student">
<!--主键-->
<id property="id" column="id"/>
<result property="name" column="name"/>
<!--复杂的属性需要单独处理,对象用association,集合用collection
javaType表示属性的类型-->
<association property="teacher" column="tid" javaType="Teacher" select="getTeacher"/>
</resultMap>
<select id="getTeacher" parameterType="int" resultType="Teacher">
select * form teacher where id = #{id}
</select>
注:在核心配置文件中已经配置了别名,所以可以省去实体类的包路径
12.2 按照结果嵌套处理
使用association标签,表示联合了一个对象,其中javaType属性是指对象的类型
<select id="getStudent" resultMap="StudentPlusTeacher">
select s.id as sid, s.name as sname, t.name as tname
from student s, teacher t
where s.tid = t.id
</select>
<resultMap id="StudentPlusTeacher" type="Student">
<id property="id" column="sid"/>
<result property="name" column="sname"/>
<association property="teacher" javaType="Teacher">
<result property="name" column="tname"/>
</association>
</resultMap>
因为在查询语句中返回的只有teacher.name,没有teacher.id,所以不用在association内写上teacher.id的映射关系
另外,在写resultMap的时候也可以不用association封装,可以使用级联属性的方式封装
<resultMap id="StudentPlusTeacher" type="Student">
<id property="id" column="sid"/>
<result property="name" column="sname"/>
<result property="teacher.name" column="tname"/>
</resultMap>
13 一对多关系处理
假设一个老师有多个学生,那么先分别写对应的实体类
@Data
public class Student implements Serializable {
private int id;
private String name;
private int tid;
}
@Data
public class Teacher implements Serializable {
private int id;
private String name;
//一个老师有多个学生
private List<Student> students;
}
要查出指定老师及其对应的所有学生
13.1 按照结果嵌套处理
使用collection标签,表示联合了一个集合,其中ofType属性表示集合内元素的类型
<select id="getTeacher" parameterType="int" resultMap="TeacherPlusStudent">
select s.id as sid, s.name as sname, t.name as tname, t.id as tid
from student s, teacher t
where s.tid = t.id and t.id = #{tid}
</select>
<resultMap id="TeacherPlusStudent" type="Teacher">
<id property="id" column="tid"/>
<result property="name" column="tname"/>
<!--复杂的属性需要单独处理,对象用association,集合用collection
ofType表示集合中的泛型信息-->
<collection proerty="students" ofType="Student">
<id property="id" column="sid"/>
<result property="name" column="sname"/>
<result property="tid" column="tid"/>
</collection>
</resultMap>
13.2 按照查询嵌套处理
<select id="getTeacher" parameterType="int" resultMap="TeacherPlusStudent">
select * from teacher where id = #{tid}
</select>
<resultMap id="TeacherPlusStudent" type="Teacher">
<collection property="students" javaType="ArrayList" ofType="Student" select="getStudentByTeacherId" column="id"/>
</resultMap>
<select id="getStudentByTeacherId" parameter="int" resultType="Student">
select * from student where tid = #{tid}
</select>
13.3 总结
- 关联:association,表示多对一
- 集合:collection,表示一对多
- javaType:用来指定实体类中属性的类型
- ofType:用来指定集合中的泛型信息,即集合内的pojo类型
14 动态SQL
动态SQL是指根据不同的条件生成不同的SQL语句,之前的项目写sql时,有时候会根据不同的条件拼接sql语句,现在可以使用动态SQL来实现
参考mybatis官方文档中对于动态SQL的解释
14.1 if
<select id="findActiveBlogWithTitleLike" resultType="Blog">
SELECT * FROM BLOG
WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
</select>
14.2 choose、when、otherwise
相当于switch语句
<select id="findActiveBlogLike" resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<choose>
<when test="title != null">
AND title like #{title}
</when>
<when test="author != null and author.name != null">
AND author_name like #{author.name}
</when>
<otherwise>
AND featured = 1
</otherwise>
</choose>
</select>
14.3 where、set、trim
上面几个例子,sql语句中WHERE关键字后面都跟了默认的条件state = ‘ACTIVE’,如果把这条语句删了或者改为动态SQL,那么整个SQL语句会出现问题,比如
<select id="findActiveBlogLike" resultType="Blog">
SELECT * FROM BLOG
WHERE
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>
如果if条件一个都不满足,那么sql变为
SELECT * FROM BLOG WHERE
很明显,这条sql语句是错误的
如果第一个id条件不满足,那么sql变为
SELECT * FROM BLOG WHERE AND title like #{title} ……
很明显,这条sql语句也是错误的
那么,MyBatis提供了where标签,可以解决这些问题。我们把上面的例子用where标签修改
<select id="findActiveBlogLike" resultType="Blog">
SELECT * FROM BLOG
<where>
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</where>
</select>
<select id="findActiveBlogLike" resultType="Blog">
SELECT * FROM BLOG
<where>
<choose>
<when test="title != null">
AND title like #{title}
</when>
<when test="author != null and author.name != null">
AND author_name like #{author.name}
</when>
<otherwise>
AND featured = 1
</otherwise>
</choose>
</where>
</select>
where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 AND
或 OR
,where 元素也会将它们去除
另外,set标签类似于where标签,会动态地设置SET关键字,同时也会删掉无关的逗号
<update id="updateAuthorIfNecessary">
update Author
<set>
<if test="username != null">
username = #{username},
</if>
<if test="password != null">
password = #{password},
</if>
<if test="email != null">
email = #{email},
</if>
<if test="bio != null">
bio = #{bio}
</if>
</set>
where id = #{id}
</update>
set会自动在行首插入SET关键字,并根据传进来的参数是否为空决定sql语句的结构,比如是否需要删除行尾的逗号
trim标签与set标签等价
14.4 SQL片段
SQL片段:将sql语句的部分提取出来,方便复用
<sql id="test">
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</sql>
<select id="findActiveBlogLike" resultType="Blog">
SELECT * FROM BLOG
<where>
<include refid="test"/>
</where>
</select>
注意:
- 最好基于单表来定义SQL片段
- SQL片段内不要存在where标签
14.5 foreach
foreach标签可以在dao层接口的方法传入参数为list集合的情况下,对list中数据进行遍历来组成SQL语句
比如,遇到IN语句的时候,可以使用foreach标签对集合进行遍历
<select id="selectPostIn" parameterType="list" resultType="domain.blog.Post">
SELECT *
FROM POST P
WHERE ID in
<!--传入参数为list集合-->
<foreach item="item" index="index" collection="list" open="(" separator="," close=")">
#{item}
</foreach>
</select>
可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数传递给 foreach。当使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素。当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值
另外,foreach也可以用在批量插入数据的情况下,比如我们要插入多条数据,那么方法传入的参数是一个list,在编写SQL语句的时候就可以使用foreach标签对list集合进行遍历来作为插入的VALUES。我们可以对比批量插入的多种方法,此方法的执行性能最好
总结
动态SQL就是在拼接SQL语句,只要保证SQL的正确性,就按照SQL的格式排列组合。先写出完整的SQL语句,再对应修改称为动态SQL实现通用
15 缓存
15.1 简介
- 什么是缓存
- 存在内存中的临时数据
- 将用户经常查询的数据放在缓存中,下次查询时不用再从数据库中获取,而是从缓存中获取,从而提高查询效率,解决高并发系统的性能问题
- 为什么使用缓存
- 减少和数据库的交互次数,减少系统开销,提高系统效率
- 什么数据能使用缓存
- 经常查询且不经常改变的数据
15.2 MyBatis缓存
- MyBatis包含一个强大的查询缓存特性,可以非常方便地定制和配置缓存
- MyBatis默认定义了两级缓存:一级缓存和二级缓存
- 默认情况下,只有一级缓存开启(SqlSession级别的缓存,也称为本地缓存,线程级别的缓存)
- 二级缓存需要手动开启和配置,是基于namespace级别的缓存,全局缓存
- MyBatis定义了缓存接口Cache,可以通过实现Cache接口来自定义二级缓存
15.3 缓存原理
15.4 一级缓存
- 一级缓存也叫本地缓存:SqlSession
- 相当于一个map
- 和数据库同一次会话(同一次SqlSession)期间查询到的数据会放在本地缓存中
- 以后如果要获取相同的数据,可以直接从缓存中取,不需要从数据库中查询
测试
@Test
public void test() {
SqlSession sqlSession = MyBatisUtils.getSqlSession();
UserDao userDao = sqlSession.getMapper(UserDao.class);
User user1 = userDao.getUserById(1);
User user2 = userDao.getUserById(1);
System.out.println(user1 == user2);
sqlSession.close();
}
根据日志,我们可以看到sql只执行了一次,user1和user2是同一个
- 缓存失效的情况
- 不同的SqlSession,使用不同的一级缓存
- 同一个方法,查询不同的数据
- 在同一个SqlSession期间,执行任何一次增删改操作会改变原来的数据,缓存会刷新
- 使用不同的Mapper进行查询
- 手动清除缓存
15.5 二级缓存
- 二级缓存也叫全局缓存,作用域比一级缓存要大
- 基于namespace级别的缓存,一个命名空间对应一个二级缓存
- 工作机制
- 一次会话查询一条数据,这个数据会被放在当前会话的一级缓存中
- 如果当前会话关闭了,那么对应的一级缓存就没了。但是二级缓存开启后,会话关闭之后,一级缓存中的数据会保存到二级缓存中
- 新的会话查询数据,就可以从二级缓存中获取数据
- 不同的Mapper查出的数据会放在对应的缓存中
步骤
- 在核心配置文件中开启全局缓存
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
- 在要使用二级缓存的Mapper.xml中开启二级缓存,可以自定义缓存属性
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
- 测试
@Test
public void test() {
SqlSession sqlSession1 = MyBatisUtils.getSqlSession();
UserDao userDao1 = sqlSession1.getMapper(UserDao.class);
User user1 = userDao1.getUserById(1);
sqlSession1.close();
//只有sqlSession1关闭后,一级缓存内的数据才会进入二级缓存
SqlSession sqlSession2 = MyBatisUtils.getSqlSession();
UserDao userDao2 = sqlSession2.getMapper(UserDao.class);
User user2 = userDao2.getUserById(1);
sqlSession2.close();
System.out.println(user1 == user2);
}
根据日志可以看到sql只执行了一次,且查到的User对象是同一个
注意:
- 二级缓存对同一个Mapper下的数据才能起到提高效率的效果
- 数据会先放在一级缓存中,只有当会话提交或者关闭后,才会放到二级缓存中
- 每次查询的时候,会优先检查二级缓存,没有查到再查一级缓存,没有查到最后才查询数据库
15.6 自定义缓存EhCache
可以使用自定义的缓存,也可以使用第三方的缓存
EhCache是专业的Java进程内的缓存框架
16 PageHelper分页插件
PageHelper是MyBatis优秀的分页插件,可以帮我们解决分页查询上的问题,github仓库 PageHelper
使用方法参考此处