MyBatis
MyBatis 本是 apache 的一个开源项目 iBatis,2010年这个项目由 apache software foundation 迁移到了 google code,并且改名为 MyBatis。2013年11月迁移到 Github。iBATIS 一词来源于 “internet” 和 “abatis” 的组合,是一个基于 Java 的持久层框架。iBATIS 提供的持久层框架包括 SQL Maps 和 Data Access Objects(DAOs)。
MyBatis 概述
框架
框架(framework)其实就是对通用代码的封装,提前写好了一堆接口和类,我们可以在做项目的时候直接引入这些接口和类(引入框架),基于这些现有的接口和类进行开发,可以大大提高开发效率。框架一般都以 jar 包的形式存在。(jar 包中有 class 文件以及各种配置文件等。)
三层架构
- 表示层(UI):直接和前端打交互(一是接收前端 ajax 技术,二是向前端返回 json 串)。
- 业务逻辑层(BLL):处理表示层转发过来的请求,并将从持久层获取的数据返回表现层。
- 持久化层(DAL):直接操作数据库完成 CRUD,并将获得的数据返回 BLL。
JDBC 的不足
JDBC 的 sql 语句是直接写在 java 程序中的,采用硬编码的方式,当功能需要增强、扩展的时候,会违背 OCP 原则。
JDBC 为了防止 SQL 注入,占位符、设置参数以及获取参数的编码方式太繁琐,代码冗余。
了解 MyBatis
MyBatis 中文网(戳我)。MyBatis 本质上就是对 JDBC 的封装,通过 MyBatis 完成 CRUD。MyBatis 在三层架构中负责持久层的,属于持久层框架。
MyBatis 涉及到一种思想叫 ORM 设计思想。ORM(对象关系映射)分为三个部分:
- O(Object):Java 虚拟机中的 Java 对象。
- R(Relational):关系型数据库。
- M(Mapping):将 Java 虚拟机中的 Java 对象映射到数据库中的一行记录,或是将数据库表中一行记录映射成 Java 虚拟机中的一个 Java 对象。
MyBatis 是一个半自动化的 ORM 框架,因为当中的 SQL 语句需要程序员自己编写。(Hibernate 框架就是一个全自动化的 ORM 框架)
MyBatis 入门
引入依赖:
<dependencies>
<!-- mysql链接驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- mybatis依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
<!-- 引入junit测试依赖 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<!-- 引入logback依赖 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.11</version>
</dependency>
</dependencies>
从 XML 中构建 SqlSessionFactory
mybatis-config.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="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test"/>
<property name="username" value="root"/>
<property name="password" value="MySQL:040809"/>
</dataSource>
</environment>
</environments>
<!-- 指定路径 -->
<mappers>
<!-- resource属性会自动从类的根路径下查找资源 -->
<mapper resource="CarMapper.xml"/>
</mappers>
</configuration>
编写 Mapper 文件
我们主要在这个文件中编写 sql 语句,通常一张表对应一个 Mapper 文件。在这里我们编写一个CarMapper.xml
来处理t_car
的数据:
<?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="hnuxcc21">
<!-- insert语句,id是sql语句的唯一标识,id代表了这个sql语句 -->
<insert id="insertCar">
insert into t_car
values (null, '1003', '丰田霸道', 30.00, '2000-10-11', '燃油车')
</insert>
</mapper>
编写 MyBatis 程序
使用 MyBatis 类库,链接数据库,进行数据的增删改查。在 MyBatis 中,负责执行 sql 语句的对象叫SqlSession
,是 Java 程序和数据库之间的一次会话。
要想获取SqlSession
对象,需要先获取SqlSessionFactory
对象,通过SqlSessionFactory
来生产SqlSession
对象。我们需要利用SqlSessionFactoryBuilder
对象的build
方法获取SqlSessionFactory
对象。
一般一个数据库对应一个SqlSessionFactory
对象。
public class MyBatisIntroductionTest {
public static void main(String[] args) throws IOException {
//获取SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//获取SqlSessionFactory对象,从类根路径下查找资源
InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory factory = builder.build(resourceAsStream);
//获取SqlSession对象
SqlSession sqlSession = factory.openSession();
//执行sql语句,传入sql语句的id,返回影响数据库表中的记录条数
int count = sqlSession.insert("insertCar");
System.out.println(count);
//手动提交
sqlSession.commit();
}
}
关于入门程序的一些细节
在配置文件中的 sql 语句可以省略分号。
如果涉及到 resource 这个单词,大部分情况下,这种加载是从类的根路径下开始加载的。除了使用
Resources
的getResourceAsStream
方法,也可以使用自己创建的流://使用类加载器创建文件流,加载类路径下的资源 InputStream resourceAsStream = ClassLoader .getSystemClassLoader() .getResourceAsStream("mybatis-config.xml");
Mapper 文件的名称和路径都不是固定的。Mapper 配置文件中,
mapper
标签的url
属性是从绝对路径来加载文件的,并且路径前方需要加上file:///
,该做法我们不推荐。
MyBatis 事务管理机制
在mybatis-config.xml
配置文件中,有一个transactionManager
。语句为:
<transactionManager type="JDBC"/>
其中,type 的值有两个:JDBC
和MANAGED
(不区分大小写)。也就是说,MyBatis 中提供了两种事务管理机制,分别是 JDBC 和 MANAGED 的事务管理器。
- JDBC 事务管理器:MyBatis 框架自己采用原生 jdbc 代码管理事务。
- MANAGED 事务管理器:MyBatis 不再负责事务,而是交给其他容器管理,例如 Spring。
JDBC 事务管理器的事务默认是手动提交的,可以在构造SqlSession
的时候开启自动提交(不建议,因为没有开启事务):
//获取SqlSession对象
SqlSession sqlSession = factory.openSession(true);
完善入门程序
事务的提交与回滚
/**
* 采用正规的方式写一个完整版的MyBatis程序
* @author n70huihui
* @version 1.0
* @since 1.0
*/
public class MyBatisComplete {
public static void main(String[] args) {
SqlSession sqlSession = null;
try {
//创建builder
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//创建factory
SqlSessionFactory factory = builder.build(Resources.getResourceAsStream("mybatis-config.xml"));
//创建SqlSession
sqlSession = factory.openSession();
//执行sql语句
sqlSession.insert("insertCar");
//提交事务
sqlSession.commit();
} catch (IOException e) {
//遇到异常,回滚事务
if (sqlSession != null) {
sqlSession.rollback();
}
e.printStackTrace();
} finally {
//关闭会话
if (sqlSession != null) {
sqlSession.close();
}
}
}
}
集成日志框架 logback
在mybatis-config.xml
配置文件的configuration
标签中引入如下片段:
<!-- 开启标准日志STDOUT_LOGGING -->
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
STDOUT_LOGGING 是标准日志,MyBatis 已经实现了这种标准日志,只要配置了上述代码,就可以直接使用。
不过,这个标签在编写的时候要注意,其应该出现在environment
标签之前。(dtd 文件有对标签的顺序进行约束)
也可以引入其他日志框架,例如 logback:
<!-- 引入logback依赖 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.11</version>
</dependency>
接下来引入 logback 所必须的配置文件,这个配置文件必须叫做logbacktest.xml
,不能是其他名字(放在类路径下,此时我们便无需配置mybatis-config.xml
):
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!-- 输出日志记录格式 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<!-- mybatis log configure -->
<logger name="com.apache.ibatis" level="TRACE"/>
<logger name="java.sql.Connection" level="DEBUG"/>
<logger name="java.sql.Statement" level="DEBUG"/>
<logger name="java.sql.PreparedStatement" level="DEBUG"/>
<!-- 日志输出级别,logback日志级别包括五个: TRACE < DEBUG < INFO < WARN < ERROR -->
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</root>
</configuration>
MyBatis 工具类编写
在 MyBatis 中,一个SqlSessionFactory
对应一个 environment,通常一个 environment 对应一个数据库。
/**
* MyBatis工具类
*/
public class SqlSessionUtil {
private SqlSessionUtil() {}
private static final SqlSessionFactory FACTORY;
private static final ThreadLocal<SqlSession> LOCAL;
static {
LOCAL = new ThreadLocal<>();
try {
FACTORY = new SqlSessionFactoryBuilder()
.build(Resources.getResourceAsStream("mybatis-config.xml"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static SqlSession openSession() {
//保证同一线程使用同一个SqlSession
SqlSession sqlSession = LOCAL.get();
if (sqlSession == null) {
sqlSession = FACTORY.openSession();
LOCAL.set(sqlSession);
}
return sqlSession;
}
public static void close(SqlSession sqlSession) {
//从当前线程中移除SqlSession对象
if (sqlSession != null) {
sqlSession.close();
LOCAL.remove();
}
}
}
此时配置类中的代码便简洁一些了:
@Test
public void testInsertByUtil() {
SqlSession sqlSession = SqlSessionUtil.openSession();
int count = sqlSession.insert("insertCar");
System.out.println(count);
sqlSession.commit();
sqlSession.close();
}
利用 MyBatis 完成 CRUD
完成 insert (使用 Map 集合传参)
在 jdbc 我们使用?
当作占位符,在 mybatis 中我们使用#{}
来作为占位符(二者作用相同)。
<?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="hnuxcc21">
<!-- insert语句,id是sql语句的唯一标识,id代表了这个sql语句 -->
<insert id="insertCar">
insert into t_car
values (null, #{k1}, #{k2}, #{k3}, #{k4}, #{k5});
</insert>
</mapper>
使用 Map 集合添加对象:
@Test
public void testInsertCar() {
SqlSession sqlSession = SqlSessionUtil.openSession();
//执行sql语句,传入pojo对象,我们先使用map集合进行数据的封装
Map<String, Object> map = new HashMap<>();
map.put("k1", "102");
map.put("k2", "比亚迪汉");
map.put("k3", 10.0);
map.put("k4", "2020-11-11");
map.put("k5", "新能源");
int count = sqlSession.insert("insertCar", map);
System.out.println(count);
sqlSession.commit();
sqlSession.close();
}
如果占位符中的 key 不存在,则会填入 null。
完成 insert(使用 POJO 对象传参)
可以使用 Java 对象给 sql 语句传值,此时占位符中需要填入 POJO 类的属性名。MyBatis 通过寻找 POJO 类的get
方法,所以实际上填入的是get
方法方法名去掉 get 后首字母小写。
<?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="hnuxcc21">
<!-- insert语句,id是sql语句的唯一标识,id代表了这个sql语句 -->
<insert id="insertCar">
insert into t_car
values (null, #{carNum}, #{brand}, #{guidePrice}, #{produceTime}, #{carType});
</insert>
</mapper>
@Test
public void testInsertCarByPoJo() {
SqlSession sqlSession = SqlSessionUtil.openSession();
//封装数据
Car car = new Car(null, "103", "比亚迪秦", 30.0, "2020-11-11", "新能源");
//直接插入Car
int count = sqlSession.insert("insertCar", car);
System.out.println(count);
sqlSession.commit();
sqlSession.close();
}
完成 delete(简单删除)
配置文件:
<?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="hnuxcc21">
<!-- delete语句,占位符如果只有一个,#{}当中可以随便写,但是最好见名知意 -->
<delete id="deleteById">
delete from t_car where id = #{id};
</delete>
</mapper>
@Test
public void testDeleteById() {
SqlSession sqlSession = SqlSessionUtil.openSession();
//删除id为15
int count = sqlSession.delete("deleteById", 15);
System.out.println(count);
sqlSession.commit();
sqlSession.close();
}
完成 update
<?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="hnuxcc21">
<!-- update语句,同样也最好填上属性名 -->
<update id="updateCar">
update
t_car
set
car_num = #{carNum},
brand = #{brand},
guide_price = #{guidePrice},
produce_time = #{produceTime},
car_type = #{carType}
where
id = #{id};
</update>
</mapper>
@Test
public void testUpdate() {
SqlSession sqlSession = SqlSessionUtil.openSession();
int count = sqlSession.update("updateCar", new Car(13, "104", "凯美瑞", 30.3, "2000-11-30", "燃油车"));
System.out.println(count);
sqlSession.commit();
sqlSession.close();
}
完成 select(查一个)
<?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="hnuxcc21">
<!-- 利用resultType指定要封装成什么对象 -->
<select id="selectById" resultType="cn.hnu.mybatis.pojo.Car">
select
id,
car_num carNum,
brand,
guide_price guidePrice,
produce_time produceTime,
car_type carType
from
t_car
where
id = #{id};
</select>
</mapper>
@Test
public void testSelectById() {
SqlSession sqlSession = SqlSessionUtil.openSession();
//调用selectOne查一个
Car car = sqlSession.selectOne("selectById", 1);
System.out.println(car);
//此时无需提交,因为这个是一个查询语句
//sqlSession.commit();
sqlSession.close();
}
需要特别注意的是:select 标签中 resultType 属性用来告诉 MyBatis 查询结果集封装成什么类型的 Java 对象。并且,记得给查询出来的结果集当中的字段起别名,使得查询字段符合 POJO 类的属性名,这样才能够成功赋值。
完成 select (查所有)
<?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="hnuxcc21">
<!-- resultType写的是List集合中的元素类型 -->
<select id="selectAll" resultType="cn.hnu.mybatis.pojo.Car">
select
id,
car_num carNum,
brand,
guide_price guidePrice,
produce_time produceTime,
car_type carType
from
t_car;
</select>
</mapper>
@Test
public void testSelectAll() {
SqlSession sqlSession = SqlSessionUtil.openSession();
//调用selectList返回list集合
List<Car> list = sqlSession.selectList("selectAll");
list.forEach(System.out::println);
sqlSession.close();
}
Mapper 中的 namespace
当多个 Mapper 文件拥有同名的 sql 语句时,可以使用 mapper 文件的 namespace 来进行区分。
@Test
public void testNameSpace() {
SqlSession sqlSession = SqlSessionUtil.openSession();
//加上命名空间hnuxcc21来防止id冲突
List<Car> list = sqlSession.selectList("hnuxcc21.selectAll");
list.forEach(System.out::println);
sqlSession.close();
}
MyBatis 核心配置文件
多环境
<?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">
<!-- dtd约束,约束了该配置文件可以出现哪些标签以及这些标签的关系 -->
<!-- 根标签configuration,只能有一个 -->
<configuration>
<!-- environments可以配置多个环境,default表示默认使用的环境 -->
<environments default="development">
<!-- 一般一个数据库(或者一个环境)会对应一个SqlSessionFactory对象 -->
<!-- 其中的一个环境,id为development,链接了test这个数据库 -->
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test"/>
<property name="username" value="root"/>
<property name="password" value="MySQL:040809"/>
</dataSource>
</environment>
<!-- 另一个环境 -->
<environment id="newsSystemDB">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/news_system"/>
<property name="username" value="root"/>
<property name="password" value="MySQL:040809"/>
</dataSource>
</environment>
</environments>
<!-- 指定路径 -->
<mappers>
<!-- resource属性会自动从类的根路径下查找资源 -->
<mapper resource="CarMapper.xml"/>
</mappers>
</configuration>
@Test
public void testEnvironment() throws IOException {
//获取SqlSessionFactory对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//使用环境id指定数据库
SqlSessionFactory factory = builder.build(Resources.getResourceAsStream("mybatis-config.xml"), "newsSystemDB");
//...
}
数据源
数据源可以为程序提供 Connection 对象。数据源实际上是一套规范,一个接口,JDK中有这套规范(javax.sql.DataSource
)。我们只要实现javax.sql.DataSource
接口,就可以拥有自己的数据源(也叫数据库连接池)
指定 type 标签也就是在指定使用哪个数据库连接池,指定具体使用什么方式来获取 Connection 对象。
常见的数据源:Druid 连接池、c3p0、dbcp…
type标签有三种:
POOLED:使用 MyBatis 自己的数据库连接池。
POOLED 中几个比较重要的参数有:
poolMaximumActiveConnections
– 在任意时间可存在的活动(正在使用)连接数量,默认值:10。poolMaximumCheckoutTime
– 在被强制返回之前,池中连接被检出(checked out)时间,默认值:20000 毫秒(即 20 秒)。poolTimeToWait
– 这是一个底层设置,如果获取连接花费了相当长的时间,连接池会打印状态日志并重新尝试获取一个连接(避免在误配置的情况下一直失败且不打印日志),默认值:20000 毫秒(即 20 秒)。poolMaximumIdleConnections
– 任意时间可能存在的空闲连接数。
UNPOOLED:不使用连接池,来一个链接就创建一个新的 Connection 对象。
JNDI:集成第三方数据连接池。
其中,JNDI 是 java 命名目录接口,是一套规范,很多 Web 容器(Tomcat、Jetty、WebLogic、WebSphere)都实现了 JNDI 规范。也就是说,数据源是配置在 Web容器里的,使用 JNDI 是为了让容器集成 MyBatis。
properties 标签
<?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>
<!-- 利用properties可以给下方的属性赋值,使得属性赋值更加灵活 -->
<!--
<properties>
<property name="jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="jdbc.url" value="jdbc:mysql://localhost:3306/test"/>
<property name="jdbc.username" value="root"/>
<property name="jdbc.password" value="MySQL:040809"/>
</properties>
-->
<!-- 利用resource引入外部properties配置文件(类路径下) -->
<!-- 使用url是从绝对路径下引入资源,使用resource是从类根路径下引入资源 -->
<properties resource="jdbc.properties"/>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<!-- 利用${}配置信息 -->
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<!-- 指定路径 -->
<mappers>
<!-- resource属性会自动从类的根路径下查找资源 -->
<mapper resource="CarMapper.xml"/>
</mappers>
</configuration>
MyBatis 三大对象作用域
- SqlSessionFactoryBuilder:这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。 你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。
- SqlSessionFactory: 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。
- SqlSession:每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。
WEB 应用中应用 MyBatis
在使用 MyBatis 处理 Web 业务的时候,我们需要考虑的到事务的异常情况。在之前的操作中,我们利用 SqlSession 的commit
方法提交事务,为了保证每个线程的事务能够被正常关闭,所以我们需要利用 LocalThread 来处理线程与 SqlSession 之间的关系。
接下来,我们以银行转账的案例来展示 MyBatis 在 Web 项目中的实际应用。首先是 Controller 层:
@WebServlet("/transfer")
public class AccountServlet extends HttpServlet {
private final AccountService accountService = new AccountServiceImpl();
@Override
protected void service(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
//获取表单数据
String fromAction = req.getParameter("fromAction");
String toAction = req.getParameter("toAction");
int money = Integer.parseInt(req.getParameter("money"));
//调用service层
try {
accountService.transfer(fromAction, toAction, money);
//调用完展示页面最终结果
resp.sendRedirect(req.getContextPath() + "/success.html");
} catch (MoneyNotEnoughException e) {
resp.sendRedirect(req.getContextPath() + "/MoneyNotEnoughException.html");
} catch (Exception e) {
resp.sendRedirect(req.getContextPath() + "/TransferException.html");
}
}
}
Service 层(在这里我们就要对事务进行提交,不能把提交放到 Dao 层中):
public class AccountServiceImpl implements AccountService {
private final AccountDao accountDao = new AccountDaoImpl();
@Override
public void transfer(String fromAction, String toAction, int money)
throws MoneyNotEnoughException, TransferException {
//控制事务
//此处的SqlSessionUtil内部自带了ThreadLocal,保证了SqlSession的线程安全
SqlSession sqlSession = SqlSessionUtil.openSession();
//判断转出账户的余额是否充足
Account fromAccount = accountDao.selectByAccount(fromAction);
if (fromAccount.getMoney() < money) {
throw new MoneyNotEnoughException("对不起,余额不足");
}
//先更新内存余额
fromAccount.setMoney(fromAccount.getMoney() - money);
Account toAccount = accountDao.selectByAccount(toAction);
toAccount.setMoney(toAccount.getMoney() + money);
//余额写入数据库
int count = accountDao.updateByAction(fromAccount);
//模拟异常
String s = null;
s.toString();
count += accountDao.updateByAction(toAccount);
if(count != 2) {
throw new TransferException("转账异常,未知原因");
}
sqlSession.commit();
//调用close方法关闭线程中的sqlSession
SqlSessionUtil.close(sqlSession);
}
}
最后是 Dao 层代码:
public class AccountDaoImpl implements AccountDao {
@Override
public Account selectByAccount(String accountName) {
SqlSession sqlSession = SqlSessionUtil.openSession();
return sqlSession.selectOne("selectByAccountName", accountName);
}
@Override
public int updateByAction(Account account) {
SqlSession sqlSession = SqlSessionUtil.openSession();
int count = sqlSession.update("updateByAction", account);
return count;
}
}
我们看出,在三层架构下,我们的 Dao 层代码变得很少。并且SqlSession
的获取代码变得重复冗余,也没有业务代码。所以,我们需要一种新的技术来帮助我们解决这些问题。我们接下来会介绍 Javassist,当我们使用了这项技术之后,我们的 Dao 层实现类便不需要我们手动编写,我们只负责编写接口即可。
Javassist 动态生成类
<!-- javassist依赖 -->
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
基础使用
(如果运行出错, 在方法编辑中点击右上角“修改选项”勾选“添加虚拟机选项”,然后在方法的构建与运行中加入参数:--add-opens java.base/java.lang=ALL-UNNAMED
,在环境变量中加入参数:--add-opens java.base/sun.util=ALL-UNNAMED
)
@Test
public void testGenerateFirstClass() throws Exception {
//获取类池,这个类池就是用来生成class的
ClassPool pool = ClassPool.getDefault();
//制造类,需要传入全类名
CtClass ctClass = pool.makeClass("cn.hnu.bank.dao.Impl.AccountDaoImpl");
//制造方法,第一个参数传方法的代码,第二个参数传入对应的ctClass
String methodCode = "public void showInfo() {System.out.println(\"hello world\");}";
CtMethod ctMethod = CtMethod.make(methodCode, ctClass);
//向类中添加方法
ctClass.addMethod(ctMethod);
//在内存中生成class
ctClass.toClass();
//类加载,把上面生成的对象加载到虚拟机中,返回AccountDao的字节码
Class<?> clazz = Class.forName("cn.hnu.bank.dao.Impl.AccountDaoImpl");
//创建对象
Object obj = clazz.getConstructor().newInstance();
//获取新添加的showInfo方法
Method method = clazz.getMethod("showInfo");
//调用方法
method.invoke(obj);
}
实现接口中的单个方法
//现有一个单方法的接口
public interface AccountDao {
void delete();
}
@Test
public void testGenerateImpl() throws Exception {
//获取类池
ClassPool pool = ClassPool.getDefault();
//制造类
CtClass ctClass = pool.makeClass("cn.hnu.bank.dao.Impl.AccountDaoImpl");
//制造接口
CtClass ctInterface = pool.makeInterface("cn.hnu.bank.dao.AccountDao");
//添加接口到类中,表示让上述的类去实现这个接口
ctClass.addInterface(ctInterface);
//实现接口中的方法,我们需要先制造方法出来
String methodCode = "public void delete() {System.out.println(\"delete\");}";
CtMethod ctMethod = CtMethod.make(methodCode, ctClass);
//将方法添加到类中
ctClass.addMethod(ctMethod);
//在内存中生成类,同时,将生成的类装载到JVM中
Class<?> clazz = ctClass.toClass();
//生成对象并调用delete方法
AccountDao accountDao = (AccountDao) clazz.getConstructor().newInstance();
accountDao.delete();
}
实现接口中的多个方法
//现有多个不同返回值,不同参数的方法
public interface AccountDao {
void delete();
int insert(String account);
int update(String account, Double balance);
String selectByAccountNo(String AccountNo);
}
@Test
public void testGenerateAccountDaoImpl() throws Exception {
//获取类池
ClassPool pool = ClassPool.getDefault();
//制造类
CtClass ctClass = pool.makeClass("cn.hnu.bank.dao.Impl.AccountDaoImpl");
//制造接口
CtClass ctInterface = pool.makeInterface("cn.hnu.bank.dao.AccountDao");
//实现接口
ctClass.addInterface(ctInterface);
//实现接口中的所有方法
//我们需要先获取接口中所有方法
Method[] methods = AccountDao.class.getDeclaredMethods();
Arrays.stream(methods).forEach(method -> {
CtMethod ctMethod = null;
try {
StringBuilder methodCode = new StringBuilder("public ");
//追加返回值类型(获得全类名后再获取实际名称)
methodCode.append(method.getReturnType().getSimpleName());
methodCode.append(" ");
//追加方法名
methodCode.append(method.getName());
methodCode.append("(");
//拼接参数,先获取方法的各个参数类型数组
Class<?>[] parameterTypes = method.getParameterTypes();
for (int i = 0; i < parameterTypes.length; ++i) {
//获取单个参数类型
Class<?> parameterType = parameterTypes[i];
//参数类型名称
methodCode.append(parameterType.getName());
methodCode.append(" ");
//参数名称
methodCode.append("arg" + i);
if (i != parameterTypes.length - 1) {
methodCode.append(", ");
}
}
methodCode.append(") {System.out.println(\"hello\");");
//动态添加返回语句
//获取参数类型(普通名称)
String returnTypeSimpleName = method.getReturnType().getSimpleName();
switch (returnTypeSimpleName) {
case "int" -> methodCode.append("return 1;");
case "String" -> methodCode.append("return \"hello\";");
}
methodCode.append("}");
ctMethod = CtMethod.make(methodCode.toString(), ctClass);
ctClass.addMethod(ctMethod);
} catch (Exception e) {
e.printStackTrace();
}
});
//在内存中生成class并加载到JVM中
Class<?> clazz = ctClass.toClass();
//创建对象
AccountDao accountDao = (AccountDao) clazz.getConstructor().newInstance();
//调用方法
accountDao.insert("aaa");
accountDao.delete();
accountDao.update("aaa", 100.0);
accountDao.selectByAccountNo("aaa");
}
MyBatis 中的代理类
在 MyBatis 中,提供了相关的机制,也可以为我们动态生成代理类。此时注意当我们使用代理类时,namespace 必须是 Dao 接口的全限定类名,sql 语句的 id 必须是 Dao 接口的方法名。此时将全限定类名与 id 名拼接在一起,代理类才可以唯一确定一条 sql 语句。
//MyBatis也使用了类似的代理机制,自动创建接口的代理类
private final AccountDao accountDao = SqlSessionUtil.openSession()
.getMapper(AccountDao.class);
MyBatis 小技巧
#{} 和 ${}
#{}
:先编译 sql 语句,再给占位符传值,底层是 PreparedStatement 实现。可以防止 sql 注入,我们需要优先使用。${}
:先进行 sql 语句拼接,然后再编译 sql 语句,底层是 Statement 实现。存在 sql 注入现象。只有在需要进行 sql 语句关键字拼接的情况下才会用到。
可以理解为,#{}
为了防止 sql 注入,拼接参数的时候会把参数当作一个字符串拼接(也就是在原本参数的基础上添加两个双引号)。而${}
是直接拼接,在参数两侧是不带双引号的。
${}
的使用场景:
<!-- 需要直接拼接asc或desc字符串的时候,使用${} -->
<mapper namespace="cn.hnu.mybatis.mapper.CarMapper">
<select id="selectByCarTypeAscOrDesc" resultType="cn.hnu.mybatis.pojo.Car">
select
id,
car_num carNum,
brand,
guide_price guidePrice,
produce_time produceTime,
car_type carType
from
t_car
order by
guidePrice ${ascOrDesc}
</select>
</mapper>
批量删除
批量删除,顾名思义就是一次性删除有多条记录。这个时候,sql 语句有两种写法:
- 使用 or 连接:
delete from t_user where id = 1 or id = 2 ...
。 - 使用 in 关键字:
delete from t_user where id in (1, 2, 3...)
。
我们可以考虑使用第二个方案,先利用${}
拼接 id:
<delete id="deleteBatch">
delete from t_car where car_num in (${ids});
</delete>
再传入 id 即可:
int count = carMapper.deleteBatch("105,106,107");
模糊查询
模糊查询也可以直接使用${}
进行拼接:
<select id="selectByBrand" resultType="cn.hnu.mybatis.pojo.Car">
select
id,
car_num carNum,
brand,
guide_price guidePrice,
produce_time produceTime,
car_type carType
from
t_car
where
brand like '%${brand}%';
</select>
<!--
第二种方案
... where brand like concat('%', #{brand}, '%');
第三种方案(注意使用${}时需要在外面自己加上两个引号)
... where brand like concat('%', '${brand}', '%');
第四种方案
... where brand like "%"#{brand}"%";
-->
别名机制
MyBatis 给我们提供了别名机制,让我们能够给resultType
需要填入的参数起别别名,我们需要在配置文件mybatis-config
中进行配置,当然,我们需要注意一下标签的顺序:
<?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>
<typeAliases>
<!--
给type起别名,别名为alias
<typeAlias type="cn.hnu.mybatis.pojo.Car" alias="MyCar"/>
-->
<!-- namespace不能起别名 -->
<!-- 也可以省略alias,省略之后,别名就是类的简名,不区分大小写-->
<typeAlias type="cn.hnu.mybatis.pojo.Car"/>
<!-- 指定包的方式,包名下所有类自动起别名,别名就是类简名,不区分大小写 -->
<package name="cn.hnu.mybatis.pojo"/>
</typeAliases>
</configuration>
mapper 的配置
mapper 中有三个标签:
- resource:这种方式是从类的根路径下开始查找资源,采用这种方式的话,配置文件需要放到类路径当中去。
- url:这种方式是绝对路径的方式,不要求放入类的根路径。这种方式使用极少,因为移植性太差。
- class:全限定接口名,带有包名。框架会在这个接口的同级目录下找这个文件。也就是说,如果使用这种方式,配置文件的 resource 目录结构需要和接口所在的包结构一致。
获取自动生成的主键
如果生成后再查询主键,这会有点麻烦。MyBatis 给我们提供了一个更加方便的方法。
mapper 配置文件:
<!--
useGeneratedKeys开启使用生成的主键
keyProperty指定将自动生成的主键放入对象的哪个属性中
-->
<insert id="insertCarAndUseKey" useGeneratedKeys="true" keyProperty="id">
insert into
t_car
values
(null, #{carNum}, #{brand}, #{guidePrice}, #{produceTime}, #{carType});
</insert>
测试程序:
@Test
public void testInsertAndUseKey() {
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper carMapper = sqlSession.getMapper(CarMapper.class);
//new对象,接收id数据
Car car = new Car(null, "107", "凯美瑞", 30.01, "2022-01-01", "新能源");
carMapper.insertCarAndUseKey(car);
sqlSession.commit();
sqlSession.close();
//自动把主键值赋值给id属性
System.out.println(car.getId());
}
MyBatis 参数处理
单个简单类型参数
<mapper namespace="cn.hnu.mybatis.mapper.StudentMapper">
<!--
parameterType可以指定单个参数的类型(可以不写,MyBatis会做自动类型推断)
方便MyBatis底层jdbc代码调用set方法进行占位符赋值
当然,MyBatis提供了很多参数类型别名,具体可以参阅官方手册
-->
<select id="selectById" resultType="Student" parameterType="Integer">
select * from t_student where id = #{id};
</select>
<!-- 也可以在#{}中填入java和jdbc中的类型区别 -->
<select id="selectByName" parameterType="Student">
select * from t_student where name = #{name, javaType=String, jdbcType=VARCHAR};
</select>
<select id="selectByBirth" resultType="Student">
select * from t_student where birth = #{birth};
</select>
<select id="selectByGender">
select * from t_student where gender = #{gender};
</select>
</mapper>
Map 集合
<!-- 使用map集合传参的时候,#{}当中需要存放map集合的key -->
<insert id="insertByMap">
insert into t_student values (null, #{姓名}, #{年龄}, #{身高}, #{生日}, #{性别});
</insert>
POJO 类
<!-- 使用pojo对象传参,占位符当中填入的是pojo对象的属性名 -->
<insert id="insertStudentByPojo">
insert into t_student values (null, #{name}, #{age}, #{height}, #{birth}, #{gender});
</insert>
多参数
如果是多个参数的话,MyBatis 框架底层会自动创建一个 map 集合,并且 map 集合的键是arg0, arg1, ...
。
<!-- 多参数传递,如果使用param的话需要从1开始索引 -->
<select id="selectByNameAndGender" resultType="Student">
select * from t_student where name = #{arg0} and gender = #{arg1};
</select>
Param 注解
通过 Param 注解可以为多参数指定 map 集合键的名字。
public interface StudentMapper {
/* 在方法中利用@Param注解为参数的key取名字 */
List<Student> selectByNameAndGenderAnnotation(@Param("name") String name,@Param("gender") Character gender);
}
<!-- 在占位符中填入利用@Param取的名字 -->
<select id="selectByNameAndGenderAnnotation" resultType="Student">
select * from t_student where name = #{name} and gender = #{gender};
</select>
使用了注解之后,arg 系列就失效了,但是 param 系列还是可以继续使用的。
MyBatis 查询语句专题
返回单个 POJO 对象
<!-- 如果数据库的字段名和pojo对象的字段名不一致,需要起别名 -->
<select id="selectById" resultType="Car">
select
id,
car_num carNum,
brand,
guide_price guidePrice,
produce_time produceTime,
car_type carType
from t_car where id = #{id};
</select>
返回多个 POJO 对象
写法同上,只不过要用 List 集合来接收。
返回 Map 集合
如果我们倒时候查询出来的结果没有合适的 Java 对象来接收数据,我们可以把数据放入一个 Map<String, Object>
集合当中。
<!-- resultType写成Map即可返回一个Map集合 -->
<select id="selectByIdReturnMap" resultType="Map">
select
id,
car_num carNum,
brand,
guide_price guidePrice,
produce_time produceTime,
car_type carType
from t_car where id = #{id};
</select>
返回多个 Map 集合
返回的时候使用List<Map<String, Object>>
即可。
返回大 Map 集合
上述我们把返回的 Map 集合放入到一个 List 集合中,但是当数据量变大的时候,List 集合一般检索效率比较慢,我们可以把 Map 放入另一个 Map 中,形成一个大 Map(Map<Integer, Map<String, Object>>
)。
/* 在方法上加上注解MapKey,将小Map的某个key作为大Map的key */
@MapKey("id")
Map<Integer, Map<String, Object>> selectAll();
查询结果映射
查询结果映射是用来解决数据库字段名称和 Java 对象属性名不一致的问题的,一共有三种解决方案:
使用查询语句的时候给字段起别名。
使用 resultMap 进行结果映射。
<!-- 专门定义一个结果映射,专门指定java对象和数据库字段的对应关系 --> <!-- type标签用来指定pojo类的类名 --> <!-- id属性,指定resultMap的唯一标识,这个id将来要在select中使用 --> <resultMap id="carResultMap" type="Car"> <!-- 数据库表有主键的话,需要额外配置一个id标签 --> <id property="id" column="id"/> <!-- property填写pojo类的属性名,column填写数据库表的字段名 --> <result property="carNum" column="car_num"/> <result property="brand" column="brand"/> <result property="guidePrice" column="guide_price"/> <result property="produceTime" column="produce_time"/> <result property="carType" column="car_type" javaType="String" jdbcType="VARCHAR"/> </resultMap> <!-- resultMap需要指定resultMap的id--> <select id="selectAllByResultMap" resultMap="carResultMap"> select * from t_car; </select>
开启自动驼峰映射,但前提是 Java 类和数据库字段命名符合规范(配置
mybatis-config.xml
)。<!-- 开启驼峰自动映射 --> <settings> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings>
返回总记录条数
<!-- resultType填上Integer -->
<select id="selectTotal" resultType="Integer">
select count(*) from t_car;
</select>
动态 SQL
当 sql 语句中的 where 条件等我们没办法写死的时候(例如批量删除,条件查询等),就需要使用动态 SQL。
if 标签
需求:多条件查询。
<!--
if标签中的test属性是必须的
test内容是个boolean类型,如果是true,就把if的内容拼接到sql语句中
如果使用了@Param,那么test属性中需要填入@Param的value属性
如果没有使用@Param,则需要使用arg0,arg1或者param1,param2...
如果使用POJO类,则需要填入属性名
在MyBatis的动态sql中,不能使用&&,需要使用and
加上 1 = 1,符合sql语句查询,防止出现条件都不符合的情况下报出语法错误
多条件查询记得在if标签中使用and连接
-->
<select id="selectByMultiCondition" resultType="Car">
select
*
from
t_car
where
1 = 1
<if test="brand != null and brand != ''">
and brand like '%${brand}%'
</if>
<if test="guidePrice != null">
and guide_price > #{guidePrice}
</if>
<if test="carType != null and carType != ''">
and car_type = #{carType}
</if>
</select>
where 标签
让 where 子句更加动态智能。有两个作用:
- 当所有条件都为空的时候,where 标签能保证不会生成 where 子句。
- 自动去除某些条件前面多余的 and 或者 or。
<!-- 使用where子句使得sql更加动态灵活 -->
<select id="selectByMultiConditionUseWhere" resultType="Car">
select
*
from
t_car
<where>
<if test="brand != null and brand != ''">
and brand like '%${brand}%'
</if>
<if test="guidePrice != null">
and guide_price > #{guidePrice}
</if>
<if test="carType != null and carType != ''">
and car_type = #{carType}
</if>
</where>
</select>
trim 标签
<!--
prefix 加前缀
suffix 加后缀
prefixOverrides 去掉前缀
suffixOverrides 去掉后缀
-->
<select id="selectByMultiConditionUseTrim" resultType="Car">
select
*
from
t_car
<!-- 加上前缀where,并且去掉后缀and或者or,使用|连接多个参数 -->
<trim prefixOverrides="" suffix="" prefix="where" suffixOverrides="and|or">
<if test="brand != null and brand != ''">
brand like '%${brand}%' and
</if>
<if test="guidePrice != null">
guide_price > #{guidePrice} and
</if>
<if test="carType != null and carType != ''">
car_type = #{carType} and
</if>
</trim>
</select>
set 标签
主要使用在 update 语句中,用来动态生成 set 关键字,同时去掉最后多余的,
。考虑到一个实际问题,就是当我们使用 set 语句的时候,如果往里传入一个 null 参数,那么很有可能会把本来不为空的参数更新为 null,我们需要使用 set 标签来避免这种情况的发生,只提交不为空的字段。
<!-- 使用set标签动态拼接sql,同时自动去除末尾多余的, -->
<update id="updateCarBySet">
update
t_car
<set>
<if test="carNum != null and carNum != ''">
car_num = #{carNum},
</if>
<if test="brand != null and brand != ''">
brand = #{brand},
</if>
<if test="guidePrice != null and guidePrice != ''">
guide_price = #{guidePrice},
</if>
<if test="produceTime != null and produceTime != ''">
produce_time = #{produceTime},
</if>
<if test="carType != null and carType != ''">
car_type = #{carType},
</if>
</set>
where
id = #{id}
</update>
choose 标签
choose、when、otherwise 这三个标签是一起使用的。类似于 if else 分支,也就是说,只有一个分支执行。
需求:先根据条件1查,如果没有提供条件1,则根据条件2查…
<!--
使用choose标签进行多分支查询
注意此时我们不再需要用and来连接多个条件
因为我们是多分支查询,一次只执行一个分支
不存在多条件合并,所以不需要使用and或者or
-->
<select id="selectByChoose" resultType="Car">
select
*
from
t_car
<where>
<choose>
<when test="brand != null and brand != ''">
brand like '%${brand}%'
</when>
<when test="guidePrice != null">
guide_price = #{guidePrice}
</when>
<otherwise>
car_type = #{carType}
</otherwise>
</choose>
</where>
</select>
foreach 批量删除
//使用数组的形式入参,记得加上@Param
int deleteByIds(@Param("ids") Integer[] ids);
<!--
collection 指定数组或者集合
item 代表数组或集合中的元素
separator 循环之间的分隔符
open 以什么字符开始
close 以什么字符结束
-->
<delete id="deleteByIds">
delete from t_car where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</delete>
<delete id="deleteByIds">
delete from t_car
<where>
<foreach collection="ids" item="id" separator="or">
id = #{id}
</foreach>
</where>
</delete>
foreach 批量插入
<!-- 利用foreach遍历集合,增加数据 -->
<insert id="insertBatch">
insert into
t_car
values
<foreach collection="cars" item="car" separator=",">
(null, #{car.carNum}, #{car.brand}, #{car.guidePrice}, #{car.produceTime}, #{car.carType})
</foreach>
</insert>
sql 和 include 标签
sql 标签用来声明 sql 片段,include 标签用来将声明的 sql 片段包含到某个 sql 语句中。可以让我们的代码复用性增强,易维护。
<!-- 抽取出相同sql代码片段 -->
<sql id="carColumnNameSql">
id,
car_num carNum,
brand,
guide_price guidePrice,
produce_time produceTime,
car_type carType
</sql>
<!-- 使用include标签使用重复代码 -->
<select id="selectAll" resultType="Map">
select
<include refid="carColumnNameSql"/>
from t_car;
</select>
高级映射及延迟加载
在之前的学习中,数据库的一张表和一个 POJO 对象进行映射,一对一的映射我们称为基本映射。所谓高级映射指的是存在多张表并且表与表之间有关系的话,我们需要进行高级映射(解决多表连结查询问题的)。
我们接下来来研究多对一和一对多怎么进行映射。如果是多对一,则多的是主表,反之一是主表。
多对一映射
例如有一张学生表(Student)和一张课程表(Clazz),学生表当中的课程编号是外键,同时是课程表的主键。这个时候,多对一指的是学生表对课程表。我们通过学生表当中的课程编号可以找到对应的课程类对象,那么我们应该在 Student 类当中添加一个 Clazz 属性,作为内部类。
多对一的关系,常见有三种处理方式:
一条 sql 语句,级联属性映射。
<!-- property是POJO对象的属性名,column是查询出来的字段名 --> <resultMap id="studentResultMap" type="Student"> <id property="sid" column="sid"/> <result property="sname" column="sname"/> <result property="clazz.cid" column="cid"/> <result property="clazz.cname" column="cname"/> </resultMap> <!-- 利用级联查询进行多表联查 --> <select id="selectById" resultMap="studentResultMap"> select s.sid, s.sname, c.cid, c.cname from t_stu s join t_clazz c on s.cid = c.cid where s.sid = #{sid} </select>
一条 sql 语句,association。
<resultMap id="studentResultMapAssociation" type="Student"> <id property="sid" column="sid"/> <result property="sname" column="sname"/> <!-- association-关联,表示一个Student对象关联一个Clazz对象 --> <!-- property提供要映射的POJO类的属性名,javaType提供要映射的类型 --> <association property="clazz" javaType="Clazz"> <id property="cid" column="cid"/> <result property="cname" column="cname"/> </association> </resultMap> <select id="selectByIdAssociation" resultMap="studentResultMapAssociation"> select s.sid, s.sname, c.cid, c.cname from t_stu s join t_clazz c on s.cid = c.cid where s.sid = #{sid} </select>
两条 sql 语句,分步查询。(这种方式常用,优点一是可复用,二是支持懒加载)
<!-- ClazzMapper当中的sql语句 --> <!-- 分步查询第二步,根据cid查询课程信息 --> <select id="selectByIdStep2" resultType="Clazz"> select * from t_clazz where cid = #{cid} </select> <!-- StudentMapper当中的sql语句 --> <!-- 两条sql语句完成多对一的分步查询 --> <resultMap id="studentResultMapByStep" type="Student"> <id property="sid" column="sid"/> <result property="sname" column="sname"/> <!-- 这里需要指定第二步要执行的sql语句的id select放入要执行的第二步的sql语句 column放入要传入到sql语句的参数 --> <association property="clazz" column="cid" select="cn.hnu.mybatis.mapper.ClazzMapper.selectByIdStep2"/> </resultMap> <!-- 分步查询第一步,先查学生课程cid --> <select id="selectByIdStep1" resultMap="studentResultMapByStep"> select sid, sname, cid from t_stu where sid = #{sid}; </select>
多对一映射延迟加载
延迟加载,又叫懒加载。核心原理便是:用到的时候再加载。在这里便是用到的时候再执行查询语句,不用的时候不查,可以提升性能。MyBatis 中开启懒加载的方式:
<association property="clazz"
column="cid"
select="cn.hnu.mybatis.mapper.ClazzMapper.selectByIdStep2"
fetchType="lazy"/>
<!-- 利用fetchType="lazy"去开启懒加载 -->
当然,上述的设置只是局部的懒加载设置,只对当前 association 关联的 sql 语句起作用。我们可以设置全局的懒加载方式,在mybatis-config
中配置:
<settings>
<!-- 所有但凡只要带了分步的,都采用懒加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
<!-- 如果有特殊需求,不需要延迟加载,则加上fetchType="eager"即可-->
<association property="clazz"
column="cid"
select="cn.hnu.mybatis.mapper.ClazzMapper.selectByIdStep2"
fetchType="eager"/>
一对多映射
现在我们把课程表当作主表,很显然,一个班级对应多个学生,一对多。我们可以在 Clazz 类中添加一个 List 集合(或者数组)来存储多个 Student。
一对多的关系,通常有两种处理方式:
collection。
<resultMap id="clazzResultMap" type="Clazz"> <id property="cid" column="cid"/> <result property="cname" column="cname"/> <!-- 一对多使用collection,ofType当中写入集合当中的元素类型 --> <collection property="stus" ofType="Student"> <id property="sid" column="sid"/> <result property="sname" column="sname"/> <!-- 这里我们不能再给Student里面的Clazz赋值了,否则会导致赋值递归 --> </collection> </resultMap> <select id="selectByCollection" resultMap="clazzResultMap"> select c.cid, c.cname, s.sid, s.sname from t_clazz c join t_stu s on c.cid = s.cid where c.cid = #{cid} </select>
分步查询。(常用)
<!-- StudentMapper当中的sql语句 --> <!-- 分步查询第二步,根据cid查询学生信息集合 --> <select id="selectByCid" resultType="Student"> select * from t_stu where cid = #{cid} </select> <!-- ClazzMapper当中的sql语句 --> <!-- 分步查询第一步,根据Clazz的cid获取班级信息--> <resultMap id="clazzResultMapStep" type="Clazz"> <id property="cid" column="cid"/> <result property="cname" column="cname"/> <collection property="stus" select="cn.hnu.mybatis.mapper.StudentMapper.selectByCid" column="cid"/> </resultMap> <select id="selectByStep1" resultMap="clazzResultMapStep"> select * from t_clazz where cid = #{cid} </select>
一对多映射延迟加载
同多对一映射的延迟加载,这里不再赘述。
MyBatis 缓存
缓存(cache)是我们开发当中一种常用的优化程序的重要手段,常见的缓存技术有:
- 字符串常量池。
- 整数型常量池。
- 线程池。
- 数据库连接池。
我们知道,计算机中的内存是用来临时存储数据的,这些数据会在断电后消失。而数据存放在磁盘当中,才是持久化存储。我们知道,数据库的数据存放在文件中,而文件存放在硬盘中,意味着我们如果要和数据库的数据进行交互,需要进行 IO 操作,IO 操作很耗时间。
如果是同一条 sql 语句执行多次,每一次都去连接数据库进行查询,那么效率实在太慢了。MyBatis 存在缓存机制,当我们执行 DQL 语句的时候,将查询结果放入缓存当中,如果下一次还是执行完全相同的 DQL 语句,如果结果相同的话,直接从缓存中拿去数据,减少和磁盘的 IO 操作,加快查询效率。
MyBatis 的缓存包括:
- 一级缓存:将查询的数据存放到 SqlSession 中(代表了一个 sql 会话)。
- 二级缓存:将查询的数据存放到 SqlSessionFactory 中(代表了整个数据库)。
- 集成第三方缓存:EhCache、Memcache。
缓存只针对 DQL 语句,也就是说缓存机制只对应 select 语句。
一级缓存
一级缓存默认是开启的,不需要任何配置。
只要是使用同一个 SqlSession 对象执行同一条 Sql 语句,就会走缓存。
//以下代码只会执行一次sql语句
@Test
public void testSelectById() {
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper carMapper = sqlSession.getMapper(CarMapper.class);
//mapper第一次调用
Car car1 = carMapper.selectById(1);
System.out.println(car1);
//mapper第二次调用
Car car2 = carMapper.selectById(1);
System.out.println(car2);
//SqlSession调用完毕
sqlSession.close();
}
不走缓存的两种情况:
- SqlSession 对象不是同一个,不走缓存。
- 两次查询的对象不一样,不走缓存。
第一次 DQL 和第二次 DQL 之间我们进行了以下操作都会导致缓存失效:
- 调用了 SqlSession 的
clearCache()
方法,手动清空缓存。 - 执行了 insert、delete、update 语句(和表没有关系,即操作 A 表也会清空 B 表的缓存)。
二级缓存
二级缓存的范围更大,属于整个数据库范围的,因为缓存是针对于 SqlSessionFactory 的。
使用二级缓存需要具备以下条件:
<settingname="cacheEnabled"value="true">
全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。默认就是 true,无需设置。- 需要使用二级缓存的 SqlMapper.xml 文件中添加配置:
<cache/>
。 - 使用二级缓存的实体类对象必须是可序列化的,也就是必须实现
java.io.Serializable
接口。 - SqlSession 对象关闭或提交之后,一级缓存中的数据才会被写入到二级缓存当中。此时二级缓存才可用。
@Test
public void testSelectById2() throws IOException {
//二级缓存对应的就是一个SqlSessionFactory对象
SqlSessionFactory factory = new SqlSessionFactoryBuilder()
.build(Resources.getResourceAsStream("mybatis-config.xml"));
//创建两个sqlSession
SqlSession sqlSession1 = factory.openSession();
SqlSession sqlSession2 = factory.openSession();
//获取两个mapper
CarMapper mapper1 = sqlSession1.getMapper(CarMapper.class);
CarMapper mapper2 = sqlSession2.getMapper(CarMapper.class);
//在这里我们不关闭SqlSession,此时二级缓存中还是没有数据的
Car car1 = mapper1.selectById2(1); //这行代码执行后,数据被放入一级缓存中
System.out.println(car1);
Car car2 = mapper2.selectById2(1); //两个一级缓存是不一致的,因为是两个SqlSession
System.out.println(car2);
//关闭SqlSession,这个时候才将一级缓存放入二级缓存中
sqlSession1.close();
sqlSession2.close();
//从二级缓存拿取数据
Car car3 = factory.openSession()
.getMapper(CarMapper.class)
.selectById2(1);
System.out.println(car3);
}
只要两次查询之间进行了增、删、改操作,二级缓存就会失效(一级缓存也会失效)。
二级缓存相关配置:
- eviction:从缓存中移除某个对象的淘汰算法。默认采用 LRU 策略。
- LRU:Least Recently Used。最近最少使用,优先淘汰在间隔时间内使用频率最低的对象。(其实还有一种淘汰算法LFU,最不常用。)
- FIFO:First In First Out。一种先进先出的数据缓存器。先进入二级缓存的对象最先被淘汰。
- SOFT:软引用,淘汰软引用指向的对象。具体算法和 JVM 的垃圾回收算法有关。
- WEAK:弱引用,淘汰弱引用指向的对象。具体算法和 JVM 的垃圾回收算法有关。
- flushInterval:二级缓存的刷新时间间隔,单位毫秒。如果没有设置,就代表不刷新缓存,只要内存足够大,一直会向二级缓存中缓存数据,除非执行了增删改。
- readOnly:
- true:多条相同的 sql 语句执行之后返回的对象是共享的同一个。性能好,但是多线程并发可能会存在安全问题。
- false:多条相同的 sql 语句执行之后返回的对象是副本,调用了 clone 方法。性能一般,但安全。
- size:设置二级缓存中最多可存储的 Java 对象数量,默认值是 1024。
MyBatis 集成 EhCache
集成第三方二级缓存只能替代 MyBatis 的二级缓存,一级缓存试没办法替代的。因为 EhCache 是使用 Java 语言写的,故 MyBatis 集成 EhCache 比较常见。
首先引入 Ehcache 依赖:
<!-- 引入ehcache依赖 -->
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.2.3</version>
</dependency>
在类路径下配置echcache.xml
文件:
<?xml version="1.0" encoding="UTF-8" ?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<!--磁盘存储:将缓存中暂时不使用的对象,转移到硬盘,类似于windows系统的虚拟内存-->
<diskStore path="d:/ehcache"/>
<defaultCache eternal="false" maxElementsInMemory="1000" overflowToDisk="false" diskPersistent="false"
timeToIdleSeconds="0" timeToLiveSeconds="600" memoryStoreEvictionPolicy="LRU"/>
</ehcache>
MyBatis 逆向工程
所谓的逆向工程是:根据数据库表逆向生成 Java 的 pojo 类,SqlMapper.xml 文件,以及 Mapper 接口类等。
要完成这个工作,我们可以借助别人写好的逆向工程插件。
使用这个插件的话,我们需要配置好以下信息:
- POJO 类名,包名以及生成位置。
- SqlMapper.xml 文件名以及生成位置。
- Mapper 接口名以及生成位置。
- 连接数据库的信息。
- 指定哪些表参与逆向工程。
逆向工程的配置与生成
在pom.xml
文件中配置:
<!-- 配置逆向工程插件 -->
<!-- 定制构建过程 -->
<build>
<!-- 可配置多个插件 -->
<plugins>
<!-- 其中的一个插件,MyBatis的逆向工程插件 -->
<plugin>
<!-- 插件的GAV坐标 -->
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.4.2</version>
<!-- 允许覆盖 -->
<configuration>
<overwrite>true</overwrite>
</configuration>
<!-- 插件的依赖 -->
<dependencies>
<!-- mysql驱动依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
在类的根路径下创建配置文件generatorConfig.xml
:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<!--
targetRuntime有两个值:
MyBatis3Simple: 生成的是基础版,只有基本的增删改查
MyBatis3: 生成的是增强版,除了基本的增删改查之外还有复杂的增删改查
-->
<context id="DB2Tables" targetRuntime="MyBatis3">
<!-- 防止生成重复代码 -->
<plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin"/>
<commentGenerator>
<!-- 是否去掉生成日期 -->
<property name="suppressDate" value="true"/>
<!-- 是否去除注释 -->
<property name="suppressAllComments" value="true"/>
</commentGenerator>
<!-- 连接数据库 -->
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/test"
userId="root"
password="MySQL:040809">
</jdbcConnection>
<!-- 生成pojo包名和位置 -->
<javaModelGenerator targetPackage="cn.hnu.mybatis.pojo" targetProject="src/main/java">
<!-- 是否开启子包 -->
<property name="enableSubPackages" value="true"/>
<!-- 是否去除字段名的前后空白 -->
<property name="trimStrings" value="true"/>
</javaModelGenerator>
<!-- 生成SQL映射文件的包名和位置 -->
<sqlMapGenerator targetPackage="cn.hnu.mybatis.mapper" targetProject="src/main/resources">
<!-- 是否开启子包 -->
<property name="enableSubPackages" value="true"/>
</sqlMapGenerator>
<!-- 生成Mapper接口的包名和位置 -->
<javaClientGenerator
type="xmlMapper"
targetPackage="cn.hnu.mybatis.mapper"
targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
</javaClientGenerator>
<!-- 表名和对应的实体类 -->
<table tableName="t_car" domainObjectName="Car"/>
</context>
</generatorConfiguration>
QBC 查询风格
上述如果我们选择增强版,则会生成的 Example 类,这个类是用来封装查询条件的。
QBC 查询风格便是面向对象的一种查询风格,看不到 SQL 语句,而是使用对象的创建来代替 SQL 语句。
@Test
public void testSelect() {
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper carMapper = sqlSession.getMapper(CarMapper.class);
//查询1个
Car car = carMapper.selectByPrimaryKey(25);
System.out.println(car);
//根据条件查询,如果是null,则查所有
List<Car> list = carMapper.selectByExample(null);
list.forEach(System.out::println);
//按照条件查询
//先封装查询条件
CarExample carExample = new CarExample();
//调用createCriteria创建查询条件
//添加and条件
carExample.createCriteria()
.andBrandLike("凯美瑞") //brand like '凯美瑞'
.andGuidePriceGreaterThan(new BigDecimal("20.00")); //guide_price > 20.00
//添加or条件
carExample.or()
.andCarTypeEqualTo("燃油车"); //car_type = '燃油车'
//执行查询
List<Car> cars = carMapper.selectByExample(carExample);
cars.forEach(System.out::println);
sqlSession.close();
}
PageHelper
标准的通用 MySql 分页代码:
select
*
from
tableName
limit
(pageNum - 1) * pageSize, pageSize
每次分页前端要给后端传递两个参数,一个是pageNum
,另一个是pageSize
。
但是我们还要考虑到,什么时候允许用户查看上一页,什么时候允许用户查看下一页等问题,比较麻烦,所以我们利用 PageHelper 来帮助我们实现这些功能。
使用 PageHelper
引入依赖:
<!-- 引入pageHelper -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.3.2</version>
</dependency>
在mybatis-config.xml
添加拦截器:
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
</plugins>
在使用的时候,sql 语句直接进行查询即可:
<!-- 在使用插件的情况下,直接select * 即可,并且语句末尾不能加分号 -->
<select id="selectAll" resultType="Car">
select
*
from
t_car
</select>
@Test
public void testSelectAll() {
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper carMapper = sqlSession.getMapper(CarMapper.class);
//在执行DQL语句之前,开启分页功能
int pageNum = 1;
int pageSize = 2;
PageHelper.startPage(pageNum, pageSize);
//进行分页
List<Car> list = carMapper.selectAll();
list.forEach(System.out::println);
sqlSession.close();
}
获取 PageInfo 对象
PageInfo 是封装分页相关的信息的对象。我们获取 PageInfo 对象的目的是为了给前端传递数据,PageInfo 对象将来会存储到 reques 域中,在页面上展示。
@Test
public void testSelectAll() {
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper carMapper = sqlSession.getMapper(CarMapper.class);
//在执行DQL语句之前,开启分页功能
int pageNum = 1;
int pageSize = 2;
PageHelper.startPage(pageNum, pageSize);
//进行分页
List<Car> list = carMapper.selectAll();
//封装分页信息对象
//5是分页导航卡片数
PageInfo<Car> carPageInfo = new PageInfo<>(list, 5);
System.out.println(carPageInfo);
sqlSession.close();
}
注解式开发
在 MyBatis 中,简单的 sql 语句使用注解式开发,复杂的 sql 语句(包含各种标签)官方建议使用配置文件。
在实际中,我们通常注解和配置混合开发。
@Insert
public interface CarMapper {
@Insert("insert into " +
"t_car " +
"values " +
"(null, #{carNum}, #{brand}, #{guidePrice}, #{produceTime}, #{carType})")
int insert(Car car);
}
@Delete
public interface CarMapper {
@Delete("delete from t_car where id = #{id}")
int deleteById(Integer id);
}
@Update
public interface CarMapper {
@Update("update t_car set car_num = #{carNum} where id = #{id}")
int update(Car car);
}
@Select
public interface CarMapper {
@Select("select * from t_car where id = #{id}")
Car selectById(Integer id);
}
@Results
该注解是用来进行命名映射的。
public interface CarMapper {
@Select("select * from t_car where id = #{id}")
@Results(id = "selectByIdMap", value = {
@Result(id = true, property = "id", column = "id"),
@Result(property = "carNum", column = "car_num"),
@Result(property = "brand", column = "brand"),
@Result(property = "guidePrice", column = "guide_price"),
@Result(property = "produceTime", column = "produce_time"),
@Result(property = "carType", column = "car_type"),
})
Car selectById(Integer id);
@Select("select * from t_car where id = #{id}")
//使用ResultMap可以对之前的映射集进行复用
@ResultMap("selectByIdMap")
Car selectById2(Integer id);
}
dom4j 解析 XML 文件
引入依赖:
<dependencies>
<!-- 引入dom4j组件,解析xml -->
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.2.0</version>
</dependency>
<!-- 引入junit测试依赖 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
解析mybatis-config.xml
:
@Test
public void testParseMyBatisConfigXML() throws DocumentException {
//创建SAXReader对象解析对象
SAXReader reader = new SAXReader();
//从类根路径下读XML文件
InputStream is = ClassLoader
.getSystemClassLoader()
.getResourceAsStream("mybatis-config.xml");
Document document = reader.read(is);
//获取文档根标签
//Element rootElement = document.getRootElement();
//获取default默认的环境id
String xpath = "/configuration/environments"; //xpath是做标签路径匹配的,帮助我们快速定位元素
Element environments = (Element) document.selectSingleNode(xpath);
//获取属性的值
String defaultEnvironmentId = environments.attributeValue("default");
//获取具体环境environment
xpath = "/configuration/environments/environment[@id='" + defaultEnvironmentId + "']";
Element environment = (Element) document.selectSingleNode(xpath);
//获取environment内部的子标签,使用element获取孩子节点
Element transactionManager = environment.element("transactionManager");
String transactionType = transactionManager.attributeValue("type");
System.out.println("事务管理的类型: " + transactionType);
Element dataSource = environment.element("dataSource");
String dataSourceType = dataSource.attributeValue("type");
System.out.println("数据源的类型: " + dataSourceType);
//获取dataSource下的所有子节点
List<Element> propertyElements = dataSource.elements();
//获取键和值
propertyElements.forEach(ele -> {
String name = ele.attributeValue("name");
String value = ele.attributeValue("value");
System.out.println(name + "=" + value);
});
//获取所有mapper标签
//不想从根下获取,我们从任意位置获取,使用两个//
xpath = "//mapper";
List<Node> mappers = document.selectNodes(xpath);
mappers.forEach(ele -> {
Element mapperElement = (Element) ele;
String resource = mapperElement.attributeValue("resource");
System.out.println("resource: " + resource);
});
}
解析 Mapper 文件:
@Test
public void testParseSqlMapperXML() throws DocumentException {
SAXReader reader = new SAXReader();
InputStream is = ClassLoader
.getSystemClassLoader()
.getResourceAsStream("CarMapper.xml");
Document document = reader.read(is);
//获取namespace
String xpath = "/mapper";
Element mapper = (Element) document.selectSingleNode(xpath);
String namespace = mapper.attributeValue("namespace");
System.out.println("namespace: " + namespace);
//获取所有子标签
List<Element> elements = mapper.elements();
elements.forEach(ele -> {
//获取sql语句id
String id = ele.attributeValue("id");
System.out.println("id: " + id);
//获取resultType
String resultType = ele.attributeValue("resultType");
System.out.println("resultType: " + resultType);
//获取sql语句,获取标签中的文本内容并且去除前后空白
String sql = ele.getTextTrim();
System.out.println("sql: " + sql);
//将sql语句改成jdbc形式
String jdbcSql = sql.replaceAll("#\\{[^}]*}", "?");
System.out.println("jdbcSql: " + jdbcSql);
});
}