1 MyBatis概述
1.1 框架
● 在⽂献中看到的framework被翻译为框架
● Java常⽤框架:
○ SSM三⼤框架:Spring + SpringMVC + MyBatis
○ SpringBoot
○ SpringCloud
○ 等。。
● 框架其实就是对通用代码的封装,提前写好了⼀堆通用接口和类,我们可以在做项⽬的时候直接引⼊这些接⼝和类(引⼊框架),基于这些现有的接口和类进⾏开发,可以⼤⼤提⾼开发效率。
● 框架⼀般都以jar包的形式存在。(jar包中有class⽂件以及各种配置⽂件等。)
● SSM三⼤框架的学习顺序:
○ ⽅式⼀:MyBatis、Spring、SpringMVC(建议)
○ ⽅式⼆:Spring、MyBatis、SpringMVC
1.2 三层架构
● 表现层(UI):直接跟前端打交互(⼀是接收前端ajax请求,⼆是返回json数据给前端)
● 业务逻辑层(BLL):⼀是处理表现层转发过来的前端请求(也就是具体业务),⼆是将从持久层获取的数据返回到表现层。
● 数据访问层(DAL):直接操作数据库完成CRUD,并将获得的数据返回到上⼀层(也就是业务逻辑层)。
● Java持久层框架:
○ MyBatis
○ Hibernate(实现了JPA规范)
○ jOOQ
○ Guzz
○ Spring Data(实现了JPA规范)
○ ActiveJDBC
○ ......
1.3 JDBC不足
● 示例代码1:
// ......
// sql语句写死在java程序中
String sql = "insert into t_user(id,idCard,username,password,birth,gender,email,city,street,zipcode,phone,grade) values(?,?,?,?,?,?,?,?,?,?,?,?)";
PreparedStatement ps = conn.prepareStatement(sql);
// 繁琐的赋值:思考一下,这种有规律的代码能不能通过反射机制来做自动化。
ps.setString(1, "1");
ps.setString(2, "123456789");
ps.setString(3, "zhangsan");
ps.setString(4, "123456");
ps.setString(5, "1980-10-11");
ps.setString(6, "男");
ps.setString(7, "zhangsan@126.com");
ps.setString(8, "北京");
ps.setString(9, "大兴区凉水河二街");
ps.setString(10, "1000000");
ps.setString(11, "16398574152");
ps.setString(12, "A");
// 执行SQL
int count = ps.executeUpdate();
// ......
● 示例代码2:
// ......
// sql语句写死在java程序中
String sql = "select id,idCard,username,password,birth,gender,email,city,street,zipcode,phone,grade from t_user";
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery();
List<User> userList = new ArrayList<>();
// 思考以下循环中的所有代码是否可以使用反射进行自动化封装。
while(rs.next()){
// 获取数据
String id = rs.getString("id");
String idCard = rs.getString("idCard");
String username = rs.getString("username");
String password = rs.getString("password");
String birth = rs.getString("birth");
String gender = rs.getString("gender");
String email = rs.getString("email");
String city = rs.getString("city");
String street = rs.getString("street");
String zipcode = rs.getString("zipcode");
String phone = rs.getString("phone");
String grade = rs.getString("grade");
// 创建对象
User user = new User();
// 给对象属性赋值
user.setId(id);
user.setIdCard(idCard);
user.setUsername(username);
user.setPassword(password);
user.setBirth(birth);
user.setGender(gender);
user.setEmail(email);
user.setCity(city);
user.setStreet(street);
user.setZipcode(zipcode);
user.setPhone(phone);
user.setGrade(grade);
// 添加到集合
userList.add(user);
}
// ......
● JDBC不足:
○ SQL语句写死在Java程序中,不灵活。改SQL的话就要改Java代码。违背开闭原则OCP。
○ 给?传值是繁琐的。能不能⾃动化???
○ 将结果集封装成Java对象是繁琐的。能不能⾃动化???
1.4 了解MyBatis
● MyBatis本质上就是对JDBC的封装,通过MyBatis完成CRUD。
● MyBatis在三层架构中负责持久层的,属于持久层框架。
● 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代码可以看到它的包结构中包含:ibatis
● ORM:对象关系映射
○ O(Object):Java虚拟机中的Java对象
○ R(Relational):关系型数据库
○ M(Mapping):将Java虚拟机中的Java对象映射到数据库表中⼀⾏记录,或是将数据库表中
⼀⾏记录映射成Java虚拟机中的⼀个Java对象。
○ ORM图示
○ MyBatis属于半⾃动化ORM框架。
○ Hibernate属于全⾃动化的ORM框架。
001-ORM思想-对象关系映射
● MyBatis框架特点:
○ ⽀持定制化 SQL、存储过程、基本映射以及⾼级映射
○ 避免了⼏乎所有的 JDBC 代码中手动设置参数以及获取结果集
○ ⽀持XML开发,也⽀持注解式开发。【为了保证sql语句的灵活,所以mybatis⼤部分是采⽤
XML⽅式开发。】
○ 将接⼝和 Java 的 POJOs(Plain Ordinary Java Object,简单普通的Java对象)映射成数据库中的
记录
○ 体积小好学:两个jar包(Mybatis的核心jar包和连接数据库的驱动),两个XML配置⽂件。
○ 完全做到sql解耦合。
○ 提供了基本映射标签。
○ 提供了⾼级映射标签。
○ 提供了XML标签,⽀持动态SQL的编写。
○ ......
2 MyBatis入门程序
只要你会JDBC,MyBatis就可以学。
2.1 版本
2.1.1 软件版本
● IntelliJ IDEA:2022.1.4
● Navicat for MySQL:16.0.14
● MySQL数据库:8.0.30
2.1.2 组件版本
● MySQL驱动:8.0.30
● MyBatis:3.5.10
● JDK:Java17
● JUnit:4.13.2
● Logback:1.2.11
2.2 MyBatis下载
● 从github上下载,地址:https://github.com/mybatis/mybatis-3
● 将框架以及框架的源码都下载下来,下载框架后解压,打开mybatis⽬录
○ 通过以上解压可以看到,框架⼀般都是以jar包的形式存在。我们的mybatis课程使⽤maven,所
以这个jar我们不需要。
○ 官方手册需要。
2.3 MyBatis入门程序开发步骤
● 写代码前准备:
○ 准备数据库表:汽⻋表t_car,字段包括:
■ id:主键(⾃增)【bigint】
■ car_num:汽⻋编号【varchar】
■ brand:品牌【varchar】
■ guide_price:⼚家指导价【decimal类型,专⻔为财务数据准备的类型】
■ produce_time:⽣产时间【char,年⽉⽇即可,10个⻓度,'2022-10-11'】
■ car_type:汽⻋类型(燃油⻋、电⻋、氢能源)【varchar】
○ 使⽤navicat for mysql⼯具建表
○ 使⽤navicat for mysql⼯具向t_car表中插⼊两条数据,如下:
○ 创建Project:建议创建Empty Project,设置Java版本以及编译版本等。
○ 设置IDEA的maven
○ 创建Module:普通的Maven Java模块
● 步骤1:打包⽅式:jar(不需要war,因为mybatis封装的是jdbc。)
<groupId>com.powernode</groupId>
<artifactId>mybatis-001-introduction</artifactId>
<version>1.0-SNAPSHOT</version>
<!--打包方式jar-->
<packaging>jar</packaging>
● 步骤2:引⼊依赖(mybatis依赖 + mysql驱动依赖)
<!--mybatis依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.10</version>
</dependency>
<!--mysql驱动依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
● 步骤3:在resources根⽬录下新建mybatis-config.xml配置⽂件(可以参考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">
<configuration>
<!--开启mybatis对标准日志的实现。-->
<!--<settings>-->
<!--<setting name="logImpl" value="STDOUT_LOGGING"/>-->
<!--<setting name="logImpl" value="SLF4J"/>-->
<!--</settings>-->
<environments default="development">
<environment id="development">
<!--<transactionManager type="MANAGED"/>-->
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/powernode_mybatis"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<!--执行XxxMapper.xml文件的路径-->
<!--resource属性自动会从类的根路径下开始查找资源。-->
<mapper resource="CarMapper.xml"/>
<!--resource属性:从类路径当中加载资源。-->
<!--<mapper resource="com/CarMapper2.xml"/>-->
<!--url属性:从绝对路径当中加载资源。-->
<!--语法格式:file:///绝对路径-->
<!--不建议:因为移植性差。-->
<!--<mapper url="file:///d:/CarMapper.xml"/>-->
</mappers>
</configuration>
注意1:mybatis核心配置⽂件的⽂件名不⼀定是mybatis-config.xml,可以是其它名字。
注意2:mybatis核心配置⽂件存放的位置也可以随意。这⾥选择放在resources根下,相当于放到了类的根路径下。
● 步骤4:在resources根目录下新建CarMapper.xml配置⽂件(可以参考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="fdsafdsa">
<!--insert语句,id是这条SQL语句的唯一标识。这个id就代表了这条SQL语句。-->
<insert id="insertCar">
insert into powernode_mybatis.t_car(id,car_num,brand,guide_price,produce_time,car_type)
values(null,'1003','丰田霸道',30.0,'2000-10-11','燃油车');
</insert>
</mapper>
注意1:sql语句最后结尾可以不写“;”
注意2:CarMapper.xml⽂件的名字不是固定的。可以使⽤其它名字。
注意3:CarMapper.xml⽂件的位置也是随意的。这⾥选择放在resources根下,相当于放到了类的根路 径下。
注意4:将CarMapper.xml⽂件路径配置到mybatis-config.xml:
<mappers>
<!--执行XxxMapper.xml文件的路径-->
<!--resource属性自动会从类的根路径下开始查找资源。-->
<mapper resource="CarMapper.xml"/>
<!--resource属性:从类路径当中加载资源。-->
<!--<mapper resource="com/CarMapper2.xml"/>-->
<!--url属性:从绝对路径当中加载资源。-->
<!--语法格式:file:///绝对路径-->
<!--不建议:因为移植性差。-->
<!--<mapper url="file:///d:/CarMapper.xml"/>-->
</mappers>
● 步骤5:编写MyBatisIntroductionTest代码
package com.powernode.mybatis.test;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.FileInputStream;
import java.io.InputStream;
public class MyBatisIntroductionTest {
public static void main(String[] args) throws Exception {
// 1.获取SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 2.获取SqlSessionFactory对象
InputStream is = Resources.getResourceAsStream("mybatis-config.xml"); // Resources.getResourceAsStream默认就是从类的根路径下开始查找资源。
//InputStream is = Resources.getResourceAsStream("com/mybatis.xml");
//InputStream is = new FileInputStream("d:\\mybatis-config.xml");
//通过系统类加载器获取资源
//InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is); // 一般情况下都是一个数据库对应一个SqlSessionFactory对象。
// 3.获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession(); // 如果使用的事务管理器是JDBC的话,底层实际上会执行:conn.setAutoCommit(false);
// 这种方式实际上是不建议的,因为没有开启事务。
//SqlSession sqlSession = sqlSessionFactory.openSession(true);
// 4.执行SQL语句
int count = sqlSession.insert("insertCar"); // 返回值是影响数据库表当中的记录条数。
System.out.println("插入了几条记录:" + count);
// 5.手动提交
sqlSession.commit(); // 如果使用的事务管理器是JDBC的话,底层实际上还是会执行conn.commit();
//6.关闭资源(只关闭是不会提交的)
sqlSession.close();
}
}
注意1:默认采⽤的事务管理器是:JDBC。JDBC事务默认是不提交的,需要⼿动提交。
● 步骤6:运⾏程序,查看运⾏结果,以及数据库表中的数据
2.4 关于MyBatis核心配置文件的名字和路径详解
核心配置⽂件的名字是随意的,因为以下的代码:
// 文件名是出现在程序中的,文件名如果修改了,对应这里的java程序也改一下就行了。
InputStream is = Thread.currentThread().getContextClassLoader().
getResourceAsStream("mybatis-config.xml");
核心配置⽂件必须放到resources下吗?放到D盘根目录下,可以吗?测试⼀下:
将mybatis-config.xml⽂件拷⻉⼀份放到D盘根下,然后编写以下程序:
package com.powernode.mybatis;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.FileInputStream;
import java.io.InputStream;
public class MyBatisConfigFilePath {
public static void main(String[] args) throws Exception{
// 1. 创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 2. 创建SqlSessionFactory对象
// 这只是一个输入流,可以自己new。
InputStream is = new FileInputStream("D:/mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
// 3. 创建SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
// 4. 执行sql
int count = sqlSession.insert("insertCar");
System.out.println("插入几条数据:" + count);
// 5. 提交(mybatis默认采用的事务管理器是JDBC,默认是不提交的,需要手动提交。)
sqlSession.commit();
// 6. 关闭资源(只关闭是不会提交的)
sqlSession.close();
}
}
以上程序运⾏后,看到数据库表t_car中⼜新增⼀条数据,如下(成功了):
经过测试说明mybatis核⼼配置⽂件的名字是随意的,存放路径也是随意的。
虽然mybatis核⼼配置⽂件的名字不是固定的,但通常该⽂件的名字叫做:mybatis-config.xml
虽然mybatis核⼼配置⽂件的路径不是固定的,但通常该⽂件会存放到类路径当中,这样让项⽬的移植更 加健壮。
● 在mybatis中提供了⼀个类:Resources【org.apache.ibatis.io.Resources】,该类可以从类路径当中获取资源,我们通常使⽤它来获取输⼊流InputStream,代码如下
// 这种方式只能从类路径当中获取资源,也就是说mybatis-config.xml文件需要在类路径下。
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
2.5 MyBatis第一个比较完整的代码写法
package com.powernode.mybatis.test;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
/**
* 采用正规的方式,写一个完整版的MyBatis程序。
* @author 动力节点
* @version 1.0
* @since 1.0
*/
public class MyBatisCompleteTest {
public static void main(String[] args) {
SqlSession sqlSession = null;
try {
// 1.创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 2.创建SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
// 3.创建SqlSession对象 开启会话(底层会开启事务)
sqlSession = sqlSessionFactory.openSession();
// 4.执行SQL语句,处理相关业务
int count = sqlSession.insert("insertCar");
System.out.println(count);
// 5.执行到这里,没有发生任何异常,提交事务。终止事务。
sqlSession.commit();
} catch (Exception e) {
// 最好回滚事务
if (sqlSession != null) {
sqlSession.rollback();
}
e.printStackTrace();
} finally {
// 6.关闭会话(释放资源)
if (sqlSession != null) {
sqlSession.close();
}
}
}
}
运行后数据库表的变化:
2.6 引入JUnit
● JUnit是专门做单元测试的组件。
○ 在实际开发中,单元测试⼀般是由我们Java程序员来完成的。
○ 我们要对我们自己写的每⼀个业务方法负责任,要保证每个业务方法在进行测试的时候都能通
过。
○ 测试的过程中涉及到两个概念:
■ 期望值
■ 实际值
○ 期望值和实际值相同表示测试通过,期望值和实际值不同则单元测试执行时会报错。
● 这里引⼊JUnit是为了代替main⽅法。
● 使用JUnit步骤:
○ 第⼀步:引⼊依赖
<!--junit依赖-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
○ 第⼆步:编写单元测试类【测试⽤例】,测试用例中每⼀个测试方法上使用@Test注解进行标注。
■ 测试用例的名字以及每个测试方法的定义都是有规范的:
● 测试用例的名字:XxxTest
● 测试方法声明格式:public void test业务方法名(){}
// 测试用例
public class CarMapperTest{
// 测试方法
@Test
public void testInsert(){}
@Test
public void testUpdate(){}
}
第三步:可以在类上执行,也可以在方法上执行
在类上执行时,该类中所有的测试方法都会执行
在方法上执行时,只执行当前的测试方法。
● 编写⼀个测试用例,来测试业务方法
package com.powernode.mybatis;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;
public class CarMapperTest {
@Test
public void testInsertCar(){
SqlSession sqlSession = null;
try {
// 1.创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 2.创建SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
// 3.创建SqlSession对象
sqlSession = sqlSessionFactory.openSession();
// 4.执行SQL
int count = sqlSession.insert("insertCar");
System.out.println("更新了几条记录:" + count);
// 5.提交
sqlSession.commit();
} catch (Exception e) {
// 回滚
if (sqlSession != null) {
sqlSession.rollback();
}
e.printStackTrace();
} finally {
// 6.关闭
if (sqlSession != null) {
sqlSession.close();
}
}
}
}
执行单元测试,查看数据库表的变化:
junit-test:
package com.powernode.junit.service;
/**
* 数学业务类
* @author 动力节点
* @version 1.0
* @since 1.0
*/
public class MathService {
/**
* 求和的业务方法
* @param a
* @param b
* @return
*/
public int sum(int a, int b){
//return a +b;
return a * 10 + b * 10;
}
/**
* 相减的业务方法
* @param a
* @param b
* @return
*/
public int sub(int a, int b){
return a - b;
}
}
package com.powernode.junit.service;
import org.junit.Assert;
import org.junit.Test;
/**
* 单元测试类
* @author 动力节点
* @version 1.0
* @since 1.0
*/
public class MathServiceTest { // 名字规范:你要测试的类名+Test
// 单元测试方法写多少个。
// 一般是一个业务方法对应一个测试方式。
// 测试方法的规范: public void testXxxx(){}
// 测试方法的方法名:以test开始。假设测试的方法是sum,这个测试方法名:testSum
// @Test注解非常重要,被这个注解标注的方法就是一个单元测试方法。
@Test
public void testSum(){
// 单元测试中有两个重要的概念:
// 一个是:实际值(被测试的业务方法的真正执行结果)
// 一个是:期望值(执行了这个业务方法之后,你期望的执行结果是多少)
MathService mathService = new MathService();
// 获取实际值
int actual = mathService.sum(1, 2);
// 期望值
//int expected = 3;
int expected = 30;
// 加断言进行测试
Assert.assertEquals(expected, actual);
}
@Test
public void testSub(){
MathService mathService = new MathService();
// 实际值
int actual = mathService.sub(10, 5);
// 期望值
int expected = 5;
// 添加断言机制
Assert.assertEquals(expected, actual);
}
}
2.7 引入日志框架logback
引入日志框架的目的是为了看清楚mybatis执行的具体sql。
启用标准日志组件,只需要在mybatis-config.xml文件中添加以下配置:【可参考mybatis⼿册】
<!--开启mybatis对标准日志的实现(mybatis内部内置的标准日志实现)。-->
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
标准日志也可以用,但是配置不够灵活,可以集成其他的日志组件,例如:log4j,logback等。
logback是目前日志框架中性能较好的,较流行的,所以我们选它。
引⼊logback的步骤:
第⼀步:引⼊logback相关依赖
<!--引入logback的依赖,这个日志框架实现了slf4j规范-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
</dependency>
第⼆步:引⼊logback相关配置⽂件(文件名叫做logback.xml或logback-test.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">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<!-- 按照每天生成日志文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名-->
<FileNamePattern>${LOG_HOME}/TestWeb.log.%d{yyyy-MM-dd}.log</FileNamePattern>
<!--日志文件保留天数-->
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
<!--日志文件最大的大小-->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>100MB</MaxFileSize>
</triggeringPolicy>
</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>
再次执行单元测试方法testInsertCar,查看控制台是否有sql语句输出
2.8 MyBatis工具类SqlSessionUtil的封装
每一次获取SqlSession对象代码太繁琐,封装一个工具类
package com.powernode.mybatis.utils;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
/**
* MyBatis工具类
* @author 动力节点
* @version 1.0
* @since 1.0
*/
public class SqlSessionUtil {
// 工具类的构造方法一般都是私有化的。
// 工具类中所有的方法都是静态的,直接采用类名即可调用。不需要new对象。
// 为了防止new对象,构造方法私有化。
private SqlSessionUtil(){}
private static SqlSessionFactory sqlSessionFactory;
// 类加载时执行
// SqlSessionUtil工具类在进行第一次加载的时候,解析mybatis-config.xml文件。创建SqlSessionFactory对象。
static {
try {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/*public static SqlSession openSession(){
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// SqlSessionFactory对象:一个SqlSessionFactory对应一个environment,一个environment通常是一个数据库。
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
SqlSession sqlSession = sqlSessionFactory.openSession();
return sqlSession;
}*/
/**
* 每调用一次openSession()可获取一个新的会话,该会话支持自动提交。
* @return 会话对象
*/
public static SqlSession openSession(){
return sqlSessionFactory.openSession();
}
}
测试工具类,将testInsertCar()改造
@Test
public void testInsertCarByUtil(){
SqlSession sqlSession = SqlSessionUtil.openSession();
int count = sqlSession.insert("insertCar");
System.out.println(count);
sqlSession.commit();
sqlSession.close();
}
2.9 课堂笔记(mybatis-001-introduction)
开发我的第一个MyBatis程序
2.9.1 resources目录
放在这个目录当中的,一般都是资源文件,配置文件。直接放到resources目录下的资源,等同于放到了类的根路径下。
2.9.2 开发步骤
* 第一步:打包方式ja
* 第二步:引入依赖
- mybatis依赖
- mysql驱动依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.powernode</groupId>
<artifactId>mybatis-001-introduction</artifactId>
<version>1.0-SNAPSHOT</version>
<!--打包方式jar-->
<packaging>jar</packaging>
<dependencies>
<!--mybatis依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.10</version>
</dependency>
<!--mysql驱动依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<!--junit依赖-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<!--引入logback的依赖,这个日志框架实现了slf4j规范-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>
* 第三步:编写mybatis核心配置文件:mybatis-config.xml
注意:
第一:这个文件名不是必须叫做mybatis-config.xml,可以用其他的名字。只是大家都采用这个名字。
第二:这个文件存放的位置也不是固定的,可以随意,但一般情况下,会放到类的根路径下。
mybatis-config.xml文件中的配置信息不理解没关系,先把连接数据库的信息修改以下即可。其他的别动。
* 第四步:编写XxxxMapper.xml文件
在这个配置文件当中编写SQL语句。
这个文件名也不是固定的,放的位置也不是固定,我们这里给它起个名字,叫做:CarMapper.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="fdsafdsa">
<!--insert语句,id是这条SQL语句的唯一标识。这个id就代表了这条SQL语句。-->
<insert id="insertCar">
insert into powernode_mybatis.t_car(id,car_num,brand,guide_price,produce_time,car_type)
values(null,'1003','丰田霸道',30.0,'2000-10-11','燃油车');
</insert>
</mapper>
* 第五步:在mybatis-config.xml文件中指定XxxxMapper.xml文件的路径:
<mapper resource="CarMapper.xml"/>
注意:resource属性会自动从类的根路径下开始查找资源。
<?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>
<!--开启mybatis对标准日志的实现。-->
<!--<settings>-->
<!--<setting name="logImpl" value="STDOUT_LOGGING"/>-->
<!--<setting name="logImpl" value="SLF4J"/>-->
<!--</settings>-->
<environments default="development">
<environment id="development">
<!--<transactionManager type="MANAGED"/>-->
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/powernode_mybatis"/>
<property name="username" value="root"/>
<property name="password" value="abc123"/>
</dataSource>
</environment>
</environments>
<mappers>
<!--执行XxxMapper.xml文件的路径-->
<!--resource属性自动会从类的根路径下开始查找资源。-->
<mapper resource="CarMapper.xml"/>
<!--resource属性:从类路径当中加载资源。-->
<!--<mapper resource="com/CarMapper2.xml"/>-->
<!--url属性:从绝对路径当中加载资源。-->
<!--语法格式:file:///绝对路径-->
<!--不建议:因为移植性差。-->
<!--<mapper url="file:///d:/CarMapper.xml"/>-->
</mappers>
</configuration>
* 第六步:编写MyBatis程序。(使用mybatis的类库,编写mybatis程序,连接数据库,做增删改查就行了。)
在MyBatis当中,负责执行SQL语句的那个对象叫做什么呢?
SqlSession
SqlSession是专门用来执行SQL语句的,是一个Java程序和数据库之间的一次会话。
要想获取SqlSession对象,需要先获取SqlSessionFactory对象,通过SqlSessionFactory工厂来生产SqlSession对象。
怎么获取SqlSessionFactory对象呢?
需要首先获取SqlSessionFactoryBuilder对象。通过SqlSessionFactoryBuilder对象的build方法,来获取一个SqlSessionFactory对象。
mybatis的核心对象包括:
SqlSessionFactoryBuilder
SqlSessionFactory
SqlSession
SqlSessionFactoryBuilder --> SqlSessionFactory --> SqlSession
2.9.3 从 XML中构建SqlSessionFactory
通过官方的这句话,你能想到什么呢?
第一:在MyBatis中一定是有一个很重要的对象,这个对象是:SqlSessionFactory对象。
第二:SqlSessionFactory对象的创建需要XML。
XML是什么?
它一定是一个配置文件。
2.9.4 mybatis中有两个主要的配置文件
其中一个是:mybatis-config.xml,这是核心配置文件,主要配置连接数据库的信息等。(一个)
另一个是:XxxxMapper.xml,这个文件是专门用来编写SQL语句的配置文件。(一个表一个)
t_user表,一般会对应一个UserMapper.xml
t_student表,一般会对应一个StudentMapper.xml
2.9.5 关于第一个程序的小细节
* mybatis中sql语句的结尾";"可以省略。
* Resources.getResourceAsStream
小技巧:以后凡是遇到resource这个单词,大部分情况下,这种加载资源的方式就是从类的根路径下开始加载。(开始查找)
优点:采用这种方式,从类路径当中加载资源,项目的移植性很强。项目从windows移植到linux,代码不需要修改,因为这个资源文件一直都在类路径当中。
* InputStream is = new FileInputStream("d:\\mybatis-config.xml");
采用这种方式也可以。
缺点:可移植性太差,程序不够健壮。可能会移植到其他的操作系统当中。导致以上路径无效,还需要修改java代码中的路径。这样违背了OCP原则。
* 已经验证了:
mybatis核心配置文件的名字,不一定是:mybatis-config.xml。可以是其它名字。
mybatis核心配置文件存放的路径,也不一定是在类的根路径下。可以放到其它位置。但为了项目的移植性,健壮性,最好将这个配置文件放到类路径下面。
* InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("mybatis-config.xml");
ClassLoader.getSystemClassLoader() 获取系统的类加载器。
系统类加载器有一个方法叫做:getResourceAsStream,它就是从类路径当中加载资源的。
通过源代码分析发现:
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
底层的源代码其实就是:
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("mybatis-config.xml");
* CarMapper.xml文件的名字是固定的吗?CarMapper.xml文件的路径是固定的吗?
都不是固定的。
<mapper resource="CarMapper.xml"/> resource属性:这种方式是从类路径当中加载资源。
<mapper url="file:///d:/CarMapper.xml"/> url属性:这种方式是从绝对路径当中加载资源。
2.9.6 关于mybatis的事务管理机制(深度剖析)
* 在mybatis-config.xml文件中,可以通过以下的配置进行mybatis的事务管理
<transactionManager type="JDBC"/>
* type属性的值包括两个:
JDBC(jdbc)
MANAGED(managed)
type后面的值,只有以上两个值可选,不区分大小写。
* 在mybatis中提供了两种事务管理机制:
第一种:JDBC事务管理器
第二种:MANAGED事务管理器
* JDBC事务管理器:
mybatis框架自己管理事务,自己采用原生的JDBC代码去管理事务:
conn.setAutoCommit(false); 开启事务。
....业务处理...
conn.commit(); 手动提交事务
使用JDBC事务管理器的话,底层创建的事务管理器对象:JdbcTransaction对象。
如果你编写的代码是下面的代码:
SqlSession sqlSession = sqlSessionFactory.openSession(true);
表示没有开启事务。因为这种方式压根不会执行:conn.setAutoCommit(false);
在JDBC事务中,没有执行conn.setAutoCommit(false);那么autoCommit就是true。
如果autoCommit是true,就表示没有开启事务。只要执行任意一条DML语句就提交一次。
* MANAGED事务管理器:
mybatis不再负责事务的管理了。事务管理交给其它容器来负责。例如:spring。
我不管事务了,你来负责吧。
对于我们当前的单纯的只有mybatis的情况下,如果配置为:MANAGED,那么事务这块是没人管的。没有人管理事务表示事务压根没有开启。
没有人管理事务就是没有事务。
* JDBC中的事务:
如果你没有在JDBC代码中执行:conn.setAutoCommit(false);的话,默认的autoCommit是true。
* 重点:
以后注意了,只要你的autoCommit是true,就表示没有开启事务。只有你的autoCommit是false的时候,就表示开启了事务。
2.9.7 关于mybatis集成日志组件(让我们调试起来更加方便)
* mybatis常见的集成的日志组件有哪些呢?
SLF4J(沙拉风):沙拉风是一个日志标准,其中有一个框架叫做logback,它实现了沙拉风规范。
LOG4J
LOG4J2
STDOUT_LOGGING
....
注意:log4j log4j2 logback都是同一个作者开发的。
* 其中STDOUT_LOGGING是标准日志,mybatis已经实现了这种标准日志。mybatis框架本身已经实现了这种标准。
只要开启即可。怎么开启呢?在mybatis-config.xml文件中使用settings标签进行配置开启。
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
这个标签在编写的时候要注意,它应该出现在environments标签之前。注意顺序。当然,不需要记忆这个顺序。
因为有dtd文件进行约束呢。我们只要参考dtd约束即可。
这种实现也是可以的,可以看到一些信息,比如:连接对象什么时候创建,什么时候关闭,sql语句是怎样的。
但是没有详细的日期,线程名字,等。如果你想使用更加丰富的配置,可以集成第三方的log组件。
* 集成logback日志框架。
logback日志框架实现了slf4j标准。(沙拉风:日志门面。日志标准。)
第一步:引入logback的依赖。
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
</dependency>
第二步:引入logback所必须的xml配置文件。
这个配置文件的名字必须叫做:logback.xml或者logback-test.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">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>[%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>
3 使用MyBatis完成CRUD
● 准备工作
○ 创建module(Maven的普通Java模块):mybatis-002-crud
○ pom.xml
■ 打包方式jar
■ 依赖:
● mybatis依赖
● mysql驱动依赖
● junit依赖
● logback依赖
○ mybatis-config.xml放在类的根路径下
○ CarMapper.xml放在类的根路径下
○ logback.xml放在类的根路径下
○ 提供com.powernode.mybatis.utils.SqlSessionUtil工具类
○ 创建测试用例:com.powernode.mybatis.CarMapperTest
3.1 insert(Create)
分析以下SQL映射文件中SQL语句存在的问题
<?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先随便写-->
<mapper namespace="car">
<insert id="insertCar">
insert into t_car(car_num,brand,guide_price,produce_time,car_type) values('103', '奔驰E300L', 50.3, '2022-01-01', '燃油车')
</insert>
</mapper>
存在的问题是:SQL语句中的值不应该写死,值应该是用户提供的。之前的JDBC代码是这样写的:
// JDBC中使用 ? 作为占位符。那么MyBatis中会使用什么作为占位符呢?
String sql = "insert into t_car(car_num,brand,guide_price,produce_time,car_type) values(?,?,?,?,?)";
// ......
// 给 ? 传值。那么MyBatis中应该怎么传值呢?
ps.setString(1,"103");
ps.setString(2,"奔驰E300L");
ps.setDouble(3,50.3);
ps.setString(4,"2022-01-01");
ps.setString(5,"燃油车");
在MyBatis中可以这样做:
在Java程序中,将数据放到Map集合中
在sql语句中使用 #{map集合的key} 来完成传值,#{} 等同于JDBC中的 ? ,#{}就是占位符
Java程序这样写:
package com.powernode.mybatis;
import com.powernode.mybatis.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
/**
* 测试MyBatis的CRUD
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class CarMapperTest {
@Test
public void testInsertCar(){
// 准备数据
// 前端传过来数据了。
// 这个对象我们先使用Map集合进行数据的封装。
Map<String, Object> map = new HashMap<>();
map.put("k1", "103");
map.put("k2", "奔驰E300L");
map.put("k3", 50.3);
map.put("k4", "2020-10-01");
map.put("k5", "燃油车");
// 获取SqlSession对象
SqlSession sqlSession = SqlSessionUtil.openSession();
// 执行SQL语句(使用map集合给sql语句传递数据)
// 第一个参数:sqlId,从CarMapper.xml文件中复制。
// 第二个参数:封装数据的对象。
int count = sqlSession.insert("insertCar", map);
System.out.println("插入了几条记录:" + count);
sqlSession.commit();
sqlSession.close();
}
}
SQL语句这样写:
<?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先随便写-->
<mapper namespace="car">
<insert id="insertCar">
insert into t_car(car_num,brand,guide_price,produce_time,car_type)
values(#{k1},#{k2},#{k3},#{k4},#{k5})
</insert>
</mapper>
#{} 的里面必须填写map集合的key,不能随便写。运行测试程序,查看数据库:
如果#{}里写的是map集合中不存在的key会有什么问题?
<?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="car">
<insert id="insertCar">
insert into t_car(car_num,brand,guide_price,produce_time,car_type) values(#{kk},#{k2},#{k3},#{k4},#{k5})
</insert>
</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="car">
<insert id="insertCar">
insert into t_car(car_num,brand,guide_price,produce_time,car_type) values(#{kk},#{k2},#{k3},#{k4},#{k5})
</insert>
</mapper>
运行程序:
通过测试,看到程序并没有报错。正常执行。不过 #{kk} 的写法导致无法获取到map集合中的数据,最终导致数据库表car_num插入了NULL。
在以上sql语句中,可以看到#{k1} #{k2} #{k3} #{k4} #{k5}的可读性太差,为了增强可读性,我们可以将Java程序做如下修改:
Map<String, Object> map = new HashMap<>();
// 让key的可读性增强
map.put("carNum", "103");
map.put("brand", "奔驰E300L");
map.put("guidePrice", 50.3);
map.put("produceTime", "2020-10-01");
map.put("carType", "燃油车");
SQL语句做如下修改,这样可以增强程序的可读性:
<?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="car">
<insert id="insertCar">
insert into t_car(car_num,brand,guide_price,produce_time,car_type)
values(#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
</insert>
</mapper>
运行程序,查看数据库表:
使用Map集合可以传参,那使用pojo(简单普通的java对象)可以完成传参吗?测试一下:
- 第一步:定义一个pojo类Car,提供相关属性。
package com.powernode.mybatis.pojo;
/**
* 封装汽车相关信息的pojo类。普通的java类。
* @author 动力节点
* @version 1.0
* @since 1.0
*/
public class Car {
// 数据库表当中的字段应该和pojo类的属性一一对应。
// 建议使用包装类,这样可以防止null的问题。
private Long id;
private String carNum;
private String brand;
private Double guidePrice;
private String produceTime;
private String carType;
@Override
public String toString() {
return "Car{" +
"id=" + id +
", carNum='" + carNum + '\'' +
", brand='" + brand + '\'' +
", guidePrice=" + guidePrice +
", produceTime='" + produceTime + '\'' +
", carType='" + carType + '\'' +
'}';
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCarNum() {
return carNum;
}
/*public String getXyz() {
return carNum;
}*/
public void setCarNum(String carNum) {
this.carNum = carNum;
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public Double getGuidePrice() {
return guidePrice;
}
public void setGuidePrice(Double guidePrice) {
this.guidePrice = guidePrice;
}
public String getProduceTime() {
return produceTime;
}
public void setProduceTime(String produceTime) {
this.produceTime = produceTime;
}
public String getCarType() {
return carType;
}
public void setCarType(String carType) {
this.carType = carType;
}
public Car(Long id, String carNum, String brand, Double guidePrice, String produceTime, String carType) {
this.id = id;
this.carNum = carNum;
this.brand = brand;
this.guidePrice = guidePrice;
this.produceTime = produceTime;
this.carType = carType;
}
public Car() {
}
}
- 第二步:Java程序
@Test
public void testInsertCarByPOJO(){
// 获取SqlSession对象
SqlSession sqlSession = SqlSessionUtil.openSession();
// 创建POJO,封装数据
Car car = new Car();
car.setCarNum("103");
car.setBrand("奔驰C200");
car.setGuidePrice(33.23);
car.setProduceTime("2020-10-11");
car.setCarType("燃油车");
// 执行SQL,传数据
int count = sqlSession.insert("insertCar", car); // ORM
System.out.println(count);
sqlSession.commit();
sqlSession.close();
}
- 第三步:SQL语句
<insert id="insertCarByPOJO">
<!--#{} 里写的是POJO的属性名-->
insert into t_car(id,car_num,brand,guide_price,produce_time,car_type)
values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
</insert>
- 运行程序,查看数据库表:
#{} 里写的是POJO的属性名,如果写成其他的会有问题吗?
<insert id="insertCarByPOJO">
insert into t_car(car_num,brand,guide_price,produce_time,car_type)
values(#{a},#{brand},#{guidePrice},#{produceTime},#{carType})
</insert>
运行程序,出现了以下异常:
错误信息中描述:在Car类中没有找到a属性的getter方法。
修改POJO类Car的代码,只将getCarNum()方法名修改为getA(),其他代码不变,如下:
再运行程序,查看数据库表中数据:
经过测试得出结论:
如果采用map集合传参,#{} 里写的是map集合的key,如果key不存在不会报错,数据库表中会插入NULL。
如果采用POJO传参,#{} 里写的是get方法的方法名去掉get之后将剩下的单词首字母变小写(例如:getAge对应的是#{age},getUserName对应的是#{userName}),如果这样的get方法不存在会报错。
注意:其实传参数的时候有一个属性parameterType,这个属性用来指定传参的数据类型,不过这个属性是可以省略的
<insert id="insertCar" parameterType="java.util.Map">
insert into t_car(car_num,brand,guide_price,produce_time,car_type) values(#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
</insert>
<insert id="insertCarByPOJO" parameterType="com.powernode.mybatis.pojo.Car">
insert into t_car(car_num,brand,guide_price,produce_time,car_type) values(#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
</insert>
3.2 delete(Delete)
需求:根据car_num进行删除。
SQL语句这样写:
<delete id="deleteByCarNum">
delete from t_car where car_num = #{SuiBianXie}
</delete>
Java程序这样写:
@Test
public void testDeleteByCarNum(){
// 获取SqlSession对象
SqlSession sqlSession = SqlSessionUtil.openSession();
// 执行SQL语句
int count = sqlSession.delete("deleteByCarNum", "102");
System.out.println("删除了几条记录:" + count);
sqlSession.commit();
sqlSession.close();
}
运行结果:
注意:当占位符只有一个的时候,#{} 里面的内容可以随便写,但是不能空着。
3.3 update(Update)
需求:修改id=4的Car信息,car_num为9999,brand为凯美瑞,guide_price为30.30,produce_time为1999-11-10,car_type为燃油车
修改前:
SQL语句如下:
CarMapper.xml
<update id="updateCarByPOJO">
update t_car set
car_num=#{carNum},
brand=#{brand},
guide_price=#{guidePrice},
produce_time=#{produceTime},
car_type=#{carType}
where
id = #{id}
</update>
Java代码如下:
@Test
public void testUpdateCarByPOJO(){
// 准备数据
Car car = new Car();
car.setId(34L);
car.setCarNum("102");
car.setBrand("比亚迪汉");
car.setGuidePrice(30.23);
car.setProduceTime("2018-09-10");
car.setCarType("电车");
// 获取SqlSession对象
SqlSession sqlSession = SqlSessionUtil.openSession();
// 执行SQL语句
int count = sqlSession.update("updateCarByPOJO", car);
System.out.println("更新了几条记录:" + count);
sqlSession.commit();
sqlSession.close();
}
运行结果:
当然了,如果使用map传数据也是可以的。
3.4 select(Retrieve)
select语句和其它语句不同的是:查询会有一个结果集。来看mybatis是怎么处理结果集的!!!
3.4.1 查询一条数据
需求:查询id为1的Car信息
SQL语句如下:
<select id="selectCarById">
select * from t_car where id = #{id}
</select>
Java程序如下:
@Test
public void testSelectById(){
// 获取SqlSession对象
SqlSession sqlSession = SqlSessionUtil.openSession();
// 执行DQL语句。查询。根据id查询。返回结果一定是一条。
// mybatis底层执行了select语句之后,一定会返回一个结果集对象:ResultSet
// JDBC中叫做ResultSet,接下来就是mybatis应该从ResultSet中取出数据,封装java对象。
Object car = sqlSession.selectOne("selectCarById", 1);
System.out.println(car);
sqlSession.close();
}
运行结果如下:
运行结果出现了异常
### Error querying database. Cause: org.apache.ibatis.executor.ExecutorException:
A query was run and no Result Maps were found for the Mapped Statement 'car.selectCarById'. 【翻译】:对于一个查询语句来说,没有找到查询的结果映射。
It's likely that neither a Result Type nor a Result Map was specified. 【翻译】:很可能既没有指定结果类型,也没有指定结果映射。
以上的异常大致的意思是:对于一个查询语句来说,你需要指定它的“结果类型”或者“结果映射”。
所以说,你想让mybatis查询之后返回一个Java对象的话,至少你要告诉mybatis返回一个什么类型的Java对象,可以在<select>标签中添加resultType属性,用来指定查询要转换的类型:
<select id="selectCarById" resultType="com.powernode.mybatis.pojo.Car">
select * from t_car where id = #{id}
</select>
运行结果:
运行后之前的异常不再出现了,这说明添加了resultType属性之后,解决了之前的异常,可以看出resultType是不能省略的。
仔细观察控制台的日志信息,不难看出,结果查询出了一条。并且每个字段都查询到值了:Row: 1, 1001, 宝马520Li, 41.00, 2022-09-01, 燃油车
但是奇怪的是返回的Car对象,只有id和brand两个属性有值,其它属性的值都是null,这是为什么呢?我们来观察一下查询结果列名和Car类的属性名是否能一一对应:
查询结果集的列名:id, car_num, brand, guide_price, produce_time, car_type
Car类的属性名:id, carNum, brand, guidePrice, produceTime, carType
通过观察发现:只有id和brand是一致的,其他字段名和属性名对应不上,这是不是导致null的原因呢?我们尝试在sql语句中使用as关键字来给查询结果列名起别名试试:
CarMapper.xml
<select id="selectCarById" resultType="com.powernode.mybatis.pojo.Car">
select
id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType
from
t_car
where
id = #{id}
</select>
运行结果如下:
通过测试得知,如果当查询结果的字段名和java类的属性名对应不上的话,可以采用as关键字起别名,当然还有其它解决方案,我们后面再看。
3.4.2 查询多条数据
需求:查询所有的Car信息。
SQL语句如下:
<!--虽然结果是List集合,但是resultType属性需要指定的是List集合中元素的类型。-->
<select id="selectCarAll" resultType="com.powernode.mybatis.pojo.Car">
<!--记得使用as起别名,让查询结果的字段名和java类的属性名对应上。-->
select
id,car_num as carNum,brand,guide_price as guidePrice,
produce_time as produceTime,
car_type as carType
from
t_car
</select>
Java代码如下:
@Test
public void testSelectCarAll(){
// 获取SqlSession对象
SqlSession sqlSession = SqlSessionUtil.openSession();
// 执行SQL语句
List<Object> cars = sqlSession.selectList("selectAll");
//List<Car> cars = sqlSession.selectList("selectCarAll");//也可以强制类型转换
// 遍历
cars.forEach(car -> System.out.println(car));
sqlSession.close();
}
运行结果如下:
3.5 关于SQL Mapper的namespace
在SQL Mapper配置文件中<mapper>标签的namespace属性可以翻译为命名空间,这个命名空间主要是为了防止sqlId冲突的。
创建CarMapper2.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="car2">
<select id="selectCarAll" resultType="com.powernode.mybatis.pojo.Car">
select
id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType
from
t_car
</select>
</mapper>
不难看出,CarMapper.xml和CarMapper2.xml文件中都有 id="selectCarAll"
将CarMapper2.xml配置到mybatis-config.xml文件中。
<mappers>
<mapper resource="CarMapper.xml"/>
<mapper resource="CarMapper2.xml"/>
</mappers>
编写Java代码如下:
@Test
public void testNamespace(){
// 获取SqlSession对象
SqlSession sqlSession = SqlSessionUtil.openSession();
// 执行SQL语句
List<Object> cars = sqlSession.selectList("selectCarAll");
// 输出结果
cars.forEach(car -> System.out.println(car));
sqlSession.close();
}
运行结果如下:
异常信息
org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: java.lang.IllegalArgumentException:
selectCarAll is ambiguous in Mapped Statements collection (try using the full name including the namespace, or rename one of the entries)
【翻译】selectCarAll在Mapped Statements集合中不明确(请尝试使用包含名称空间的全名,或重命名其中一个条目)
【大致意思是】selectCarAll重名了,你要么在selectCarAll前添加一个名称空间,要么你改个其它名字。
Java代码修改如下:
CarMapperTest.testNamespace
@Test
public void testNamespace(){
// 获取SqlSession对象
SqlSession sqlSession = SqlSessionUtil.openSession();
// 执行SQL语句
//List<Object> cars = sqlSession.selectList("car.selectCarAll");
List<Object> cars = sqlSession.selectList("car2.selectCarAll");
// 输出结果
cars.forEach(car -> System.out.println(car));
sqlSession.close();
}
运行结果如下:
3.6 课堂笔记(mybatis-002-crud)
使用mybatis完成CRUD
3.6.1 什么是CRUD
C: Create增
R: Retrieve查(检索)
U: Update改
D: Delete删
3.6.2 insert
<insert id="insertCar">
insert into t_car(id,car_num,brand,guide_price,produce_time,car_type)
values(null,'1003','丰田霸道',30.0,'2000-10-11','燃油车');
</insert>
这样写的问题是?
值 显然是写死到配置文件中的。
这个在实际开发中是不存在的。
一定是前端的form表单提交过来数据。然后将值传给sql语句。
例如:JDBC的代码是怎么写的?
String sql = "insert into t_car(id,car_num,brand,guide_price,produce_time,car_type)
values(null,?,?,?,?,?)";
ps.setString(1, xxx);//第一个?传值
ps.setString(2, yyy);//第二个?传值
....
在JDBC当中占位符采用的是?,在mybatis当中是什么呢?
和?等效的写法是:#{}
在mybatis当中不能使用?占位符,必须使用 #{} 来代替JDBC当中的 ?
#{} 和 JDBC当中的 ? 是等效的。
java程序中使用Map可以给SQL语句的占位符传值:
Map<String, Object> map = new HashMap<>();
map.put("k1", "1111");
map.put("k2", "比亚迪汉");
map.put("k3", 10.0);
map.put("k4", "2020-11-11");
map.put("k5", "电车");
insert into t_car(id,car_num,brand,guide_price,produce_time,car_type)
values(null,#{k1},#{k2},#{k3},#{k4},#{k5});
注意:#{这里写什么?写map集合的key,如果key不存在,获取的是null}
一般map集合的key起名的时候要见名知意。
map.put("carNum", "1111");
map.put("brand", "比亚迪汉2");
map.put("guidePrice", 10.0);
map.put("produceTime", "2020-11-11");
map.put("carType", "电车");
insert into t_car(id,car_num,brand,guide_price,produce_time,car_type)
values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType});
java程序中使用POJO类给SQL语句的占位符传值:
Car car = new Car(null, "3333", "比亚迪秦", 30.0, "2020-11-11", "新能源");
注意:占位符#{},大括号里面写:pojo类的属性名
insert into t_car(id,car_num,brand,guide_price,produce_time,car_type)
values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
把SQL语句写成这个德行:
insert into t_car(id,car_num,brand,guide_price,produce_time,car_type)
values(null,#{xyz},#{brand},#{guidePrice},#{produceTime},#{carType})
出现了什么问题呢?
There is no getter for property named 'xyz' in 'class com.powernode.mybatis.pojo.Car'
mybatis去找:Car类中的getXyz()方法去了。没找到。报错了。
怎么解决的?
可以在Car类中提供一个getXyz()方法。这样问题就解决了。
通过这个测试,得出一个结论:
严格意义上来说:如果使用POJO对象传递值的话,#{}这个大括号中到底写什么?
写的是get方法的方法名去掉get,然后将剩下的单词首字母小写,然后放进去。
例如:getUsername() --> #{username}
例如:getEmail() --> #{email}
....
也就是说mybatis在底层给?传值的时候,先要获取值,怎么获取的?
调用了pojo对象的get方法。例如:car.getCarNum(),car.getCarType(),car.getBrand()
3.6.3 delete
* 需求:根据id删除数据
将id=59的数据删除。
实现:
int count = sqlSession.delete("deleteById", 59);
<delete id="deleteById">
delete from t_car where id = #{fdsfd}
</delete>
注意:如果占位符只有一个,那么#{}的大括号里可以随意。但是最好见名知意。
3.6.4 update
* 需求:根据id修改某条记录。
实现:
<update id="updateById">
update t_car set
car_num=#{carNum},
brand=#{brand},
guide_price=#{guidePrice},
produce_time=#{produceTime},
car_type=#{carType}
where
id = #{id}
</update>
Car car = new Car(4L, "9999", "凯美瑞", 30.3, "1999-11-10", "燃油车");
int count = sqlSession.update("updateById", car);
3.6.5 select(查一个,根据主键查询的话,返回的结果一定是一个)
* 需求:根据id查询。
实现:
<select id="selectById" resultType="com.powernode.mybatis.pojo.Car">
select * from t_car where id = #{id}
</select>
Object car = sqlSession.selectOne("selectById", 1);
需要特别注意的是:
select标签中resultType属性,这个属性用来告诉mybatis,查询结果集封装成什么类型的java对象。你需要告诉mybatis。
resultType通常写的是:全限定类名。
Car{id=1, carNum='null', brand='宝马520Li', guidePrice=null, produceTime='null', carType='null'}
输出结果有点不对劲:
id和brand属性有值。
其他属性为null。
carNum以及其他的这几个属性没有赋上值的原因是什么?
select * from t_car where id = 1
执行结果:
+----+---------+-----------+-------------+--------------+----------+
| id | car_num | brand | guide_price | produce_time | car_type |
+----+---------+-----------+-------------+--------------+----------+
| 1 | 1001 | 宝马520Li | 10.00 | 2020-10-11 | 燃油车 |
+----+---------+-----------+-------------+--------------+----------+
car_num、guide_price、produce_time、car_type这是查询结果的列名。
这些列名和Car类中的属性名对不上。
Car类的属性名:
carNum、guidePrice、produceTime、carType
那这个问题怎么解决呢?
select语句查询的时候,查询结果集的列名是可以使用as关键字起别名的。
<select id="selectById" resultType="com.powernode.mybatis.pojo.Car">
select
id,car_num as carNum,brand,guide_price as guidePrice,
produce_time as produceTime,
car_type as carType
from
t_car
where
id = #{id}
</select>
起别名之后:
+----+--------+-----------+------------+-------------+---------+
| id | carNum | brand | guidePrice | produceTime | carType |
+----+--------+-----------+------------+-------------+---------+
| 1 | 1001 | 宝马520Li | 10.00 | 2020-10-11 | 燃油车 |
+----+--------+-----------+------------+-------------+---------+
3.6.6 select(查所有的)
<select id="selectAll" resultType="com.powernode.mybatis.pojo.Car">
select
id,car_num as carNum,brand,guide_price as guidePrice,
produce_time as produceTime,
car_type as carType
from
t_car
</select>
List<Object> cars = sqlSession.selectList("selectAll");
注意:resultType还是指定要封装的结果集的类型。不是指定List类型,是指定List集合中元素的类型。
selectList方法:mybatis通过这个方法就可以得知你需要一个List集合。它会自动给你返回一个List集合。
3.6.7 在sql mapper.xml文件当中有一个namespace,这个属性是用来指定命名空间的。用来防止id重复。
怎么用?
在xml文件中:
<mapper namespace="aaaaaaaaa">
<select id="selectAll" resultType="com.powernode.mybatis.pojo.Car">
select
id,car_num as carNum,brand,guide_price as guidePrice,
produce_time as produceTime,
car_type
from
t_car
</select>
</mapper>
在java程序中的写法:
List<Object> cars = sqlSession.selectList("aaaaaaaaa.selectAll");
实际上,本质上,mybatis中的sqlId的完整写法:namespace.id
4 MyBatis核心配置文件详解
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/powernode"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="CarMapper.xml"/>
<mapper resource="CarMapper2.xml"/>
</mappers>
</configuration>
● configuration:根标签,表示配置信息。
● environments:环境(多个),以“s”结尾表示复数,也就是说mybatis的环境可以配置多个数据源。
○ default属性:表示默认使用的是哪个环境,default后面填写的是environment的id。default的值只需要和environment的id值一致即可。
● environment:具体的环境配置(主要包括:事务管理器的配置 + 数据源的配置)
○ id:给当前环境一个唯一标识,该标识用在environments的default后面,用来指定默认环境的选择。
● transactionManager:配置事务管理器
○ type属性:指定事务管理器具体使用什么方式,可选值包括两个
■ JDBC:使用JDBC原生的事务管理机制。底层原理:事务开启conn.setAutoCommit(false); ...处理业务...事务提交conn.commit();
■ MANAGED:交给其它容器来管理事务,比如WebLogic、JBOSS等。如果没有管理事务的容器,则没有事务。没有事务的含义:只要执行一条DML语句,则提交一次。
● dataSource:指定数据源
○ type属性:用来指定具体使用的数据库连接池的策略,可选值包括三个
■ UNPOOLED:采用传统的获取连接的方式,虽然也实现Javax.sql.DataSource接口,但是并没有使用池的思想。
● property可以是:
○ driver 这是 JDBC 驱动的 Java 类全限定名。
○ url 这是数据库的 JDBC URL 地址。
○ username 登录数据库的用户名。
○ password 登录数据库的密码。
○ defaultTransactionIsolationLevel 默认的连接事务隔离级别。
○ defaultNetworkTimeout 等待数据库操作完成的默认网络超时时间(单位:毫秒)
■ POOLED:采用传统的javax.sql.DataSource规范中的连接池,mybatis中有针对规范的实现。
● property可以是(除了包含UNPOOLED中之外):
○ poolMaximumActiveConnections 在任意时间可存在的活动(正在使用)连接数量,默认值:10
○ poolMaximumIdleConnections 任意时间可能存在的空闲连接数。
○ 其它....
■ JNDI:采用服务器提供的JNDI技术实现,来获取DataSource对象,不同的服务器所能拿到DataSource是不一样。如果不是web或者maven的war工程,JNDI是不能使用的。
● property可以是(最多只包含以下两个属性):
○initial_context 这个属性用来在 InitialContext 中寻找上下文(即,initialContext.lookup(initial_context))这是个可选属性,如果忽略,那么将会直接从 InitialContext 中寻找 data_source 属性。
○ data_source 这是引用数据源实例位置的上下文路径。提供了 initial_context 配置时会在其返回的上下文中进行查找,没有提供时则直接在 InitialContext 中查找。
● mappers:在mappers标签中可以配置多个sql映射文件的路径。
● mapper:配置某个sql映射文件的路径
○ resource属性:使用相对于类路径的资源引用方式
○ url属性:使用完全限定资源定位符(URL)方式
4.1 environment
mybatis-003-configuration
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="dev">-->
<!--默认使用生产环境-->
<environments default="production">
<!--开发环境-->
<environment id="dev">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/powernode"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
<!--生产环境-->
<environment id="production">
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="CarMapper.xml"/>
</mappers>
</configuration>
CarMapper.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="car">
<insert id="insertCar">
insert into t_car(id,car_num,brand,guide_price,produce_time,car_type) values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
</insert>
</mapper>
ConfigurationTest.testEnvironment
package com.powernode.mybatis;
import com.powernode.mybatis.pojo.Car;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;
public class ConfigurationTest {
@Test
public void testEnvironment() throws Exception{
// 准备数据
Car car = new Car();
car.setCarNum("133");
car.setBrand("丰田霸道");
car.setGuidePrice(50.3);
car.setProduceTime("2020-01-10");
car.setCarType("燃油车");
// 一个数据库对应一个SqlSessionFactory对象
// 两个数据库对应两个SqlSessionFactory对象,以此类推
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 使用默认数据库
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
SqlSession sqlSession = sqlSessionFactory.openSession(true);
int count = sqlSession.insert("insertCar", car);
System.out.println("插入了几条记录:" + count);
// 使用指定数据库
SqlSessionFactory sqlSessionFactory1 = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"), "dev");
SqlSession sqlSession1 = sqlSessionFactory1.openSession(true);
int count1 = sqlSession1.insert("insertCar", car);
System.out.println("插入了几条记录:" + count1);
}
}
执行结果:
4.2 transactionManager
mybatis-config2.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="dev">
<environment id="dev">
<transactionManager type="MANAGED"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/powernode"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="CarMapper.xml"/>
</mappers>
</configuration>
ConfigurationTest.testTransactionManager
@Test
public void testTransactionManager() throws Exception{
// 准备数据
Car car = new Car();
car.setCarNum("133");
car.setBrand("丰田霸道");
car.setGuidePrice(50.3);
car.setProduceTime("2020-01-10");
car.setCarType("燃油车");
// 获取SqlSessionFactory对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config2.xml"));
// 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
// 执行SQL
int count = sqlSession.insert("insertCar", car);
System.out.println("插入了几条记录:" + count);
}
当事务管理器是:JDBC
● 采用JDBC的原生事务机制:
○ 开启事务:conn.setAutoCommit(false);
○ 处理业务......
○ 提交事务:conn.commit();
当事务管理器是:MANAGED
● 交给容器去管理事务,但目前使用的是本地程序,没有容器的支持,当mybatis找不到容器的支持时:没有事务。也就是说只要执行一条DML语句,则提交一次。
4.3 dataSource
mybatis-config3.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="dev">
<environment id="dev">
<transactionManager type="JDBC"/>
<dataSource type="UNPOOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/powernode"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="CarMapper.xml"/>
</mappers>
</configuration>
ConfigurationTest.testDataSource
@Test
public void testDataSource() throws Exception{
// 准备数据
Car car = new Car();
car.setCarNum("133");
car.setBrand("丰田霸道");
car.setGuidePrice(50.3);
car.setProduceTime("2020-01-10");
car.setCarType("燃油车");
// 获取SqlSessionFactory对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config3.xml"));
// 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession(true);
// 执行SQL
int count = sqlSession.insert("insertCar", car);
System.out.println("插入了几条记录:" + count);
// 关闭会话
sqlSession.close();
}
当type是UNPOOLED,控制台输出:
修改配置文件mybatis-config3.xml中的配置:
<dataSource type="POOLED">
Java测试程序不需要修改,直接执行,看控制台输出:
通过测试得出:UNPOOLED不会使用连接池,每一次都会新建JDBC连接对象。POOLED会使用数据库连接池。【这个连接池是mybatis自己实现的。】
<dataSource type="JNDI">
JNDI的方式:表示对接JNDI服务器中的连接池。这种方式给了我们可以使用第三方连接池的接口。如果想使用dbcp、c3p0、druid(德鲁伊)等,需要使用这种方式。
这种再重点说一下type="POOLED"的时候,它的属性有哪些?
mybatis-config3.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="dev">
<environment id="dev">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/powernode"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
<!--最大连接数-->
<property name="poolMaximumActiveConnections" value="3"/>
<!--这是一个底层设置,如果获取连接花费了相当长的时间,连接池会打印状态日志并重新尝试获取一个连接(避免在误配置的情况下一直失败且不打印日志),默认值:20000 毫秒(即 20 秒)。-->
<property name="poolTimeToWait" value="20000"/>
<!--强行回归池的时间-->
<property name="poolMaximumCheckoutTime" value="20000"/>
<!--最多空闲数量-->
<property name="poolMaximumIdleConnections" value="1"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="CarMapper.xml"/>
</mappers>
</configuration>
poolMaximumActiveConnections:最大的活动的连接数量。默认值10
poolMaximumIdleConnections:最大的空闲连接数量。默认值5
poolMaximumCheckoutTime:强行回归池的时间。默认值20秒。
poolTimeToWait:当无法获取到空闲连接时,每隔20秒打印一次日志,避免因代码配置有误,导致傻等。(时长是可以配置的)
当然,还有其他属性。对于连接池来说,以上几个属性比较重要。
最大的活动的连接数量就是连接池连接数量的上限。默认值10,如果有10个请求正在使用这10个连接,第11个请求只能等待空闲连接。
最大的空闲连接数量。默认值5,如何已经有了5个空闲连接,当第6个连接要空闲下来的时候,连接池会选择关闭该连接对象。来减少数据库的开销。
需要根据系统的并发情况,来合理调整连接池最大连接数以及最多空闲数量。充分发挥数据库连接池的性能。【可以根据实际情况进行测试,然后调整一个合理的数量。】
下图是默认配置:
在以上配置的基础之上,可以编写java程序测试:
ConfigurationTest.testPool
@Test
public void testPool() throws Exception{
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config3.xml"));
for (int i = 0; i < 4; i++) {
SqlSession sqlSession = sqlSessionFactory.openSession();
Object selectCarByCarNum = sqlSession.selectOne("selectCarByCarNum");
}
}
CarMapper.xml
<select id="selectCarByCarNum" resultType="com.powernode.mybatis.pojo.Car">
select id,car_num carNum,brand,guide_price guidePrice,produce_time produceTime,car_type carType from t_car where car_num = '100'
</select>
4.4 properties
mybatis提供了更加灵活的配置,连接数据库的信息可以单独写到一个属性资源文件中,假设在类的根路径下创建jdbc.properties文件,配置如下:
jdbc.properties
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/powernode
在mybatis核心配置文件中引入并使用:
mybatis-config4.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>
<!--引入外部属性资源文件-->
<properties resource="jdbc.properties">
<property name="jdbc.username" value="root"/>
<property name="jdbc.password" value="root"/>
</properties>
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<!--${key}使用-->
<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>
<mapper resource="CarMapper.xml"/>
</mappers>
</configuration>
编写Java程序进行测试:
ConfigurationTest.testProperties
@Test
public void testProperties() throws Exception{
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config4.xml"));
SqlSession sqlSession = sqlSessionFactory.openSession();
Object car = sqlSession.selectOne("selectCarByCarNum");
System.out.println(car);
}
properties两个属性:
resource:这个属性从类的根路径下开始加载。【常用的。】
url:从指定的url加载,假设文件放在d:/jdbc.properties,这个url可以写成:file:///d:/jdbc.properties。注意是三个斜杠哦。
注意:如果不知道mybatis-config.xml文件中标签的编写顺序的话,可以有两种方式知道它的顺序:
● 第一种方式:查看dtd约束文件。
● 第二种方式:通过idea的报错提示信息。【一般采用这种方式】
4.5 mapper
mapper标签用来指定SQL映射文件的路径,包含多种指定方式,这里先主要看其中两种:
第一种:resource,从类的根路径下开始加载【比url常用】
mybatis-config4.xml
<mappers>
<mapper resource="CarMapper.xml"/>
</mappers>
如果是这样写的话,必须保证类的根下有CarMapper.xml文件。
如果类的根路径下有一个包叫做test,CarMapper.xml如果放在test包下的话,这个配置应该是这样写:
mybatis-config4.xml
<mappers>
<mapper resource="test/CarMapper.xml"/>
</mappers>
第二种:url,从指定的url位置加载
假设CarMapper.xml文件放在d盘的根下,这个配置就需要这样写:
mybatis-config4.xml
<mappers>
<mapper url="file:///d:/CarMapper.xml"/>
</mappers>
mapper还有其他的指定方式,后面再看!!!
4.6 课堂程序(mybatis-003-configuration)
JNDI(Java Naming and Directory Interface,Java命名和目录接口)是SUN公司提供的一种标准的Java命名系统接
002-连接池
<?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>
<!--java.util.Properties类。是一个Map集合。key和value都是String类型-->
<!--在properties标签中可以配置很多属性-->
<!--<properties>-->
<!--这是其中的一个属性-->
<!--<property name="属性名" value="属性值"/>-->
<!--<property name="jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="jdbc.url" value="jdbc:mysql://localhost:3306/powernode"/>
<property name="jdbc.username" value="root"/>
<property name="jdbc.password" value="root"/>-->
<!--</properties>-->
<!--resource,一定是从类路径下开始查找资源-->
<properties resource="jdbc.properties" />
<!--从绝对路径当中加载资源。绝对路径怎么写?file:///路径-->
<!--<properties url="file:///d:/jdbc.properties" />-->
<!--default表示默认使用的环境。-->
<!--默认环境什么意思?当你使用mybatis创建SqlSessionFactory对象的时候,没有指定环境的话,默认使用哪个环境。-->
<environments default="powernodeDB">
<!--其中的一个环境。连接的数据库是powernode-->
<!--一般一个数据库会对应一个SqlSessionFactory对象。-->
<!--一个环境environment会对应一个SqlSessionFactory对象-->
<environment id="powernodeDB">
<!--
transactionManager标签:
1.作用:配置事务管理器。指定mybatis具体使用什么方式去管理事务。
2.type属性有两个值:
第一个:JDBC: 使用原生的JDBC代码来管理事务。
conn.setAutoCommit(false);
....
conn.commit();
第二个:MANAGED:mybatis不再负责事务的管理,将事务管理交给其它的JEE(JavaEE)容器来管理。例如:spring
3. 大小写无所谓。不缺分大小写。但是不能写其他值。只能是二选一:
jdbc、managed
4. 在mybatis中提供了一个事务管理器接口:Transaction
该接口下有两个实现类:
JdbcTransaction
ManagedTransaction
如果type="JDBC",那么底层会实例化JdbcTransaction对象。
如果type="MANAGED",那么底层会实例化ManagedTransaction
-->
<transactionManager type="JDBC"/>
<!--
dataSource配置:
1.dataSource被称为数据源。
2.dataSource作用是什么?为程序提供Connection对象。(但凡是给程序提供Connection对象的,都叫做数据源。)
3.数据源实际上是一套规范。JDK中有这套规范:javax.sql.DataSource(这个数据源的规范,这套接口实际上是JDK规定的。)
4.我们自己也可以编写数据源组件,只要实现javax.sql.DataSource接口就行了。实现接口当中所有的方法。这样就有了自己的数据源。
比如你可以写一个属于自己的数据库连接池(数据库连接池是提供连接对象的,所以数据库连接池就是一个数据源)。
5.常见的数据源组件有哪些呢【常见的数据库连接池有哪些呢】?
阿里巴巴的德鲁伊连接池:druid
c3p0
dbcp
....
6. type属性用来指定数据源的类型,就是指定具体使用什么方式来获取Connection对象:
type属性有三个值:必须是三选一。
type="[UNPOOLED|POOLED|JNDI]"
UNPOOLED:不使用数据库连接池技术。每一次请求过来之后,都是创建新的Connection对象。
POOLED:使用mybatis自己实现的数据库连接池。
JNDI:集成其它第三方的数据库连接池。
JNDI是一套规范。谁实现了这套规范呢?大部分的web容器都实现了JNDI规范:
例如:Tomcat、Jetty、WebLogic、WebSphere,这些服务器(容器)都实现了JNDI规范。
JNDI是:java命名目录接口。Tomcat服务器实现了这个规范。
JNDI(Java Naming and Directory Interface,Java命名和目录接口)是SUN公司提供的一种标准的Java命名系统接
-->
<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}"/>
<!--提醒:正常使用连接池的话,池中有很多参数是需要设置的。设置好参数,可以让连接池发挥的更好。事半功倍的效果。-->
<!--具体连接池当中的参数如何配置呢?需要反复的根据当前业务情况进行测试。-->
<!--poolMaximumActiveConnections:连接池当中最多的正在使用的连接对象的数量上限。最多有多少个连接可以活动。默认值10-->
<property name="poolMaximumActiveConnections" value="10"/>
<!--每隔2秒打印日志,并且尝试获取连接对象-->
<property name="poolTimeToWait" value="2000"/>
<!--强行让某个连接空闲,超时时间的设置-->
<property name="poolMaximumCheckoutTime" value="10000"/>
<!--最多的空闲数量-->
<property name="poolMaximumIdleConnections" value="5"/>
</dataSource>
</environment>
<!--这是mybatis的另一个环境,也就是连接的数据库是另一个数据库mybatis-->
<environment id="mybatisDB">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="CarMapper.xml"/>
</mappers>
</configuration>
5 手写MyBatis框架(掌握原理)
警示:该部分内容有难度,基础较弱的程序员可能有些部分是听不懂的,如果无法跟下来,可直接跳过,不影响后续知识点的学习。当然,如果你要能够跟下来,必然会让你加深对MyBatis框架的理解。
我们给自己的框架起个名:GodBatis(起名灵感来源于:my god!!! 我的天呢!)
5.1 dom4j解析XML文件
该部分内容不再赘述,不会解析XML的,请观看老杜前面讲解的dom4j解析XML文件的视频。
模块名:parse-xml-by-dom4j(普通的Java Maven模块)
第一步:引入dom4j的依赖
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.group</groupId>
<artifactId>parse-xml-by-dom4j</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<!--dom4j依赖-->
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.3</version>
</dependency>
<!--jaxen依赖-->
<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>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
</project>
第二步:编写配置文件godbatis-config.xml
godbatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/powernode"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
<mappers>
<mapper resource="sqlmapper.xml"/>
</mappers>
</environments>
</configuration>
第三步:解析godbatis-config.xml
package com.powernode.dom4j;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import org.junit.Test;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 使用dom4j解析XML文件
*/
public class ParseXMLByDom4j {
@Test
public void testGodBatisConfig() throws Exception{
// 读取xml,获取document对象
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(Thread.currentThread().getContextClassLoader().getResourceAsStream("godbatis-config.xml"));
// 获取<environments>标签的default属性的值
Element environmentsElt = (Element)document.selectSingleNode("/configuration/environments");
String defaultId = environmentsElt.attributeValue("default");
System.out.println(defaultId);
// 获取environment标签
Element environmentElt = (Element)document.selectSingleNode("/configuration/environments/environment[@id='" + defaultId + "']");
// 获取事务管理器类型
Element transactionManager = environmentElt.element("transactionManager");
String transactionManagerType = transactionManager.attributeValue("type");
System.out.println(transactionManagerType);
// 获取数据源类型
Element dataSource = environmentElt.element("dataSource");
String dataSourceType = dataSource.attributeValue("type");
System.out.println(dataSourceType);
// 将数据源信息封装到Map集合
Map<String,String> dataSourceMap = new HashMap<>();
dataSource.elements().forEach(propertyElt -> {
dataSourceMap.put(propertyElt.attributeValue("name"), propertyElt.attributeValue("value"));
});
dataSourceMap.forEach((k, v) -> System.out.println(k + ":" + v));
// 获取sqlmapper.xml文件的路径
Element mappersElt = (Element) document.selectSingleNode("/configuration/environments/mappers");
mappersElt.elements().forEach(mapper -> {
System.out.println(mapper.attributeValue("resource"));
});
}
}
执行结果:
第四步:编写配置文件sqlmapper.xml
sqlmapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="car">
<insert id="insertCar">
insert into t_car(id,car_num,brand,guide_price,produce_time,car_type) values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
</insert>
<select id="selectCarByCarNum" resultType="com.powernode.mybatis.pojo.Car">
select id,car_num carNum,brand,guide_price guidePrice,produce_time produceTime,car_type carType from t_car where car_num = #{carNum}
</select>
</mapper>
第五步:解析sqlmapper.xml
ParseXMLByDom4j.testSqlMapper
@Test
public void testSqlMapper() throws Exception{
// 读取xml,获取document对象
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(Thread.currentThread().getContextClassLoader().getResourceAsStream("sqlmapper.xml"));
// 获取namespace
Element mapperElt = (Element) document.selectSingleNode("/mapper");
String namespace = mapperElt.attributeValue("namespace");
System.out.println(namespace);
// 获取sql id
mapperElt.elements().forEach(statementElt -> {
// 标签名
String name = statementElt.getName();
System.out.println("name:" + name);
// 如果是select标签,还要获取它的resultType
if ("select".equals(name)) {
String resultType = statementElt.attributeValue("resultType");
System.out.println("resultType:" + resultType);
}
// sql id
String id = statementElt.attributeValue("id");
System.out.println("sqlId:" + id);
// sql语句
String sql = statementElt.getTextTrim();
System.out.println("sql:" + sql);
});
}
执行结果:
5.1.1 实战代码
parse-xml-by-dom4j/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="mybatisDB">
<environment id="powernodeDB">
<transactionManager type="MANAGED"/>
<dataSource type="UNPOOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/powernode_mybatis"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
<environment id="mybatisDB">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="CarMapper.xml"/>
</mappers>
</configuration>
parse-xml-by-dom4j/CarMapper.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="car">
<insert id="insertCar">
insert into t_car values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
</insert>
<select id="selectById" resultType="com.powernode.mybatis.pojo.Car">
select
id,car_num as carNum,brand,guide_price as guidePrice,
produce_time as produceTime,
car_type as carType
from
t_car
where
id = #{id}
</select>
</mapper>
解析以上两个XML文件
parse-xml-by-dom4j/ParseXMLByDom4jTest.java
package com.powernode.xml.test;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import org.junit.Test;
import java.io.InputStream;
import java.util.List;
public class ParseXMLByDom4jTest {
@Test
public void testParseSqlMapperXML() throws Exception{
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);
// 获取mapper节点下所有的子节点
List<Element> elements = mapper.elements();
// 遍历
elements.forEach(element -> {
String name = element.getName();
System.out.println("标签名字:" + name);
// 获取sqlId
String id = element.attributeValue("id");
System.out.println(id);
// 获取resultType
String resultType = element.attributeValue("resultType"); // 没有这个属性的话,会自动返回"null"
System.out.println(resultType);
// 获取标签中的sql语句(表示获取标签中的文本内容,而且去除前后空白)
String sql = element.getTextTrim();
System.out.println(sql);
// insert into t_car values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
// insert into t_car values(null,?,?,?,?,?)
// mybaits封装了jdbc。早晚要执行带有?的sql语句。
// 转换
String newSql = sql.replaceAll("#\\{[0-9A-Za-z_$]*}", "?");
System.out.println(newSql);
});
}
@Test
public void testParseMyBatisConfigXML() throws Exception{
// 创建SAXReader对象
SAXReader reader = new SAXReader();
// 获取输入流
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("mybatis-config.xml");
// 读XML文件,返回document对象。document对象是文档对象,代表了整个XML文件。
Document document = reader.read(is);
// 获取文档当中的根标签
//Element rootElt = document.getRootElement();
//String rootEltName = rootElt.getName();
//System.out.println("根节点的名字:" + rootEltName);
//获取default默认的环境id
// xpath是做标签路径匹配的。能够让我们快速定位XML文件中的元素。
// 以下的xpath代表了:从根下开始找configuration标签,然后找configuration标签下的子标签environments
String xpath = "/configuration/environments";
Element environments = (Element) document.selectSingleNode(xpath); // Element是Node类的子类,方法更多,使用更便捷。
// 获取属性的值
String defaultEnvironmentId = environments.attributeValue("default");
//System.out.println("默认环境的id:" + defaultEnvironmentId);
// 获取具体的环境environment
xpath = "/configuration/environments/environment[@id='"+defaultEnvironmentId+"']";
//System.out.println(xpath);
Element environment = (Element) document.selectSingleNode(xpath);
// 获取environment节点下的transactionManager节点(Element的element()方法用来获取孩子节点)
Element transactionManager = environment.element("transactionManager");
String transactionType = transactionManager.attributeValue("type");
System.out.println("事务管理器的类型:" + transactionType);
// 获取dataSource节点
Element dataSource = environment.element("dataSource");
String dataSourceType = dataSource.attributeValue("type");
System.out.println("数据源的类型:" + dataSourceType);
// 获取dataSource节点下的所有子节点
List<Element> propertyElts = dataSource.elements();
// 遍历
propertyElts.forEach(propertyElt -> {
String name = propertyElt.attributeValue("name");
String value = propertyElt.attributeValue("value");
System.out.println(name + "=" + value);
});
// 获取所有的mapper标签
// 不想从根下开始获取,你想从任意位置开始,获取所有的某个标签,xpath该这样写
xpath = "//mapper";
List<Node> mappers = document.selectNodes(xpath);
// 遍历
mappers.forEach(mapper -> {
Element mapperElt = (Element) mapper;
String resource = mapperElt.attributeValue("resource");
System.out.println(resource);
});
}
}
5.2 GodBatis
手写框架之前,如果没有思路,可以先参考一下mybatis的客户端程序,通过客户端程序来逆推需要的类,参考代码:
可参考的MyBatis客户端代码
@Test
public void testInsert(){
SqlSession sqlSession = null;
try {
// 1.创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 2.创建SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
// 3.创建SqlSession对象
sqlSession = sqlSessionFactory.openSession();
// 4.执行SQL
Car car = new Car(null, "111", "宝马X7", "70.3", "2010-10-11", "燃油车");
int count = sqlSession.insert("insertCar",car);
System.out.println("更新了几条记录:" + count);
// 5.提交
sqlSession.commit();
} catch (Exception e) {
// 回滚
if (sqlSession != null) {
sqlSession.rollback();
}
e.printStackTrace();
} finally {
// 6.关闭
if (sqlSession != null) {
sqlSession.close();
}
}
}
@Test
public void testSelectOne(){
SqlSession sqlSession = null;
try {
// 1.创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 2.创建SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
// 3.创建SqlSession对象
sqlSession = sqlSessionFactory.openSession();
// 4.执行SQL
Car car = (Car)sqlSession.selectOne("selectCarByCarNum", "111");
System.out.println(car);
// 5.提交
sqlSession.commit();
} catch (Exception e) {
// 回滚
if (sqlSession != null) {
sqlSession.rollback();
}
e.printStackTrace();
} finally {
// 6.关闭
if (sqlSession != null) {
sqlSession.close();
}
}
}
5.2.1 第一步:IDEA中创建模块
模块:godbatis(创建普通的Java Maven模块,打包方式jar),引入相关依赖
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.god</groupId>
<artifactId>godbatis</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<dependencies>
<!--dom4j依赖-->
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.3</version>
</dependency>
<!--jaxen依赖-->
<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>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
</project>
5.2.2 第二步:资源工具类,方便获取指向配置文件的输入流
Resources
package org.god.core;
import java.io.InputStream;
/**
* 资源工具类
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class Resources {
/**
* 从类路径中获取配置文件的输入流
* @param config
* @return 输入流,该输入流指向类路径中的配置文件
*/
public static InputStream getResourcesAsStream(String config){
return Thread.currentThread().getContextClassLoader().getResourceAsStream(config);
}
}
5.2.3 第三步:定义SqlSessionFactoryBuilder类
提供一个无参数构造方法,再提供一个build方法,该build方法要返回SqlSessionFactory对象
SqlSessionFactoryBuilder
package org.god.core;
import java.io.InputStream;
/**
* SqlSessionFactory对象构建器
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class SqlSessionFactoryBuilder {
/**
* 创建构建器对象
*/
public SqlSessionFactoryBuilder() {
}
/**
* 获取SqlSessionFactory对象
* 该方法主要功能是:读取godbatis核心配置文件,并构建SqlSessionFactory对象
* @param inputStream 指向核心配置文件的输入流
* @return SqlSessionFactory对象
*/
public SqlSessionFactory build(InputStream inputStream){
// 解析配置文件,创建数据源对象
// 解析配置文件,创建事务管理器对象
// 解析配置文件,获取所有的SQL映射对象
// 将以上信息封装到SqlSessionFactory对象中
// 返回
return null;
}
}
5.2.4 第四步:分析SqlSessionFactory类中有哪些属性
事务管理器
GodJDBCTransaction
SQL映射对象集合
Map<String, GodMappedStatement>
5.2.5 第五步:定义GodJDBCTransaction
事务管理器最好是定义一个接口,然后每一个具体的事务管理器都实现这个接口。
TransactionManager
package org.god.core;
import java.sql.Connection;
/**
* 事务管理器接口
* @author 老杜
* @version 1.0
* @since 1.0
*/
public interface TransactionManager {
/**
* 提交事务
*/
void commit();
/**
* 回滚事务
*/
void rollback();
/**
* 关闭事务
*/
void close();
/**
* 开启连接
*/
void openConnection();
/**
* 获取连接对象
* @return 连接对象
*/
Connection getConnection();
}
GodJDBCTransaction
package org.god.core;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
/**
* 事务管理器
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class GodJDBCTransaction implements TransactionManager {
/**
* 连接对象,控制事务时需要
*/
private Connection conn;
/**
* 数据源对象
*/
private DataSource dataSource;
/**
* 自动提交标志:
* true表示自动提交
* false表示不自动提交
*/
private boolean autoCommit;
/**
* 构造事务管理器对象
* @param autoCommit
*/
public GodJDBCTransaction(DataSource dataSource, boolean autoCommit) {
this.dataSource = dataSource;
this.autoCommit = autoCommit;
}
/**
* 提交事务
*/
public void commit(){
try {
conn.commit();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* 回滚事务
*/
public void rollback(){
try {
conn.rollback();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public void close() {
try {
conn.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public void openConnection() {
try {
this.conn = dataSource.getConnection();
this.conn.setAutoCommit(this.autoCommit);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public Connection getConnection() {
return conn;
}
}
5.2.6 第六步:事务管理器中需要数据源,定义GodUNPOOLEDDataSource
GodUNPOOLEDDataSource
package org.god.core;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
/**
* 数据源实现类,不使用连接池
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class GodUNPOOLEDDataSource implements javax.sql.DataSource{
private String url;
private String username;
private String password;
public GodUNPOOLEDDataSource(String driver, String url, String username, String password) {
try {
// 注册驱动
Class.forName(driver);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
this.url = url;
this.username = username;
this.password = password;
}
@Override
public Connection getConnection() throws SQLException {
return DriverManager.getConnection(url, username, password);
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return null;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
}
@Override
public int getLoginTimeout() throws SQLException {
return 0;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return null;
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
}
5.2.7 第七步:定义GodMappedStatement
GodMappedStatement
package org.god.core;
/**
* SQL映射实体类
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class GodMappedStatement {
private String sqlId;
private String resultType;
private String sql;
private String parameterType;
private String sqlType;
@Override
public String toString() {
return "GodMappedStatement{" +
"sqlId='" + sqlId + '\'' +
", resultType='" + resultType + '\'' +
", sql='" + sql + '\'' +
", parameterType='" + parameterType + '\'' +
", sqlType='" + sqlType + '\'' +
'}';
}
public String getSqlId() {
return sqlId;
}
public void setSqlId(String sqlId) {
this.sqlId = sqlId;
}
public String getResultType() {
return resultType;
}
public void setResultType(String resultType) {
this.resultType = resultType;
}
public String getSql() {
return sql;
}
public void setSql(String sql) {
this.sql = sql;
}
public String getParameterType() {
return parameterType;
}
public void setParameterType(String parameterType) {
this.parameterType = parameterType;
}
public String getSqlType() {
return sqlType;
}
public void setSqlType(String sqlType) {
this.sqlType = sqlType;
}
public GodMappedStatement(String sqlId, String resultType, String sql, String parameterType, String sqlType) {
this.sqlId = sqlId;
this.resultType = resultType;
this.sql = sql;
this.parameterType = parameterType;
this.sqlType = sqlType;
}
}
5.2.8 第八步:完善SqlSessionFactory类
SqlSessionFactory
package org.god.core;
import javax.sql.DataSource;
import java.util.List;
import java.util.Map;
/**
* SqlSession工厂对象,使用SqlSessionFactory可以获取会话对象
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class SqlSessionFactory {
private TransactionManager transactionManager;
private Map<String, GodMappedStatement> mappedStatements;
public SqlSessionFactory(TransactionManager transactionManager, Map<String, GodMappedStatement> mappedStatements) {
this.transactionManager = transactionManager;
this.mappedStatements = mappedStatements;
}
public TransactionManager getTransactionManager() {
return transactionManager;
}
public void setTransactionManager(TransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
public Map<String, GodMappedStatement> getMappedStatements() {
return mappedStatements;
}
public void setMappedStatements(Map<String, GodMappedStatement> mappedStatements) {
this.mappedStatements = mappedStatements;
}
}
5.2.9 第九步:完善SqlSessionFactoryBuilder中的build方法
package org.god.core;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import javax.sql.DataSource;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
/**
* SqlSessionFactory对象构建器
*
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class SqlSessionFactoryBuilder {
/**
* 创建构建器对象
*/
public SqlSessionFactoryBuilder() {
}
/**
* 获取SqlSessionFactory对象
* 该方法主要功能是:读取godbatis核心配置文件,并构建SqlSessionFactory对象
*
* @param inputStream 指向核心配置文件的输入流
* @return SqlSessionFactory对象
*/
public SqlSessionFactory build(InputStream inputStream) throws DocumentException {
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(inputStream);
Element environmentsElt = (Element) document.selectSingleNode("/configuration/environments");
String defaultEnv = environmentsElt.attributeValue("default");
Element environmentElt = (Element) document.selectSingleNode("/configuration/environments/environment[@id='" + defaultEnv + "']");
// 解析配置文件,创建数据源对象
Element dataSourceElt = environmentElt.element("dataSource");
DataSource dataSource = getDataSource(dataSourceElt);
// 解析配置文件,创建事务管理器对象
Element transactionManagerElt = environmentElt.element("transactionManager");
TransactionManager transactionManager = getTransactionManager(transactionManagerElt, dataSource);
// 解析配置文件,获取所有的SQL映射对象
Element mappers = environmentsElt.element("mappers");
Map<String, GodMappedStatement> mappedStatements = getMappedStatements(mappers);
// 将以上信息封装到SqlSessionFactory对象中
SqlSessionFactory sqlSessionFactory = new SqlSessionFactory(transactionManager, mappedStatements);
// 返回
return sqlSessionFactory;
}
private Map<String, GodMappedStatement> getMappedStatements(Element mappers) {
Map<String, GodMappedStatement> mappedStatements = new HashMap<>();
mappers.elements().forEach(mapperElt -> {
try {
String resource = mapperElt.attributeValue("resource");
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(Resources.getResourcesAsStream(resource));
Element mapper = (Element) document.selectSingleNode("/mapper");
String namespace = mapper.attributeValue("namespace");
mapper.elements().forEach(sqlMapper -> {
String sqlId = sqlMapper.attributeValue("id");
String sql = sqlMapper.getTextTrim();
String parameterType = sqlMapper.attributeValue("parameterType");
String resultType = sqlMapper.attributeValue("resultType");
String sqlType = sqlMapper.getName().toLowerCase();
// 封装GodMappedStatement对象
GodMappedStatement godMappedStatement = new GodMappedStatement(sqlId, resultType, sql, parameterType, sqlType);
mappedStatements.put(namespace + "." + sqlId, godMappedStatement);
});
} catch (DocumentException e) {
throw new RuntimeException(e);
}
});
return mappedStatements;
}
private TransactionManager getTransactionManager(Element transactionManagerElt, DataSource dataSource) {
String type = transactionManagerElt.attributeValue("type").toUpperCase();
TransactionManager transactionManager = null;
if ("JDBC".equals(type)) {
// 使用JDBC事务
transactionManager = new GodJDBCTransaction(dataSource, false);
} else if ("MANAGED".equals(type)) {
// 事务管理器是交给JEE容器的
}
return transactionManager;
}
private DataSource getDataSource(Element dataSourceElt) {
// 获取所有数据源的属性配置
Map<String, String> dataSourceMap = new HashMap<>();
dataSourceElt.elements().forEach(propertyElt -> {
dataSourceMap.put(propertyElt.attributeValue("name"), propertyElt.attributeValue("value"));
});
String dataSourceType = dataSourceElt.attributeValue("type").toUpperCase();
DataSource dataSource = null;
if ("POOLED".equals(dataSourceType)) {
} else if ("UNPOOLED".equals(dataSourceType)) {
dataSource = new GodUNPOOLEDDataSource(dataSourceMap.get("driver"), dataSourceMap.get("url"), dataSourceMap.get("username"), dataSourceMap.get("password"));
} else if ("JNDI".equals(dataSourceType)) {
}
return dataSource;
}
}
5.2.10 第十步:在SqlSessionFactory中添加openSession方法
SqlSessionFactory.openSession
public SqlSession openSession(){
transactionManager.openConnection();
SqlSession sqlSession = new SqlSession(transactionManager, mappedStatements);
return sqlSession;
}
5.2.11 第十一步:编写SqlSession类中commit rollback close方法
SqlSession
package org.god.core;
import java.sql.SQLException;
import java.util.Map;
/**
* 数据库会话对象
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class SqlSession {
private TransactionManager transactionManager;
private Map<String, GodMappedStatement> mappedStatements;
public SqlSession(TransactionManager transactionManager, Map<String, GodMappedStatement> mappedStatements) {
this.transactionManager = transactionManager;
this.mappedStatements = mappedStatements;
}
public void commit(){
try {
transactionManager.getConnection().commit();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public void rollback(){
try {
transactionManager.getConnection().rollback();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public void close(){
try {
transactionManager.getConnection().close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
5.2.12 第十二步:编写SqlSession类中的insert方法
SqlSession.insert
/**
* 插入数据
*
* @param sqlId 要执行的sqlId
* @param obj 插入的数据
* @return
*/
public int insert(String sqlId, Object obj) {
GodMappedStatement godMappedStatement = mappedStatements.get(sqlId);
Connection connection = transactionManager.getConnection();
// 获取sql语句
// insert into t_car(id,car_num,brand,guide_price,produce_time,car_type) values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
String godbatisSql = godMappedStatement.getSql();
// insert into t_car(id,car_num,brand,guide_price,produce_time,car_type) values(null,?,?,?,?,?)
String sql = godbatisSql.replaceAll("#\\{[a-zA-Z0-9_\\$]*}", "?");
// 重点一步
Map<Integer, String> map = new HashMap<>();
int index = 1;
while (godbatisSql.indexOf("#") >= 0) {
int beginIndex = godbatisSql.indexOf("#") + 2;
int endIndex = godbatisSql.indexOf("}");
map.put(index++, godbatisSql.substring(beginIndex, endIndex).trim());
godbatisSql = godbatisSql.substring(endIndex + 1);
}
final PreparedStatement ps;
try {
ps = connection.prepareStatement(sql);
// 给?赋值
map.forEach((k, v) -> {
try {
// 获取java实体类的get方法名
String getMethodName = "get" + v.toUpperCase().charAt(0) + v.substring(1);
Method getMethod = obj.getClass().getDeclaredMethod(getMethodName);
ps.setString(k, getMethod.invoke(obj).toString());
} catch (Exception e) {
throw new RuntimeException(e);
}
});
int count = ps.executeUpdate();
ps.close();
return count;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
5.2.13 第十三步:编写SqlSession类中的selectOne方法
SqlSession.selectOne
/**
* 查询一个对象
* @param sqlId
* @param parameterObj
* @return
*/
public Object selectOne(String sqlId, Object parameterObj){
GodMappedStatement godMappedStatement = mappedStatements.get(sqlId);
Connection connection = transactionManager.getConnection();
// 获取sql语句
String godbatisSql = godMappedStatement.getSql();
String sql = godbatisSql.replaceAll("#\\{[a-zA-Z0-9_\\$]*}", "?");
// 执行sql
PreparedStatement ps = null;
ResultSet rs = null;
Object obj = null;
try {
ps = connection.prepareStatement(sql);
ps.setString(1, parameterObj.toString());
rs = ps.executeQuery();
if (rs.next()) {
// 将结果集封装对象,通过反射
String resultType = godMappedStatement.getResultType();
Class<?> aClass = Class.forName(resultType);
Constructor<?> con = aClass.getDeclaredConstructor();
obj = con.newInstance();
// 给对象obj属性赋值
ResultSetMetaData rsmd = rs.getMetaData();
int columnCount = rsmd.getColumnCount();
for (int i = 1; i <= columnCount; i++) {
String columnName = rsmd.getColumnName(i);
String setMethodName = "set" + columnName.toUpperCase().charAt(0) + columnName.substring(1);
Method setMethod = aClass.getDeclaredMethod(setMethodName, aClass.getDeclaredField(columnName).getType());
setMethod.invoke(obj, rs.getString(columnName));
}
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
try {
ps.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
return obj;
}
5.3 GodBatis使用Maven打包
查看本地仓库中是否已经有jar包:
5.4 使用GodBatis
使用GodBatis就和使用MyBatis是一样的。
第一步:准备数据库表t_user
第二步:创建模块,普通的Java Maven模块:godbatis-test
第三步:引入依赖
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.powernode</groupId>
<artifactId>godbatis-test</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<!--godbatis依赖-->
<dependency>
<groupId>org.god</groupId>
<artifactId>godbatis</artifactId>
<version>1.0.0</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<!--junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
</project>
第四步:编写pojo类
User
package com.powernode.godbatis.pojo;
public class User {
private String id;
private String name;
private String email;
private String address;
@Override
public String toString() {
return "User{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", email='" + email + '\'' +
", address='" + address + '\'' +
'}';
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public User() {
}
public User(String id, String name, String email, String address) {
this.id = id;
this.name = name;
this.email = email;
this.address = address;
}
}
第五步:编写核心配置文件:godbatis-config.xml
godbatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"/>
<dataSource type="UNPOOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/powernode"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
<mappers>
<mapper resource="UserMapper.xml"/>
</mappers>
</environments>
</configuration>
第六步:编写sql映射文件:UserMapper.xml
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="user">
<insert id="insertUser">
insert into t_user(id,name,email,address) values(#{id},#{name},#{email},#{address})
</insert>
<select id="selectUserById" resultType="com.powernode.godbatis.pojo.User">
select * from t_user where id = #{id}
</select>
</mapper>
第七步:编写测试类
GodBatisTest
package com.powernode.godbatis.test;
import com.powernode.godbatis.pojo.User;
import org.god.core.Resources;
import org.god.core.SqlSession;
import org.god.core.SqlSessionFactory;
import org.god.core.SqlSessionFactoryBuilder;
import org.junit.Test;
public class GodBatisTest {
@Test
public void testInsertUser() throws Exception{
User user = new User("1", "zhangsan", "zhangsan@1234.com", "北京大兴区");
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourcesAsStream("godbatis-config.xml"));
SqlSession sqlSession = sqlSessionFactory.openSession();
int count = sqlSession.insert("user.insertUser", user);
System.out.println("插入了几条记录:" + count);
sqlSession.commit();
sqlSession.close();
}
@Test
public void testSelectUserById() throws Exception{
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourcesAsStream("godbatis-config.xml"));
SqlSession sqlSession = sqlSessionFactory.openSession();
Object user = sqlSession.selectOne("user.selectUserById", "1");
System.out.println(user);
sqlSession.close();
}
}
第八步:运行结果
5.5 总结MyBatis框架的重要实现原理
<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="user">
<insert id="insertUser">
insert into t_user(id,name,email,address) values(#{id},#{name},#{email},#{address})
</insert>
<select id="selectUserById" resultType="com.powernode.godbatis.pojo.User">
select id,name,email,address from t_user where id = #{id}
</select>
</mapper>
思考两个问题:
- 为什么insert语句中 #{} 里填写的必须是属性名?
- 为什么select语句查询结果列名要属性名一致?
5.6 课堂程序(godbatis)
5.6.1 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.god.ibatis</groupId>
<artifactId>godbatis</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<!--依赖-->
<dependencies>
<!--dom4j依赖--> <!--解析xml文件用-->
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.3</version>
</dependency>
<!--jaxen依赖--> <!--解析xml文件用-->
<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>
</dependency>
<!--mysql驱动依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>
5.6.2 核心文件
org/god/ibatis/utils/Resources.java
package org.god.ibatis.utils;
import java.io.InputStream;
/**
* godbatis框架提供的一个工具类。
* 这个工具类专门完成“类路径”中资源的加载。
* @author 动力节点
* @since 1.0
* @version 1.0
*/
public class Resources {
/**
* 工具类的构造方法都是建议私有化的。
* 因为工具类中的方法都是静态的,不需要创建对象就能调用。
* 为了避免new对象,所有构造方法私有化。
* 这只是一种编程习惯。
*/
private Resources(){}
/**
* 从类路径当中加载资源。
* @param resource 放在类路径当中的资源文件。
* @return 指向资源文件的一个输入流。
*/
public static InputStream getResourceAsStream(String resource){
return ClassLoader.getSystemClassLoader().getResourceAsStream(resource);
}
}
org/god/ibatis/core/Const.java
package org.god.ibatis.core;
/**
* 整个godbatis框架的常量类
* @author 动力节点
* @version 1.0
* @since 1.0
*/
public class Const {
public static final String UN_POOLED_DATASOURCE = "UNPOOLED";
public static final String POOLED_DATASOURCE = "POOLED";
public static final String JNDI_DATASOURCE = "JNDI";
public static final String JDBC_TRANSACTION = "JDBC";
public static final String MANAGED_TRANSACTION = "MANAGED";
}
org/god/ibatis/core/SqlSessionFactoryBuilder.java
package org.god.ibatis.core;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import org.god.ibatis.utils.Resources;
import javax.sql.DataSource;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* SqlSessionFactory构建器对象。
* 通过SqlSessionFactoryBuilder的build方法来解析
* godbatis-config.xml文件,然后创建SqlSessionFactory对象。
* @author 动力节点
* @version 1.0
* @since 1.0
*/
public class SqlSessionFactoryBuilder {
/**
* 无参数构造方法。
*/
public SqlSessionFactoryBuilder(){}
/**
* 解析godbatis-config.xml文件,来构建SqlSessionFactory对象。
* @param in 指向godbatis-config.xml文件的一个输入流。
* @return SqlSessionFactory对象。
*/
public SqlSessionFactory build(InputStream in){
SqlSessionFactory factory = null;
try {
// 解析godbatis-config.xml文件
SAXReader reader = new SAXReader();
Document document = reader.read(in);
Element environments = (Element) document.selectSingleNode("/configuration/environments");
String defaultId = environments.attributeValue("default");
Element environment = (Element) document.selectSingleNode("/configuration/environments/environment[@id='" + defaultId + "']");
Element transactionElt = environment.element("transactionManager");
Element dataSourceElt = environment.element("dataSource");
List<String> sqlMapperXMLPathList = new ArrayList<>();
List<Node> nodes = document.selectNodes("//mapper"); // //代表获取整个配置文件中所有的mapper标签
nodes.forEach(node -> {
Element mapper = (Element) node;
String resource = mapper.attributeValue("resource");
sqlMapperXMLPathList.add(resource);
});
// 获取数据源对象
DataSource dataSource = getDataSource(dataSourceElt);
// 获取事务管理器
Transaction transaction = getTransaction(transactionElt,dataSource);
// 获取mappedStatements
Map<String, MappedStatement> mappedStatements = getMappedStatements(sqlMapperXMLPathList);
// 解析完成之后,构建SqlSessionFactory对象。
factory = new SqlSessionFactory(transaction, mappedStatements);
} catch (Exception e) {
e.printStackTrace();
}
return factory;
}
/**
* 解析所有的SqlMapper.xml文件,然后构建Map集合。
* @param sqlMapperXMLPathList
* @return
*/
private Map<String, MappedStatement> getMappedStatements(List<String> sqlMapperXMLPathList) {
Map<String, MappedStatement> mappedStatements = new HashMap<>();
sqlMapperXMLPathList.forEach(sqlMapperXMLPath -> {
try {
SAXReader reader = new SAXReader();
Document document = reader.read(Resources.getResourceAsStream(sqlMapperXMLPath));
Element mapper = (Element) document.selectSingleNode("mapper");
String namespace = mapper.attributeValue("namespace");
List<Element> elements = mapper.elements();
elements.forEach(element -> {
String id = element.attributeValue("id");
// 这里进行了namespace和id的拼接,生成最终的sqlId
String sqlId = namespace + "." + id;
String resultType = element.attributeValue("resultType");
String sql = element.getTextTrim();
MappedStatement mappedStatement = new MappedStatement(sql, resultType);
mappedStatements.put(sqlId, mappedStatement);
});
} catch (Exception e) {
e.printStackTrace();
}
});
return mappedStatements;
}
/**
* 获取事务管理器的
* @param transactionElt 事务管理器标签元素
* @param dataSource 数据源对象
* @return
*/
private Transaction getTransaction(Element transactionElt, DataSource dataSource) {
Transaction transaction = null;
// JDBC | MANAGED
String type = transactionElt.attributeValue("type").trim().toUpperCase();
if (Const.JDBC_TRANSACTION.equals(type)) {
transaction = new JdbcTransaction(dataSource, false); // 默认是开启事务的,将来需要手动提交的。
}
if (Const.MANAGED_TRANSACTION.equals(type)) {
transaction = new ManagedTransaction();
}
return transaction;
}
/**
* 获取数据源对象
* @param dataSourceElt 数据源标签元素
* @return
*/
private DataSource getDataSource(Element dataSourceElt) {
Map<String,String> map = new HashMap<>();
// 获取所有的property
List<Element> propertyElts = dataSourceElt.elements("property");
propertyElts.forEach(propertyElt -> {
String name = propertyElt.attributeValue("name");
String value = propertyElt.attributeValue("value");
map.put(name, value);
});
DataSource dataSource = null;
// UNPOOLED | POOLED | JNDI
String type = dataSourceElt.attributeValue("type").trim().toUpperCase();
if (Const.UN_POOLED_DATASOURCE.equals(type)) {
dataSource = new UnPooledDataSource(map.get("driver"), map.get("url"), map.get("username"), map.get("password"));
}
if (Const.POOLED_DATASOURCE.equals(type)) {
dataSource = new PooledDataSource();
}
if (Const.JNDI_DATASOURCE.equals(type)) {
dataSource = new JNDIDataSource();
}
return dataSource;
}
}
org/god/ibatis/core/SqlSessionFactory.java
package org.god.ibatis.core;
import java.util.Map;
/**
* SqlSessionFactory对象:
* 一个数据库对应一个SqlSessionFactory对象。
* 通过SqlSessionFactory对象可以获取SqlSession对象。(开启会话)
* 一个SqlSessionFactory对象可以开启多个SqlSession会话。
* @author 动力节点
* @version 1.0
* @since 1.0
*/
public class SqlSessionFactory {
/**
* 事务管理器属性
* 事务管理器是可以灵活切换的。
* SqlSessionFactory类中的事务管理器应该是面向接口编程的。
* SqlSessionFactory类中应该有一个事务管理器接口。
*/
private Transaction transaction;
/**
* 存放sql语句的Map集合。
* key是sqlId
* value是对应的SQL标签信息对象。
*/
private Map<String, MappedStatement> mappedStatements;
public Transaction getTransaction() {
return transaction;
}
public void setTransaction(Transaction transaction) {
this.transaction = transaction;
}
public Map<String, MappedStatement> getMappedStatements() {
return mappedStatements;
}
public void setMappedStatements(Map<String, MappedStatement> mappedStatements) {
this.mappedStatements = mappedStatements;
}
/**
* 获取Sql会话对象。
* @return
*/
public SqlSession openSession(){
// 开启会话的前提是开启连接。(连接打开了)
transaction.openConnection();
// 创建SqlSession对象
SqlSession sqlSession = new SqlSession(this);
return sqlSession;
}
public SqlSessionFactory(Transaction transaction, Map<String, MappedStatement> mappedStatements) {
this.transaction = transaction;
this.mappedStatements = mappedStatements;
}
public SqlSessionFactory() {
}
}
org/god/ibatis/core/SqlSession.java
package org.god.ibatis.core;
import java.lang.reflect.Method;
import java.sql.*;
/**
* 专门负责执行SQL语句的会话对象
* @author 动力节点
* @version 1.0
* @since 1.0
*/
public class SqlSession {
private SqlSessionFactory factory;
public SqlSession(SqlSessionFactory factory) {
this.factory = factory;
}
/**
* 执行insert语句,向数据库表当中插入记录。
* @param sqlId sql语句的id
* @param pojo 插入的数据。
* @return
*/
public int insert(String sqlId, Object pojo){
int count = 0;
try {
// JDBC代码,执行insert语句,完成插入操作。
Connection connection = factory.getTransaction().getConnection();
// insert into t_user values(#{id},#{name},#{age})
String godbatisSql = factory.getMappedStatements().get(sqlId).getSql();
// insert into t_user(id,name,age) values(?,?,?)
String sql = godbatisSql.replaceAll("#\\{[a-zA-Z0-9_$]*}", "?");
PreparedStatement ps = connection.prepareStatement(sql);
// 给?占位符传值
// 难度是什么:
// 第一:你不知道有多少个?
// 第二:你不知道该将pojo对象中的哪个属性赋值给哪个 ?
// ps.String(第几个问号, 传什么值); // 这里都是setString,所以数据库表中的字段类型要求都是varchar才行。这是godbatis比较失败的地方。
int fromIndex = 0;
int index = 1;
while(true){
int jingIndex = godbatisSql.indexOf("#", fromIndex);
if (jingIndex < 0) {
break;
}
int youKuoHaoIndex = godbatisSql.indexOf("}", fromIndex);
String propertyName = godbatisSql.substring(jingIndex + 2, youKuoHaoIndex).trim();
fromIndex = youKuoHaoIndex + 1;
// 有属性名id,怎么获取id的属性值呢?调用getId()方法
String getMethodName = "get" + propertyName.toUpperCase().charAt(0) + propertyName.substring(1);
Method getMethod = pojo.getClass().getDeclaredMethod(getMethodName);
Object propertyValue = getMethod.invoke(pojo);
ps.setString(index, propertyValue.toString());
index++;
}
// 执行SQL语句
count = ps.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
}
return count;
}
/**
* 执行查询语句,返回一个对象。该方法只适合返回一条记录的sql语句。
* @param sqlId
* @param param
* @return
*/
public Object selectOne(String sqlId, Object param){
Object obj = null;
try {
Connection connection = factory.getTransaction().getConnection();
MappedStatement mappedStatement = factory.getMappedStatements().get(sqlId);
// 这是那个DQL查询语句
// select * from t_user where id = #{id}
String godbatisSql = mappedStatement.getSql();
String sql = godbatisSql.replaceAll("#\\{[a-zA-Z0-9_$]*}", "?");
PreparedStatement ps = connection.prepareStatement(sql);
// 给占位符传值
ps.setString(1, param.toString());
// 查询返回结果集
ResultSet rs = ps.executeQuery();
// 要封装的结果类型。
String resultType = mappedStatement.getResultType(); // org.god.ibatis.pojo.User
// 从结果集中取数据,封装java对象
if (rs.next()) {
// 获取resultType的Class
Class<?> resultTypeClass = Class.forName(resultType);
// 调用无参数构造方法创建对象
obj = resultTypeClass.newInstance(); // Object obj = new User();
// 给User类的id,name,age属性赋值
// 给obj对象的哪个属性赋哪个值。
/*
mysql> select * from t_user where id = '1111';
+------+----------+------+
| id | name | age |
+------+----------+------+
| 1111 | zhangsan | 20 |
+------+----------+------+
解决问题的关键:将查询结果的列名作为属性名。
列名是id,那么属性名就是:id
列名是name,那么属性名就是:name
*/
ResultSetMetaData rsmd = rs.getMetaData();
int columnCount = rsmd.getColumnCount();
for (int i = 0; i < columnCount; i++) {
String propertyName = rsmd.getColumnName(i + 1);
// 拼接方法名
String setMethodName = "set" + propertyName.toUpperCase().charAt(0) + propertyName.substring(1);
// 获取set方法
Method setMethod = resultTypeClass.getDeclaredMethod(setMethodName, String.class);
// 调用set方法给对象obj属性赋值
setMethod.invoke(obj, rs.getString(propertyName));
}
}
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}
// 局部测试
public static void main(String[] args) {
String sql = "insert into t_user values(#{id},#{name},#{age})";
int fromIndex = 0;
int index = 1;
while(true){
int jingIndex = sql.indexOf("#", fromIndex);
if (jingIndex < 0) {
break;
}
System.out.println(index);
index++;
int youKuoHaoIndex = sql.indexOf("}", fromIndex);
String propertyName = sql.substring(jingIndex + 2, youKuoHaoIndex).trim();
System.out.println(propertyName);
fromIndex = youKuoHaoIndex + 1;
}
}
/**
* 提交事务
*/
public void commit(){
factory.getTransaction().commit();
}
/**
* 回滚事务
*/
public void rollback(){
factory.getTransaction().rollback();
}
/**
* 关闭事务
*/
public void close(){
factory.getTransaction().close();
}
}
5.6.3 事务管理器
事务管理器:JDBC MANAGED二选一,为了让事务管理器能够灵活切换,应该定义接口
org/god/ibatis/core/Transaction.java(事务管理器接口)
package org.god.ibatis.core;
import java.sql.Connection;
/**
* 事务管理器接口。
* 所有的事务管理器都应该遵循该规范。
* JDBC事务管理器,MANAGED事务管理器都应该实现这个接口。
* Transaction事务管理器:提供管理事务方法。
* @author 动力节点
* @version 1.0
* @since 1.0
*/
public interface Transaction {
/**
* 提交事务
*/
void commit();
/**
* 回滚事务
*/
void rollback();
/**
* 关闭事务
*/
void close();
/**
* 真正的开启数据库连接。
*/
void openConnection();
/**
* 获取数据库连接对象的。
*/
Connection getConnection();
}
事务管理器实现类:
org/god/ibatis/core/JdbcTransaction.java
package org.god.ibatis.core;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
/**
* JDBC事务管理器(godbatis框架目前只对JdbcTransaction进行实现。)
* @author 动力节点
* @version 1.0
* @since 1.0
*/
public class JdbcTransaction implements Transaction{
/**
* 数据源属性
* 经典的设计:面向接口编程。
*/
private DataSource dataSource;
/**
* 自动提交标志
* true表示自动提交
* false表示不采用自动提交
*/
private boolean autoCommit;
/**
* 连接对象
*/
private Connection connection;
@Override
public Connection getConnection() {
return connection;
}
/**
* 创建事务管理器对象
* @param dataSource
* @param autoCommit
*/
public JdbcTransaction(DataSource dataSource, boolean autoCommit) {
this.dataSource = dataSource;
this.autoCommit = autoCommit;
}
@Override
public void commit() {
try {
connection.commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
@Override
public void rollback() {
try {
connection.rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}
@Override
public void close() {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
@Override
public void openConnection(){
if (connection == null) {
try {
connection = dataSource.getConnection();
// 开启事务
connection.setAutoCommit(autoCommit);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
org/god/ibatis/core/ManagedTransaction.java
package org.god.ibatis.core;
import java.sql.Connection;
/**
* MANAGED事务管理器。(godbatis对这个类就不再实现了。)
*
* @author 动力节点
* @version 1.0
* @since 1.0
*/
public class ManagedTransaction implements Transaction{
@Override
public void commit() {
}
@Override
public void rollback() {
}
@Override
public void close() {
}
@Override
public void openConnection() {
}
@Override
public Connection getConnection() {
return null;
}
}
5.6.4 存放sql语句的Map集合
org/god/ibatis/core/MappedStatement.java
003-MappedStatement类的封装
package org.god.ibatis.core;
/**
* 普通的java类。POJO,封装了一个SQL标签。
* 一个MappedStatement对象对应一个SQL标签。
* 一个SQL标签中的所有信息封装到MappedStatement对象当中。
* 面向对象编程思想。
* @author 动力节点
* @version 1.0
* @since 1.0
*/
public class MappedStatement {
/**
* sql语句
*/
private String sql;
/**
* 要封装的结果集类型。有的时候resultType是null。
* 比如:insert delete update语句的时候resultType是null。
* 只有当sql语句是select语句的时候resultType才有值。
*/
private String resultType;
@Override
public String toString() {
return "MappedStatement{" +
"sql='" + sql + '\'' +
", resultType='" + resultType + '\'' +
'}';
}
public String getSql() {
return sql;
}
public void setSql(String sql) {
this.sql = sql;
}
public String getResultType() {
return resultType;
}
public void setResultType(String resultType) {
this.resultType = resultType;
}
public MappedStatement(String sql, String resultType) {
this.sql = sql;
this.resultType = resultType;
}
public MappedStatement() {
}
}
5.6.5 数据源
数据源是获取connection对象的:POOLED UNPOOLED JNDI
所有数据源都要实现JDK带的规范:javax.sql.DataSource
org/god/ibatis/core/UnPooledDataSource.java
package org.god.ibatis.core;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
/**
* 数据源的实现类:UNPOOLED (重点实现这种方式。)
* 不使用连接池,每一次都新建Connection对象。
* @author 动力节点
* @version 1.0
* @since 1.0
*/
public class UnPooledDataSource implements javax.sql.DataSource{
private String url;
private String username;
private String password;
/**
* 创建一个数据源对象。
* @param driver
* @param url
* @param username
* @param password
*/
public UnPooledDataSource(String driver, String url, String username, String password) {
try {
// 直接注册驱动
Class.forName(driver);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
this.url = url;
this.username = username;
this.password = password;
}
@Override
public Connection getConnection() throws SQLException {
Connection connection = DriverManager.getConnection(url, username, password);
return connection;
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return null;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
}
@Override
public int getLoginTimeout() throws SQLException {
return 0;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return null;
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
}
org/god/ibatis/core/PooledDataSource.java
package org.god.ibatis.core;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
/**
* 数据源的实现类:POOLED
* 使用godbatis框架内置的数据库连接池来获取Connection对象。(这个不实现了。)
* @author 动力节点
* @version 1.0
* @since 1.0
*/
public class PooledDataSource implements javax.sql.DataSource{
@Override
public Connection getConnection() throws SQLException {
// 从数据库连接池当中获取Connection对象。(这个数据库连接池是我godbatis框架内部封装好的。)
return null;
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return null;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
}
@Override
public int getLoginTimeout() throws SQLException {
return 0;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return null;
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
}
org/god/ibatis/core/JNDIDataSource.java
package org.god.ibatis.core;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
/**
* 数据源的实现类:JNDI
* 使用第三方的数据库连接池获取Connection对象。(这个不实现了。)
* @author 动力节点
* @version 1.0
* @since 1.0
*/
public class JNDIDataSource implements javax.sql.DataSource{
@Override
public Connection getConnection() throws SQLException {
return null;
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return null;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
}
@Override
public int getLoginTimeout() throws SQLException {
return 0;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return null;
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
}
5.6.6 测试程序
org/god/ibatis/pojo/User.java
package org.god.ibatis.pojo;
public class User {
private String id;
private String name;
private String age;
@Override
public String toString() {
return "User{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", age='" + age + '\'' +
'}';
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public User(String id, String name, String age) {
this.id = id;
this.name = name;
this.age = age;
}
public User() {
}
}
org/god/ibatis/test/GodBatisTest.java
package org.god.ibatis.test;
import org.god.ibatis.core.SqlSession;
import org.god.ibatis.core.SqlSessionFactory;
import org.god.ibatis.core.SqlSessionFactoryBuilder;
import org.god.ibatis.pojo.User;
import org.god.ibatis.utils.Resources;
import org.junit.Test;
public class GodBatisTest {
@Test
public void testSelectOne(){
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("godbatis-config.xml"));
SqlSession sqlSession = sqlSessionFactory.openSession();
// 执行SQL语句
Object obj = sqlSession.selectOne("user.selectById", "1112");
System.out.println(obj);
sqlSession.close();
}
@Test
public void testSqlSessionFactory(){
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("godbatis-config.xml"));
System.out.println(sqlSessionFactory);
}
@Test
public void testInsertUser(){
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("godbatis-config.xml"));
SqlSession sqlSession = sqlSessionFactory.openSession();
// 执行SQL insert
User user = new User("1111", "zhangsan", "20");
int count = sqlSession.insert("user.insertUser", user);
System.out.println(count);
sqlSession.commit();
sqlSession.close();
}
}
6 在WEB中应用MyBatis(使用MVC架构模式)
目标:
- 掌握mybatis在web应用中怎么用
- mybatis三大对象的作用域和生命周期
- ThreadLocal原理及使用
- 巩固MVC架构模式
- 为学习MyBatis的接口代理机制做准备
实现功能:
- 银行账户转账
使用技术:
- HTML + Servlet + MyBatis
WEB应用的名称:
- bank
6.1 需求描述
6.2 数据库表的设计和准备数据
6.3 实现步骤
6.3.1 第一步:环境搭建
IDEA中创建Maven WEB应用(mybatis-004-web)
IDEA配置Tomcat,这里Tomcat使用10+版本。并部署应用到tomcat。
默认创建的maven web应用没有java和resources目录,包括两种解决方案
第一种:自己手动加上。
第二种:修改maven-archetype-webapp-1.4.jar中的配置文件
web.xml文件的版本较低,可以从tomcat10的样例文件中复制,然后修改
web.xml(metadata-complete="false">,表示支持注解)
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0"
metadata-complete="false"><!--true表示只支持配置文件,false可以支持注解-->
</web-app>
删除index.jsp文件,因为我们这个项目不使用JSP。只使用html。
确定pom.xml文件中的打包方式是war包。
引入相关依赖
编译器版本修改为17
引入的依赖包括:mybatis,mysql驱动,junit,logback,servlet。
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.powernode</groupId>
<artifactId>mybatis-004-web</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>mybatis-004-web</name>
<url>http://localhost:8080/bank</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<!--mybatis依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.10</version>
</dependency>
<!--mysql驱动依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</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.2.11</version>
</dependency>
<!--servlet依赖-->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>5.0.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>mybatis-004-web</finalName>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
引入相关配置文件,放到resources目录下(全部放到类的根路径下)
mybatis-config.xml
AccountMapper.xml
logback.xml
jdbc.properties
jdbc.properties:
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/powernode
jdbc.username=root
jdbc.password=root
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>
<properties resource="jdbc.properties"/>
<environments default="dev">
<environment id="dev">
<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>
<!--一定要注意这里的路径哦!!!-->
<mapper resource="AccountMapper.xml"/>
</mappers>
</configuration>
AccountMapper.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="account">
</mapper>
6.3.2 第二步:前端页面index.html
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>银行账户转账</title>
</head>
<body>
<!--/bank是应用的根,部署web应用到tomcat的时候一定要注意这个名字-->
<form action="/bank/transfer" method="post">
转出账户:<input type="text" name="fromActno"/><br>
转入账户:<input type="text" name="toActno"/><br>
转账金额:<input type="text" name="money"/><br>
<input type="submit" value="转账"/>
</form>
</body>
</html>
其余前端页面:
error1.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>转账报告</title>
</head>
<body>
<h1>余额不足!!!</h1>
</body>
</html>
error2.htm
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>转账报告</title>
</head>
<body>
<h1>转账失败,未知原因!!!</h1>
</body>
</html>
success.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>转账报告</title>
</head>
<body>
<h1>转账成功!</h1>
</body>
</html>
6.3.3 第三步:创建pojo包、service包、dao包、web包、utils包
com.powernode.bank.pojo
com.powernode.bank.service
com.powernode.bank.service.impl
com.powernode.bank.dao
com.powernode.bank.dao.impl
com.powernode.bank.web.controller
com.powernode.bank.exception
package com.powernode.bank.exceptions;
/**
* 转账异常
*/
public class TransferException extends Exception{
public TransferException(){}
public TransferException(String msg){
}
}
package com.powernode.bank.exceptions;
/**
* 余额不足异常。
*/
public class MoneyNotEnoughException extends Exception {
public MoneyNotEnoughException(){}
public MoneyNotEnoughException(String msg){
super(msg);
}
}
com.powernode.bank.utils:将之前编写的SqlSessionUtil工具类拷贝到该包下。
package com.powernode.bank.utils;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
/**
* MyBatis工具类
* @author 动力节点
* @version 1.0
* @since 1.0
*/
public class SqlSessionUtil {
private SqlSessionUtil(){}
private static SqlSessionFactory sqlSessionFactory;
static {
try {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 全局的,服务器级别的,一个服务器当中定义一个即可。
// 为什么把SqlSession对象放到ThreadLocal当中呢?为了保证一个线程对应一个SqlSession。
private static ThreadLocal<SqlSession> local = new ThreadLocal<>();
/**
* 获取会话对象。
* @return 会话对象
*/
public static SqlSession openSession(){
SqlSession sqlSession = local.get();
if (sqlSession == null) {
sqlSession = sqlSessionFactory.openSession();
// 将sqlSession对象绑定到当前线程上。
local.set(sqlSession);
}
return sqlSession;
}
/**
* 关闭SqlSession对象(从当前线程中移除SqlSession对象。)
* @param sqlSession
*/
public static void close(SqlSession sqlSession){
if (sqlSession != null) {
sqlSession.close();
// 注意移除SqlSession对象和当前线程的绑定关系。
// 因为Tomcat服务器支持线程池。也就是说:用过的线程对象t1,可能下一次还会使用这个t1线程。
local.remove();
}
}
}
6.3.4 第四步:定义pojo类:Account
Account
package com.powernode.bank.pojo;
/**
* 银行账户类
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class Account {
private Long id;
private String actno;
private Double balance;
@Override
public String toString() {
return "Account{" +
"id=" + id +
", actno='" + actno + '\'' +
", balance=" + balance +
'}';
}
public Account() {
}
public Account(Long id, String actno, Double balance) {
this.id = id;
this.actno = actno;
this.balance = balance;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public Double getBalance() {
return balance;
}
public void setBalance(Double balance) {
this.balance = balance;
}
}
6.3.5 第五步:编写AccountDao接口,以及AccountDaoImpl实现类
分析dao中至少要提供几个方法,才能完成转账:
- 转账前需要查询余额是否充足:selectByActno
- 转账时要更新账户:update
AccountDao
package com.powernode.bank.dao;
import com.powernode.bank.pojo.Account;
/**
* 账户的DAO对象。负责t_act表中数据的CRUD.
* 强调一下:DAO对象中的任何一个方法和业务不挂钩。没有任何业务逻辑在里面。
* DAO中的方法就是做CRUD的。所以方法名大部分是:insertXXX deleteXXX updateXXX selectXXX
* @author 动力节点
* @version 1.0
* @since 1.0
*/
public interface AccountDao {
/**
* 根据账号获取账户信息
* @param actno 账号
* @return 账户信息
*/
Account selectByActno(String actno);
/**
* 更新账户信息
* @param act 账户信息
* @return 1表示更新成功,其他值表示失败
*/
int update(Account act);
}
AccountDaoImpl
package com.powernode.bank.dao.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
public class AccountDaoImpl implements AccountDao {
@Override
public Account selectByActno(String actno) {
SqlSession sqlSession = SqlSessionUtil.openSession();
Account act = (Account)sqlSession.selectOne("account.selectByActno", actno);
sqlSession.close();
return act;
}
@Override
public int updateByActno(Account act) {
SqlSession sqlSession = SqlSessionUtil.openSession();
int count = sqlSession.update("account.updateByActno", act);
sqlSession.commit();
sqlSession.close();
return count;
}
}
6.3.6 第六步:AccountDaoImpl中编写mybatis代码,需要编写SQL映射文件了
AccountMapper.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="account">
<select id="selectByActno" resultType="com.powernode.bank.pojo.Account">
select * from t_act where actno = #{actno}
</select>
<update id="updateByActno">
update t_act set balance = #{balance} where actno = #{actno}
</update>
</mapper>
6.3.7 第七步:编写AccountService接口以及AccountServiceImpl
MoneyNotEnoughException
package com.powernode.bank.exception;
/**
* 余额不足异常
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class MoneyNotEnoughException extends Exception{
public MoneyNotEnoughException(){}
public MoneyNotEnoughException(String msg){ super(msg); }
}
AppException
package com.powernode.bank.exception;
/**
* 应用异常
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class AppException extends Exception{
public AppException(){}
public AppException(String msg){ super(msg); }
}
AccountService
package com.powernode.bank.service;
import com.powernode.bank.exception.AppException;
import com.powernode.bank.exception.MoneyNotEnoughException;
/**
* 账户业务类。
* @author 老杜
* @version 1.0
* @since 1.0
*/
public interface AccountService {
/**
* 银行账户转正
* @param fromActno 转出账户
* @param toActno 转入账户
* @param money 转账金额
* @throws MoneyNotEnoughException 余额不足异常
* @throws AppException App发生异常
*/
void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, AppException;
}
AccountServiceImpl
package com.powernode.bank.service.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.dao.impl.AccountDaoImpl;
import com.powernode.bank.exceptions.MoneyNotEnoughException;
import com.powernode.bank.exceptions.TransferException;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.service.AccountService;
import com.powernode.bank.utils.GenerateDaoProxy;
import com.powernode.bank.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
public class AccountServiceImpl implements AccountService {
//private AccountDao accountDao = new AccountDaoImpl();
// 这是咱们自己封装的。
//private AccountDao accountDao = (AccountDao) GenerateDaoProxy.generate(SqlSessionUtil.openSession(), AccountDao.class);
// 在mybatis当中,mybatis提供了相关的机制。也可以动态为我们生成dao接口的实现类。(代理类:dao接口的代理)
// mybatis当中实际上采用了代理模式。在内存中生成dao接口的代理类,然后创建代理类的实例。
// 使用mybatis的这种代理机制的前提:SqlMapper.xml文件中namespace必须是dao接口的全限定名称,id必须是dao接口中的方法名。
// 怎么用?代码怎么写?AccountDao accountDao = sqlSession.getMapper(AccountDao.class);
private AccountDao accountDao = SqlSessionUtil.openSession().getMapper(AccountDao.class);
@Override
public void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, TransferException {
// 添加事务控制代码
SqlSession sqlSession = SqlSessionUtil.openSession();
// 1. 判断转出账户的余额是否充足(select)
Account fromAct = accountDao.selectByActno(fromActno);
if (fromAct.getBalance() < money) {
// 2. 如果转出账户余额不足,提示用户
throw new MoneyNotEnoughException("对不起,余额不足!");
}
// 3. 如果转出账户余额充足,更新转出账户余额(update)
// 先更新内存中java对象account的余额
Account toAct = accountDao.selectByActno(toActno);
fromAct.setBalance(fromAct.getBalance() - money);
toAct.setBalance(toAct.getBalance() + money);
int count = accountDao.updateByActno(fromAct);
// 模拟异常
/*String s = null;
s.toString();*/
// 4. 更新转入账户余额(update)
count += accountDao.updateByActno(toAct);
if (count != 2) {
throw new TransferException("转账异常,未知原因");
}
// 提交事务
sqlSession.commit();
// 关闭事务
SqlSessionUtil.close(sqlSession);
}
}
6.3.8 第八步:编写AccountController
AccountController
package com.powernode.bank.web.controller;
import com.powernode.bank.exception.AppException;
import com.powernode.bank.exception.MoneyNotEnoughException;
import com.powernode.bank.service.AccountService;
import com.powernode.bank.service.impl.AccountServiceImpl;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 账户控制器
* @author 老杜
* @version 1.0
* @since 1.0
*/
@WebServlet("/transfer")
public class AccountController extends HttpServlet {
private AccountService accountService = new AccountServiceImpl();
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 获取响应流
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
// 获取账户信息
String fromActno = request.getParameter("fromActno");
String toActno = request.getParameter("toActno");
double money = Integer.parseInt(request.getParameter("money"));
// 调用业务方法完成转账
try {
accountService.transfer(fromActno, toActno, money);
out.print("<h1>转账成功!!!</h1>");
} catch (MoneyNotEnoughException e) {
out.print(e.getMessage());
} catch (AppException e) {
out.print(e.getMessage());
}
}
}
AccountServlet
package com.powernode.bank.web;
import com.powernode.bank.exceptions.MoneyNotEnoughException;
import com.powernode.bank.exceptions.TransferException;
import com.powernode.bank.service.AccountService;
import com.powernode.bank.service.impl.AccountServiceImpl;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/transfer")
public class AccountServlet extends HttpServlet {
// 为了让这个对象在其他方法中也可以用。声明为成员变量。
private AccountService accountService = new AccountServiceImpl();
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 获取表单数据
String fromActno = request.getParameter("fromActno");
String toActno = request.getParameter("toActno");
double money = Double.parseDouble(request.getParameter("money"));
try {
// 调用service的转账方法完成转账。(调业务层)
accountService.transfer(fromActno, toActno, money);
// 程序能够走到这里,表示转账一定成功了。
// 调用View完成展示结果。
response.sendRedirect(request.getContextPath() + "/success.html");
} catch (MoneyNotEnoughException e) {
response.sendRedirect(request.getContextPath() + "/error1.html");
} catch (TransferException e) {
response.sendRedirect(request.getContextPath() + "/error2.html");
} catch (Exception e){
response.sendRedirect(request.getContextPath() + "/error2.html");
}
}
}
启动服务器,打开浏览器,输入地址:http://localhost:8080/bank,测试:
6.4 MyBatis对象作用域以及事务问题
6.4.1 MyBatis核心对象的作用域
6.4.1.1 SqlSessionFactoryBuilder
这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。 你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。
6.4.1.2 SqlSessionFactory
SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。
6.4.1.3 SqlSession
每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。 绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。 也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession。 如果你现在正在使用一种 Web 框架,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中。 换句话说,每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它。 这个关闭操作很重要,为了确保每次都能执行关闭操作,你应该把这个关闭操作放到 finally 块中。 下面的示例就是一个确保 SqlSession 关闭的标准模式:
try (SqlSession session = sqlSessionFactory.openSession()) {
// 你的应用逻辑代码
}
6.4.2 事务问题
在之前的转账业务中,更新了两个账户,我们需要保证它们的同时成功或同时失败,这个时候就需要使用事务机制,在transfer方法开始执行时开启事务,直到两个更新都成功之后,再提交事务,我们尝试将transfer方法进行如下修改:
package com.powernode.bank.service.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.dao.impl.AccountDaoImpl;
import com.powernode.bank.exception.AppException;
import com.powernode.bank.exception.MoneyNotEnoughException;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.service.AccountService;
import com.powernode.bank.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
public class AccountServiceImpl implements AccountService {
private AccountDao accountDao = new AccountDaoImpl();
@Override
public void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, AppException {
// 查询转出账户的余额
Account fromAct = accountDao.selectByActno(fromActno);
if (fromAct.getBalance() < money) {
throw new MoneyNotEnoughException("对不起,您的余额不足。");
}
try {
// 程序如果执行到这里说明余额充足
// 修改账户余额
Account toAct = accountDao.selectByActno(toActno);
fromAct.setBalance(fromAct.getBalance() - money);
toAct.setBalance(toAct.getBalance() + money);
// 更新数据库(添加事务)
SqlSession sqlSession = SqlSessionUtil.openSession();
accountDao.update(fromAct);
// 模拟异常
String s = null;
s.toString();
accountDao.update(toAct);
sqlSession.commit();
sqlSession.close();
} catch (Exception e) {
throw new AppException("转账失败,未知原因!");
}
}
}
运行前注意看数据库表中当前的数据:
执行程序:
再次查看数据库表中的数据:
傻眼了吧!!!事务出问题了,转账失败了,钱仍然是少了1万。这是什么原因呢?主要是因为service和dao中使用的SqlSession对象不是同一个。
怎么办?为了保证service和dao中使用的SqlSession对象是同一个,可以将SqlSession对象存放到ThreadLocal当中。修改SqlSessionUtil工具类:
SqlSessionUtil
package com.powernode.bank.utils;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
/**
* MyBatis工具类
*
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class SqlSessionUtil {
private static SqlSessionFactory sqlSessionFactory;
/**
* 类加载时初始化sqlSessionFactory对象
*/
static {
try {
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
} catch (Exception e) {
e.printStackTrace();
}
}
private static ThreadLocal<SqlSession> local = new ThreadLocal<>();
/**
* 每调用一次openSession()可获取一个新的会话,该会话支持自动提交。
*
* @return 新的会话对象
*/
public static SqlSession openSession() {
SqlSession sqlSession = local.get();
if (sqlSession == null) {
sqlSession = sqlSessionFactory.openSession();
local.set(sqlSession);
}
return sqlSession;
}
/**
* 关闭SqlSession对象
* @param sqlSession
*/
public static void close(SqlSession sqlSession){
if (sqlSession != null) {
sqlSession.close();
}
local.remove();
}
}
修改dao中的方法:AccountDaoImpl中所有方法中的提交commit和关闭close代码全部删除。
AccountDaoImpl
package com.powernode.bank.dao.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
public class AccountDaoImpl implements AccountDao {
@Override
public Account selectByActno(String actno) {
SqlSession sqlSession = SqlSessionUtil.openSession();
Account act = (Account)sqlSession.selectOne("account.selectByActno", actno);
return act;
}
@Override
public int update(Account act) {
SqlSession sqlSession = SqlSessionUtil.openSession();
int count = sqlSession.update("account.update", act);
return count;
}
}
修改service中的方法:
AccountServiceImpl
package com.powernode.bank.service.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.dao.impl.AccountDaoImpl;
import com.powernode.bank.exception.AppException;
import com.powernode.bank.exception.MoneyNotEnoughException;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.service.AccountService;
import com.powernode.bank.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
public class AccountServiceImpl implements AccountService {
private AccountDao accountDao = new AccountDaoImpl();
@Override
public void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, AppException {
// 查询转出账户的余额
Account fromAct = accountDao.selectByActno(fromActno);
if (fromAct.getBalance() < money) {
throw new MoneyNotEnoughException("对不起,您的余额不足。");
}
try {
// 程序如果执行到这里说明余额充足
// 修改账户余额
Account toAct = accountDao.selectByActno(toActno);
fromAct.setBalance(fromAct.getBalance() - money);
toAct.setBalance(toAct.getBalance() + money);
// 更新数据库(添加事务)
SqlSession sqlSession = SqlSessionUtil.openSession();
accountDao.update(fromAct);
// 模拟异常
String s = null;
s.toString();
accountDao.update(toAct);
sqlSession.commit();
SqlSessionUtil.close(sqlSession); // 只修改了这一行代码。
} catch (Exception e) {
throw new AppException("转账失败,未知原因!");
}
}
}
当前数据库表中的数据:
再次运行程序:
查看数据库表:没有问题。
再测试转账成功:
如果余额不足呢:
账户的余额依然正常:
6.5 分析当前程序存在的问题
我们来看一下DaoImpl的代码
AccountDaoImpl
package com.powernode.bank.dao.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
public class AccountDaoImpl implements AccountDao {
@Override
public Account selectByActno(String actno) {
SqlSession sqlSession = SqlSessionUtil.openSession();
Account act = (Account)sqlSession.selectOne("account.selectByActno", actno);
return act;
}
@Override
public int update(Account act) {
SqlSession sqlSession = SqlSessionUtil.openSession();
int count = sqlSession.update("account.update", act);
return count;
}
}
我们不难发现,这个dao实现类中的方法代码很固定,基本上就是一行代码,通过SqlSession对象调用insert、delete、update、select等方法,这个类中的方法没有任何业务逻辑,既然是这样,这个类我们能不能动态的生成,以后可以不写这个类吗?答案:可以。
7 使用javassist生成类
来自百度百科:
Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态"AOP"框架。
它比JDK的反射机制还要强大
7.1 Javassist的使用
我们要使用javassist,首先要引入它的依赖
javassist依赖
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.29.1-GA</version>
</dependency>
样例代码:
JavassistTest
package com.powernode.javassist;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.Modifier;
import java.lang.reflect.Method;
public class JavassistTest {
public static void main(String[] args) throws Exception {
// 获取类池
ClassPool pool = ClassPool.getDefault();
// 创建类
CtClass ctClass = pool.makeClass("com.powernode.javassist.Test");
// 创建方法
// 1.返回值类型 2.方法名 3.形式参数列表 4.所属类
CtMethod ctMethod = new CtMethod(CtClass.voidType, "execute", new CtClass[]{}, ctClass);
// 设置方法的修饰符列表
ctMethod.setModifiers(Modifier.PUBLIC);
// 设置方法体
ctMethod.setBody("{System.out.println(\"hello world\");}");
// 给类添加方法
ctClass.addMethod(ctMethod);
// 调用方法
Class<?> aClass = ctClass.toClass();
Object o = aClass.newInstance();
Method method = aClass.getDeclaredMethod("execute");
method.invoke(o);
}
}
运行要注意:加两个参数,要不然会有异常。(JDK8之前的不会有异常)
- --add-opens java.base/java.lang=ALL-UNNAMED
- --add-opens java.base/sun.net.util=ALL-UNNAMED
运行结果:
7.2 使用Javassit生成DaoImpl类
使用Javassist动态生成DaoImpl类
GenerateDaoByJavassist
package com.powernode.bank.utils;
import org.apache.ibatis.javassist.CannotCompileException;
import org.apache.ibatis.javassist.ClassPool;
import org.apache.ibatis.javassist.CtClass;
import org.apache.ibatis.javassist.CtMethod;
import org.apache.ibatis.session.SqlSession;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
/**
* 使用javassist库动态生成dao接口的实现类
*
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class GenerateDaoByJavassist {
/**
* 根据dao接口生成dao接口的代理对象
*
* @param sqlSession sql会话
* @param daoInterface dao接口
* @return dao接口代理对象
*/
public static Object getMapper(SqlSession sqlSession, Class daoInterface) {
ClassPool pool = ClassPool.getDefault();
// 生成代理类
CtClass ctClass = pool.makeClass(daoInterface.getPackageName() + ".impl." + daoInterface.getSimpleName() + "Impl");
// 接口
CtClass ctInterface = pool.makeClass(daoInterface.getName());
// 代理类实现接口
ctClass.addInterface(ctInterface);
// 获取所有的方法
Method[] methods = daoInterface.getDeclaredMethods();
Arrays.stream(methods).forEach(method -> {
// 拼接方法的签名
StringBuilder methodStr = new StringBuilder();
String returnTypeName = method.getReturnType().getName();
methodStr.append(returnTypeName);
methodStr.append(" ");
String methodName = method.getName();
methodStr.append(methodName);
methodStr.append("(");
Class<?>[] parameterTypes = method.getParameterTypes();
for (int i = 0; i < parameterTypes.length; i++) {
methodStr.append(parameterTypes[i].getName());
methodStr.append(" arg");
methodStr.append(i);
if (i != parameterTypes.length - 1) {
methodStr.append(",");
}
}
methodStr.append("){");
// 方法体当中的代码怎么写?
// 获取sqlId(这里非常重要:因为这行代码导致以后namespace必须是接口的全限定接口名,sqlId必须是接口中方法的方法名。)
String sqlId = daoInterface.getName() + "." + methodName;
// 获取SqlCommondType
String sqlCommondTypeName = sqlSession.getConfiguration().getMappedStatement(sqlId).getSqlCommandType().name();
if ("SELECT".equals(sqlCommondTypeName)) {
methodStr.append("org.apache.ibatis.session.SqlSession sqlSession = com.powernode.bank.utils.SqlSessionUtil.openSession();");
methodStr.append("Object obj = sqlSession.selectOne(\"" + sqlId + "\", arg0);");
methodStr.append("return (" + returnTypeName + ")obj;");
} else if ("UPDATE".equals(sqlCommondTypeName)) {
methodStr.append("org.apache.ibatis.session.SqlSession sqlSession = com.powernode.bank.utils.SqlSessionUtil.openSession();");
methodStr.append("int count = sqlSession.update(\"" + sqlId + "\", arg0);");
methodStr.append("return count;");
}
methodStr.append("}");
System.out.println(methodStr);
try {
// 创建CtMethod对象
CtMethod ctMethod = CtMethod.make(methodStr.toString(), ctClass);
ctMethod.setModifiers(Modifier.PUBLIC);
// 将方法添加到类
ctClass.addMethod(ctMethod);
} catch (CannotCompileException e) {
throw new RuntimeException(e);
}
});
try {
// 创建代理对象
Class<?> aClass = ctClass.toClass();
Constructor<?> defaultCon = aClass.getDeclaredConstructor();
Object o = defaultCon.newInstance();
return o;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
修改AccountMapper.xml文件:namespace必须是dao接口的全限定名称,id必须是dao接口中的方法名:
AccountMapper.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.powernode.bank.dao.AccountDao">
<select id="selectByActno" resultType="com.powernode.bank.pojo.Account">
select * from t_act where actno = #{actno}
</select>
<update id="update">
update t_act set balance = #{balance} where actno = #{actno}
</update>
</mapper>
修改service类中获取dao对象的代码:
启动服务器:启动过程中显示,tomcat服务器自动添加了以下的两个运行参数。所以不需要再单独配置。
测试前数据:
打开浏览器测试:
7.3 课堂程序(javassist-test)
com/powernode/bank/dao/AccountDao.java
package com.powernode.bank.dao;
public interface AccountDao {
void delete();
int insert(String actno);
int update(String actno, Double balance);
String selectByActno(String actno);
}
com/powernode/javassist/JavassistTest.java
package com.powernode.javassist;
import com.powernode.bank.dao.AccountDao;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.junit.Test;
import java.lang.reflect.Method;
import java.util.Arrays;
public class JavassistTest {
@Test
public void testGenerateAccountDaoImpl() throws Exception{
// 获取类池
ClassPool pool = ClassPool.getDefault();
// 制造类
CtClass ctClass = pool.makeClass("com.powernode.bank.dao.impl.AccountDaoImpl");
// 制造接口
CtClass ctInterface = pool.makeInterface("com.powernode.bank.dao.AccountDao");
// 实现接口
ctClass.addInterface(ctInterface);
// 实现接口中所有的方法
// 获取接口中所有的方法
Method[] methods = AccountDao.class.getDeclaredMethods();
Arrays.stream(methods).forEach(method -> {
// method是接口中的抽象方法
// 把method抽象方法给实现了。
try {
// public void delete(){}
// public int update(String actno, Double balance){}
StringBuilder methodCode = new StringBuilder();
methodCode.append("public "); // 追加修饰符列表
methodCode.append(method.getReturnType().getName()); // 追加返回值类型
methodCode.append(" ");
methodCode.append(method.getName()); //追加方法名
methodCode.append("(");
// 拼接参数 String actno, Double balance
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(11111); ");
// 动态的添加return语句
String returnTypeSimpleName = method.getReturnType().getSimpleName();
if ("void".equals(returnTypeSimpleName)) {
}else if("int".equals(returnTypeSimpleName)){
methodCode.append("return 1;");
}else if("String".equals(returnTypeSimpleName)){
methodCode.append("return \"hello\";");
}
methodCode.append("}");
System.out.println(methodCode);
CtMethod ctMethod = CtMethod.make(methodCode.toString(), ctClass);
ctClass.addMethod(ctMethod);
} catch (Exception e) {
e.printStackTrace();
}
});
// 在内存中生成class,并且加载到JVM当中
Class<?> clazz = ctClass.toClass();
// 创建对象
AccountDao accountDao = (AccountDao) clazz.newInstance();
// 调用方法
accountDao.insert("aaaaa");
accountDao.delete();
accountDao.update("aaaa", 1000.0);
accountDao.selectByActno("aaaa");
}
@Test
public void testGenerateImpl() throws Exception{
// 获取类池
ClassPool pool = ClassPool.getDefault();
// 制造类
CtClass ctClass = pool.makeClass("com.powernode.bank.dao.impl.AccountDaoImpl");
// 制造接口
CtClass ctInterface = pool.makeInterface("com.powernode.bank.dao.AccountDao");
// 添加接口到类中
ctClass.addInterface(ctInterface); // AccountDaoImpl implements AccountDao
// 实现接口中的方法
// 制造方法
CtMethod ctMethod = CtMethod.make("public void delete(){System.out.println(\"hello delete!\");}", ctClass);
// 将方法添加到类中
ctClass.addMethod(ctMethod);
// 在内存中生成类,同时将生成的类加载到JVM当中。
Class<?> clazz = ctClass.toClass();
AccountDao accountDao = (AccountDao)clazz.newInstance();
accountDao.delete();
}
@Test
public void testGenerateFirstClass() throws Exception{
// 获取类池,这个类池就是用来给我生成class的
ClassPool pool = ClassPool.getDefault();
// 制造类(需要告诉javassist,类名是啥)
CtClass ctClass = pool.makeClass("com.powernode.bank.dao.impl.AccountDaoImpl");
// 制造方法
String methodCode = "public void insert(){System.out.println(123);}";
CtMethod ctMethod = CtMethod.make(methodCode, ctClass);
// 将方法添加到类中
ctClass.addMethod(ctMethod);
// 在内存中生成class
ctClass.toClass();
// 类加载到JVM当中,返回AccountDaoImpl类的字节码
Class<?> clazz = Class.forName("com.powernode.bank.dao.impl.AccountDaoImpl");
// 创建对象
Object obj = clazz.newInstance();
// 获取AccountDaoImpl中的insert方法
Method insertMethod = clazz.getDeclaredMethod("insert");
// 调用方法insert
insertMethod.invoke(obj);
}
}
7.4 GenerateDaoProxy
mybatis-004-web:com/powernode/bank/utils/GenerateDaoProxy.java
在mybatis当中,mybatis提供了相关的机制。也可以动态为我们生成dao接口的实现类。(代理类:dao接口的代理)
mybatis当中实际上采用了代理模式。在内存中生成dao接口的代理类,然后创建代理类的实例。
使用mybatis的这种代理机制的前提:SqlMapper.xml文件中namespace必须是dao接口的全限定名称,id必须是dao接口中的方法名。
怎么用?代码怎么写?AccountDao accountDao = sqlSession.getMapper(AccountDao.class);
package com.powernode.bank.utils;
import org.apache.ibatis.javassist.ClassPool;
import org.apache.ibatis.javassist.CtClass;
import org.apache.ibatis.javassist.CtMethod;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.session.SqlSession;
import java.lang.reflect.Method;
import java.util.Arrays;
/**
* 工具类:可以动态的生成DAO的实现类。(或者说可以动态生成DAO的代理类)
* 注意注意注意注意注意!!!!!!:
* 凡是使用GenerateDaoProxy的,SQLMapper.xml映射文件中namespace必须是dao接口的全名,id必须是dao接口中的方法名。
* @author 动力节点
* @version 1.0
* @since 1.0
*/
public class GenerateDaoProxy { // GenerateDaoProxy是mybatis框架的开发者写的。
/**
* 生成dao接口实现类,并且将实现类的对象创建出来并返回。
* @param daoInterface dao接口
* @return dao接口实现类的实例化对象。
*/
public static Object generate(SqlSession sqlSession, Class daoInterface){
// 类池
ClassPool pool = ClassPool.getDefault();
// 制造类(com.powernode.bank.dao.AccountDao --> com.powernode.bank.dao.AccountDaoProxy)
CtClass ctClass = pool.makeClass(daoInterface.getName() + "Proxy"); // 实际本质上就是在内存中动态生成一个代理类。
// 制造接口
CtClass ctInterface = pool.makeInterface(daoInterface.getName());
// 实现接口
ctClass.addInterface(ctInterface);
// 实现接口中所有的方法
Method[] methods = daoInterface.getDeclaredMethods();
Arrays.stream(methods).forEach(method -> {
// method是接口中的抽象方法
// 将method这个抽象方法进行实现
try {
// Account selectByActno(String actno);
// public Account selectByActno(String actno){ 代码; }
StringBuilder methodCode = new StringBuilder();
methodCode.append("public ");
methodCode.append(method.getReturnType().getName());
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(")");
methodCode.append("{");
// 需要方法体当中的代码
methodCode.append("org.apache.ibatis.session.SqlSession sqlSession = com.powernode.bank.utils.SqlSessionUtil.openSession();");
// 需要知道是什么类型的sql语句
// sql语句的id是框架使用者提供的,具有多变性。对于我框架的开发人员来说。我不知道。
// 既然我框架开发者不知道sqlId,怎么办呢?mybatis框架的开发者于是就出台了一个规定:凡是使用GenerateDaoProxy机制的。
// sqlId都不能随便写。namespace必须是dao接口的全限定名称。id必须是dao接口中方法名。
String sqlId = daoInterface.getName() + "." + method.getName();
SqlCommandType sqlCommandType = sqlSession.getConfiguration().getMappedStatement(sqlId).getSqlCommandType();
if (sqlCommandType == SqlCommandType.INSERT) {
}
if (sqlCommandType == SqlCommandType.DELETE) {
}
if (sqlCommandType == SqlCommandType.UPDATE) {
methodCode.append("return sqlSession.update(\""+sqlId+"\", arg0);");
}
if (sqlCommandType == SqlCommandType.SELECT) {
String returnType = method.getReturnType().getName();
methodCode.append("return ("+returnType+")sqlSession.selectOne(\""+sqlId+"\", arg0);");
}
methodCode.append("}");
CtMethod ctMethod = CtMethod.make(methodCode.toString(), ctClass);
ctClass.addMethod(ctMethod);
} catch (Exception e) {
e.printStackTrace();
}
});
// 创建对象
Object obj = null;
try {
// 在内存中生成class,并且加载到JVM当中
Class<?> clazz = ctClass.toClass();
obj = clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}
}
8 MyBatis中接口代理机制及使用
好消息!!!其实以上所讲内容mybatis内部已经实现了。直接调用以下代码即可获取dao接口的代理类:
使用mybatis获取dao接口代理类对象
AccountDao accountDao = (AccountDao)sqlSession.getMapper(AccountDao.class);
使用以上代码的前提是:AccountMapper.xml文件中的namespace必须和dao接口的全限定名称一致,id必须和dao接口中方法名一致。
将service中获取dao对象的代码再次修改,如下:
测试前数据:
测试后数据:
8.1 面向接口的方式进行CRUD
一般使用mybatis的话,一般不叫做XXXDao了。一般都是XXXMapper
com/powernode/mybatis/mapper/CarMapper.java
package com.powernode.mybatis.mapper; // 包名也有叫做:com.powernode.mybatis.dao
//mapper包就是dao包。
import com.powernode.mybatis.pojo.Car;
import java.util.List;
// 一般使用mybatis的话,一般不叫做XXXDao了。一般都是XXXMapper。
public interface CarMapper { //CarDao。
/**
* 新增Car
* @param car
* @return
*/
int insert(Car car);
/**
* 根据id删除Car
* @param id
* @return
*/
int deleteById(Long id);
/**
* 修改汽车信息
* @param car
* @return
*/
int update(Car car);
/**
* 根据id查询汽车信息
* @param id
* @return
*/
Car selectById(Long id);
/**
* 获取所有的汽车信息。
* @return
*/
List<Car> selectAll();
}
CarMapper.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.powernode.mybatis.mapper.CarMapper">
<insert id="insert">
insert into t_car values(null, #{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
</insert>
<delete id="deleteById">
delete from t_car where id = #{id}
</delete>
<update id="update">
update t_car set
car_num=#{carNum},brand=#{brand},guide_price=#{guidePrice},produce_time=#{produceTime},car_type=#{carType}
where id = #{id}
</update>
<select id="selectById" resultType="com.powernode.mybatis.pojo.Car">
select
id,
car_num as carNum,
brand,
guide_price as guidePrice,
produce_time as produceTime,
car_type as carType
from t_car where id = #{id}
</select>
<select id="selectAll" resultType="com.powernode.mybatis.pojo.Car">
select
id,
car_num as carNum,
brand,
guide_price as guidePrice,
produce_time as produceTime,
car_type as carType
from t_car
</select>
</mapper>
com/powernode/mybatis/test/CarMapperTest.java
package com.powernode.mybatis.test;
import com.powernode.mybatis.mapper.CarMapper;
import com.powernode.mybatis.pojo.Car;
import com.powernode.mybatis.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;
import java.util.List;
public class CarMapperTest {
@Test
public void testInsert(){
SqlSession sqlSession = SqlSessionUtil.openSession();
// 面向接口获取接口的代理对象
// mapper实际上就是daoImpl对象.
// 底层不但为CarMapper接口生成了字节码,并且还new实现类对象了。
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
Car car = new Car(null, "4444", "奔驰C200", 32.0, "2000-10-10", "新能源");
int count = mapper.insert(car);
System.out.println(count);
sqlSession.commit();
}
@Test
public void testDeleteById(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
int count = mapper.deleteById(154L);
System.out.println(count);
sqlSession.commit();
}
@Test
public void testUpdate(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
Car car = new Car(155L, "2222", "凯美瑞222", 3.0, "2000-10-10", "新能源");
mapper.update(car);
sqlSession.commit();
}
@Test
public void testSelectById(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
Car car = mapper.selectById(155L);
System.out.println(car);
}
@Test
public void testSelectAll(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
List<Car> cars = mapper.selectAll();
cars.forEach(car -> System.out.println(car));
}
}
9 MyBatis小技巧
9.1 #{}和${}
#{}:先编译sql语句,再给占位符传值,底层是PreparedStatement实现。可以防止sql注入,比较常用。
${}:先进行sql语句拼接,然后再编译sql语句,底层是Statement实现。存在sql注入现象。只有在需要进行sql语句关键字拼接的情况下才会用到。
需求:根据car_type查询汽车
模块名:mybatis-005-antic
9.1.1 使用#{}
依赖
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.powernode</groupId>
<artifactId>mybatis-005-antic</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<!--mybatis依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.10</version>
</dependency>
<!--mysql驱动依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</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.2.11</version>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
</project>
jdbc.properties放在类的根路径下
jdbc.properties
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/powernode
jdbc.username=root
jdbc.password=root
logback.xml,可以拷贝之前的,放到类的根路径下
utils
SqlSessionUtil
package com.powernode.mybatis.utils;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
/**
* MyBatis工具类
*
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class SqlSessionUtil {
private static SqlSessionFactory sqlSessionFactory;
/**
* 类加载时初始化sqlSessionFactory对象
*/
static {
try {
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
} catch (Exception e) {
e.printStackTrace();
}
}
private static ThreadLocal<SqlSession> local = new ThreadLocal<>();
/**
* 每调用一次openSession()可获取一个新的会话,该会话支持自动提交。
*
* @return 新的会话对象
*/
public static SqlSession openSession() {
SqlSession sqlSession = local.get();
if (sqlSession == null) {
sqlSession = sqlSessionFactory.openSession();
local.set(sqlSession);
}
return sqlSession;
}
/**
* 关闭SqlSession对象
* @param sqlSession
*/
public static void close(SqlSession sqlSession){
if (sqlSession != null) {
sqlSession.close();
}
local.remove();
}
}
pojo
Car
package com.powernode.mybatis.pojo;
/**
* 普通实体类:汽车
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class Car {
private Long id;
private String carNum;
private String brand;
private Double guidePrice;
private String produceTime;
private String carType;
// 构造方法
// set get方法
// toString方法
}
mapper接口
CarMapper
package com.powernode.mybatis.mapper;
import com.powernode.mybatis.pojo.Car;
import java.util.List;
/**
* Car的sql映射对象
* @author 老杜
* @version 1.0
* @since 1.0
*/
public interface CarMapper {
/**
* 根据car_num获取Car
* @param carType
* @return
*/
List<Car> selectByCarType(String carType);
}
mybatis-config.xml,放在类的根路径下
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>
<properties resource="jdbc.properties"/>
<environments default="dev">
<environment id="dev">
<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>
<mapper resource="CarMapper.xml"/>
</mappers>
</configuration>
CarMapper.xml,放在类的根路径下:注意namespace必须和接口名一致。id必须和接口中方法名一致。
CarMapper.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.powernode.mybatis.mapper.CarMapper">
<select id="selectByCarType" resultType="com.powernode.mybatis.pojo.Car">
select
id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
from
t_car
where
car_type = #{carType}
</select>
</mapper>
测试程序
CarMapperTest
package com.powernode.mybatis.test;
import com.powernode.mybatis.mapper.CarMapper;
import com.powernode.mybatis.pojo.Car;
import com.powernode.mybatis.utils.SqlSessionUtil;
import org.junit.Test;
import java.util.List;
/**
* CarMapper测试类
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class CarMapperTest {
@Test
public void testSelectByCarType(){
CarMapper mapper = (CarMapper) SqlSessionUtil.openSession().getMapper(CarMapper.class);
List<Car> cars = mapper.selectByCarType("燃油车");
cars.forEach(car -> System.out.println(car));
}
}
执行结果:
通过执行可以清楚的看到,sql语句中是带有 ? 的,这个 ? 就是大家在JDBC中所学的占位符,专门用来接收值的。
把“燃油车”以String类型的值,传递给 ?
这就是 #{},它会先进行sql语句的预编译,然后再给占位符传值
9.1.2 使用${}
同样的需求,我们使用${}来完成
CarMapper.xml文件修改如下:
CarMapper.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.powernode.mybatis.mapper.CarMapper">
<select id="selectByCarType" resultType="com.powernode.mybatis.pojo.Car">
select
id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
from
t_car
where
<!--car_type = #{carType}-->
car_type = ${carType}
</select>
</mapper>
再次运行测试程序:
出现异常了,这是为什么呢?看看生成的sql语句:
很显然,${} 是先进行sql语句的拼接,然后再编译,出现语法错误是正常的,因为 燃油车 是一个字符串,在sql语句中应该添加单引号
修改:
CarMapper.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.powernode.mybatis.mapper.CarMapper">
<select id="selectByCarType" resultType="com.powernode.mybatis.pojo.Car">
select
id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
from
t_car
where
<!--car_type = #{carType}-->
<!--car_type = ${carType}-->
car_type = '${carType}'
</select>
</mapper>
再执行测试程序:
通过以上测试,可以看出,对于以上这种需求来说,还是建议使用 #{} 的方式。
原则:能用 #{} 就不用 ${}
9.1.3 什么情况下必须使用${}
当需要进行sql语句关键字拼接的时候。必须使用${}
需求:通过向sql语句中注入asc或desc关键字,来完成数据的升序或降序排列。
- 先使用#{}尝试:
CarMapper接口:
CarMapper
/**
* 查询所有的汽车信息。然后通过asc升序,desc降序。
* @param ascOrDesc
* @return
*/
List<Car> selectAllByAscOrDesc(String ascOrDesc);
CarMapper.xml文件:
CarMapper.xml
<select id="selectAllByAscOrDesc" resultType="car">
select
id,
car_num as carNum,
brand,
guide_price as guidePrice,
produce_time as produceTime,
car_type as carType
from
t_car
order by
produce_time #{ascOrDesc}
</select>
测试程序
CarMapperTest.testSelectAll
@Test
public void testSelectAllByAscOrDesc(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
List<Car> cars = mapper.selectAllByAscOrDesc("desc");
cars.forEach(car -> System.out.println(car));
sqlSession.close();
}
运行:
报错的原因是sql语句不合法,因为采用这种方式传值,最终sql语句会是这样:
select id,car_num as carNum,brand,guide_price as guidePrice,
produce_time as produceTime,car_type as carType
from t_car order by carNum 'desc'
desc是一个关键字,不能带单引号的,所以在进行sql语句关键字拼接的时候,必须使用${}
- 使用${} 改造
CarMapper.xml
<select id="selectAllByAscOrDesc" resultType="car">
select
id,
car_num as carNum,
brand,
guide_price as guidePrice,
produce_time as produceTime,
car_type as carType
from
t_car
order by
produce_time ${ascOrDesc}
</select>
再次执行测试程序:
9.1.4 拼接表名
业务背景:实际开发中,有的表数据量非常庞大,可能会采用分表方式进行存储,比如每天生成一张表,表的名字与日期挂钩,例如:2022年8月1日生成的表:t_user20220108。2000年1月1日生成的表:t_user20000101。此时前端在进行查询的时候会提交一个具体的日期,比如前端提交的日期为:2000年1月1日,那么后端就会根据这个日期动态拼接表名为:t_user20000101。有了这个表名之后,将表名拼接到sql语句当中,返回查询结果。那么大家思考一下,拼接表名到sql语句当中应该使用#{} 还是 ${} 呢?
使用#{}会是这样:select * from 't_car'
使用${}会是这样:select * from t_car
CarMapper.xml
<select id="selectAllByTableName" resultType="car">
select
id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
from
${tableName}
</select>
CarMapper接口
/**
* 根据表名查询所有的Car
* @param tableName
* @return
*/
List<Car> selectAllByTableName(String tableName);
CarMapperTest.testSelectAllByTableName
@Test
public void testSelectAllByTableName(){
CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
List<Car> cars = mapper.selectAllByTableName("t_car");
cars.forEach(car -> System.out.println(car));
}
执行结果:
9.1.5 批量删除
业务背景:一次删除多条记录。
对应的sql语句:
delete from t_user where id = 1 or id = 2 or id = 3;
delete from t_user where id in(1, 2, 3);
假设现在使用in的方式处理,前端传过来的字符串:1, 2, 3
如果使用mybatis处理,应该使用#{} 还是 ${}
使用#{} :delete from t_user where id in('1,2,3') 执行错误:1292 - Truncated incorrect DOUBLE value: '1,2,3'
使用${} :delete from t_user where id in(1, 2, 3)
CarMapper接口
/**
* 根据id批量删除
* @param ids
* @return
*/
int deleteBatch(String ids);
CarMapper.xml
<delete id="deleteBatch">
delete from t_car where id in(${ids})
</delete>
CarMapperTest.testDeleteBatch
@Test
public void testDeleteBatch(){
CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
int count = mapper.deleteBatch("1,2,3");
System.out.println("删除了几条记录:" + count);
SqlSessionUtil.openSession().commit();
}
执行结果:
9.1.6 模糊查询
需求:查询奔驰系列的汽车。【只要品牌brand中含有奔驰两个字的都查询出来。】
9.1.6.1 使用${}
CarMapper接口
/**
* 根据品牌进行模糊查询
* @param likeBrank
* @return
*/
List<Car> selectLikeByBrand(String likeBrank);
CarMapper.xml
<select id="selectLikeByBrand" resultType="Car">
select
id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
from
t_car
where
brand like '%${brand}%'
</select>
CarMapperTest.testSelectLikeByBrand
@Test
public void testSelectLikeByBrand(){
CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
List<Car> cars = mapper.selectLikeByBrand("奔驰");
cars.forEach(car -> System.out.println(car));
}
执行结果:
9.1.6.2 使用#{}
第一种:concat函数
CarMapper.xml
<select id="selectLikeByBrand" resultType="Car">
select
id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
from
t_car
where
brand like concat('%',#{brand},'%')
</select>
执行结果:
第二种:双引号方式
CarMapper.xml
<select id="selectLikeByBrand" resultType="Car">
select
id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
from
t_car
where
brand like "%"#{brand}"%"
</select>
9.2 typeAliases
我们来观察一下CarMapper.xml中的配置信息:
CarMapper.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.powernode.mybatis.mapper.CarMapper">
<select id="selectAll" resultType="com.powernode.mybatis.pojo.Car">
select
id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
from
t_car
order by carNum ${key}
</select>
<select id="selectByCarType" resultType="com.powernode.mybatis.pojo.Car">
select
id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
from
t_car
where
car_type = '${carType}'
</select>
</mapper>
resultType属性用来指定查询结果集的封装类型,这个名字太长,可以起别名吗?可以。
在mybatis-config.xml文件中使用typeAliases标签来起别名,包括两种方式:
9.2.1 第一种方式:typeAlias
mybatis-config.xml
<typeAliases>
<!--
type: 指定给哪个类型起别名
alias:指定别名
注意:别名不区分大小写。
alias属性是可以省略的。有默认的别名。
-->
<!--<typeAlias type="com.powernode.mybatis.pojo.Car" alias="aaa"/>
<typeAlias type="com.powernode.mybatis.pojo.Log" alias="bbb"/>-->
<!--省略alias之后,别名就是类的简名,比如:com.powernode.mybatis.pojo.Car的别名就是Car/car/cAR/cAr,不区分大小写。 -->
<!--<typeAlias type="com.powernode.mybatis.pojo.Car"/>
<typeAlias type="com.powernode.mybatis.pojo.Log"/>-->
<!--将这个包下的所有的类全部自动起别名。别名就是类简名。不区分大小写。-->
<package name="com.powernode.mybatis.pojo"/>
<typeAlias type="com.powernode.mybatis.pojo.Car" alias="Car"/>
</typeAliases>
namespace不能使用别名机制。必须写全限定接口名称。带有包名的。
首先要注意typeAliases标签的放置位置,如果不清楚的话,可以看看错误提示信息。
typeAliases标签中的typeAlias可以写多个。
typeAlias:
type属性:指定给哪个类起别名
alias属性:别名。
alias属性不是必须的,如果缺省的话,type属性指定的类型名的简类名作为别名。
alias是大小写不敏感的。也就是说假设alias="Car",再用的时候,可以CAR,也可以car,也可以Car,都行。
9.2.2 第二种方式:package
如果一个包下的类太多,每个类都要起别名,会导致typeAlias标签配置较多,所以mybatis用提供package的配置方式,只需要指定包名,该包下的所有类都自动起别名,别名就是简类名。并且别名不区分大小写。
mybatis-config.xml
<typeAliases>
<package name="com.powernode.mybatis.pojo"/>
</typeAliases>
package也可以配置多个的。
9.2.3 在SQL映射文件中用一下
CarMapper.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.powernode.mybatis.mapper.CarMapper">
<select id="selectAll" resultType="CAR">
select
id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
from
t_car
order by carNum ${key}
</select>
<select id="selectByCarType" resultType="car">
select
id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
from
t_car
where
car_type = '${carType}'
</select>
</mapper>
运行测试程序:正常。
9.3 mappers
SQL映射文件的配置方式包括四种:
- resource:从类路径中加载
- url:从指定的全限定资源路径中加载
- class:使用映射器接口实现类的完全限定类名
- package:将包内的映射器接口实现全部注册为映射器
9.3.1 resource
这种方式是从类路径中加载配置文件,所以这种方式要求SQL映射文件必须放在resources目录下或其子目录下。
<mappers>
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
<mapper resource="org/mybatis/builder/BlogMapper.xml"/>
<mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
9.3.2 url
这种方式显然使用了绝对路径的方式,这种配置对SQL映射文件存放的位置没有要求,随意。
<mappers>
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
<mapper url="file:///var/mappers/BlogMapper.xml"/>
<mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
9.3.3 class
如果使用这种方式必须满足以下条件:
- SQL映射文件和mapper接口放在同一个目录下。
- SQL映射文件的名字也必须和mapper接口名一致。
<!-- 使用映射器接口实现类的完全限定类名 -->
<mappers>
<mapper class="org.mybatis.builder.AuthorMapper"/>
<mapper class="org.mybatis.builder.BlogMapper"/>
<mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
将CarMapper.xml文件移动到和mapper接口同一个目录下:
在resources目录下新建:com/powernode/mybatis/mapper【这里千万要注意:不能这样新建 com.powernode.mybatis.dao】
将CarMapper.xml文件移动到mapper目录下
修改mybatis-config.xml文件
mybatis-config.xml
<!--使用这种方式的前提:CarMapper.xml文件的位置不能随便放-->
<!--必须和CarMapper接口放在一起。-->
<!--XML文件的名字必须和接口名一致。-->
<mappers>
<mapper class="com.powernode.mybatis.mapper.CarMapper"/>
</mappers>
运行程序:正常!!!
9.3.4 package
如果class较多,可以使用这种package的方式,但前提条件和上一种方式一样。
mybatis-config.xml
<!-- 这种方式在实际开发中是使用的。-->
<!--前提是:XML文件必须和接口放在一起。并且名字一致。-->
<!-- 将包内的映射器接口实现全部注册为映射器 -->
<mappers>
<package name="com.powernode.mybatis.mapper"/>
</mappers>
9.4 idea配置文件模板
mybatis-config.xml和SqlMapper.xml文件可以在IDEA中提前创建好模板,以后通过模板创建配置文件。
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>
<properties resource=""/>
<typeAliases>
<package name=""/>
</typeAliases>
<environments default="dev">
<environment id="dev">
<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>
<package name=""/>
</mappers>
</configuration>
sqlMapper.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="">
</mapper>
9.5 插入数据时获取自动生成的主键
前提是:主键是自动生成的。
业务背景:一个用户有多个角色。(一对多,两张表,多的表加外键)
插入一条新的记录之后,自动生成了主键,而这个主键需要在其他表中使用时。
插入一个用户数据的同时需要给该用户分配角色:需要将生成的用户的id插入到角色表的user_id字段上。
第一种方式:可以先插入用户数据,再写一条查询语句获取id,然后再插入user_id字段。【比较麻烦】
第二种方式:mybatis提供了一种方式更加便捷。(建立在id主键值不是我们指定的,是由数据库自动生成的基础上,在插入数据时直接获取到)
CarMapper接口
/**
* 插入Car信息,并且获取自动生成的主键值
* @param car
*/
void insertUseGeneratedKeys(Car car);
CarMapper.xml
<!--
useGeneratedKeys="true" 使用自动生成的主键值。
keyProperty="id" 指定主键值赋值给对象的哪个属性。这个就表示将主键值赋值给Car对象的id属性。
-->
<insert id="insertUseGeneratedKeys" useGeneratedKeys="true" keyProperty="id">
insert into t_car(id,car_num,brand,guide_price,produce_time,car_type) values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
</insert>
CarMapperTest.testInsertUseGeneratedKeys
@Test
public void testInsertUseGeneratedKeys(){
CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
Car car = new Car();
car.setCarNum("5262");
car.setBrand("BYD汉");
car.setGuidePrice(30.3);
car.setProduceTime("2020-10-11");
car.setCarType("新能源");
mapper.insertUseGeneratedKeys(car);
SqlSessionUtil.openSession().commit();
System.out.println(car.getId());
}
9.6 课堂笔记(mybatis-006-antic)
readme.txt
mybatis小技巧
1. #{}和${}的区别
#{}的执行结果:
[main] DEBUG c.p.mybatis.mapper.CarMapper.selectByCarType - ==> Preparing: select id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car where car_type = ?
[main] DEBUG c.p.mybatis.mapper.CarMapper.selectByCarType - ==> Parameters: 新能源(String)
[main] DEBUG c.p.mybatis.mapper.CarMapper.selectByCarType - <== Total: 2
${}的执行结果:
[main] DEBUG c.p.mybatis.mapper.CarMapper.selectByCarType - ==> Preparing: select id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car where car_type = 新能源
[main] DEBUG c.p.mybatis.mapper.CarMapper.selectByCarType - ==> Parameters:
org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: java.sql.SQLSyntaxErrorException: Unknown column '新能源' in 'where clause'
### The error may exist in CarMapper.xml
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: select id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car where car_type = 新能源
### Cause: java.sql.SQLSyntaxErrorException: Unknown column '新能源' in 'where clause'
#{}和${}的区别:
#{}: 底层使用PreparedStatement。特点:先进行SQL语句的编译,然后给SQL语句的占位符问号?传值。可以避免SQL注入的风险。
${}:底层使用Statement。特点:先进行SQL语句的拼接,然后再对SQL语句进行编译。存在SQL注入的风险。
优先使用#{},这是原则。避免SQL注入的风险。
#{}的执行结果:
Preparing: select
id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType
from t_car order by produce_time ?
Parameters: asc(String)
select
id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType
from t_car order by produce_time 'asc'
${}的执行结果:
Preparing:
select id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType
from t_car order by produce_time asc
Parameters:
如果需要SQL语句的关键字放到SQL语句中,只能使用${},因为#{}是以值的形式放到SQL语句当中的。
2. 向SQL语句当中拼接表名,就需要使用${}
现实业务当中,可能会存在分表存储数据的情况。因为一张表存的话,数据量太大。查询效率比较低。
可以将这些数据有规律的分表存储,这样在查询的时候效率就比较高。因为扫描的数据量变少了。
日志表:专门存储日志信息的。如果t_log只有一张表,这张表中每一天都会产生很多log,慢慢的,这个表中数据会很多。
怎么解决问题?
可以每天生成一个新表。每张表以当天日期作为名称,例如:
t_log_20220901
t_log_20220902
....
你想知道某一天的日志信息怎么办?
假设今天是20220901,那么直接查:t_log_20220901的表即可。
3.批量删除:一次删除多条记录。
批量删除的SQL语句有两种写法:
第一种or:delete from t_car where id=1 or id=2 or id=3;
第二种int:delete from t_car where id in(1,2,3);
应该采用${}的方式:
delete from t_car where id in(${ids});
4.模糊查询:like
需求:根据汽车品牌进行模糊查询
select * from t_car where brand like '%奔驰%';
select * from t_car where brand like '%比亚迪%';
第一种方案:
'%${brand}%'
第二种方案:concat函数,这个是mysql数据库当中的一个函数,专门进行字符串拼接
concat('%',#{brand},'%') //concat('%','奔驰','%')
第三种方案:比较鸡肋了。可以不算。
concat('%','${brand}','%')
第四种方案:(重点掌握这种)
"%"#{brand}"%"
5. 关于MyBatis中别名机制:
<typeAliases>
<!--别名自己指定的-->
<typeAlias type="com.powernode.mybatis.pojo.Car" alias="aaa"/>
<typeAlias type="com.powernode.mybatis.pojo.Log" alias="bbb"/>
<!--采用默认的别名机制-->
<typeAlias type="com.powernode.mybatis.pojo.Car"/>
<typeAlias type="com.powernode.mybatis.pojo.Log"/>
<!--包下所有的类自动起别名。使用简名作为别名。-->
<package name="com.powernode.mybatis.pojo"/>
</typeAliases>
所有别名不区分大小写。
namespace不能使用别名机制。
6. mybatis-config.xml文件中的mappers标签。
<mapper resource="CarMapper.xml"/> 要求类的根路径下必须有:CarMapper.xml
<mapper url="file:///d:/CarMapper.xml"/> 要求在d:/下有CarMapper.xml文件
<mapper class="全限定接口名,带有包名"/>
mapper标签的属性可以有三个:
resource:这种方式是从类的根路径下开始查找资源。采用这种方式的话,你的配置文件需要放到类路径当中才行。
url: 这种方式是一种绝对路径的方式,这种方式不要求配置文件必须放到类路径当中,哪里都行,只要提供一个绝对路径就行。这种方式使用极少,因为移植性太差。
class: 这个位置提供的是mapper接口的全限定接口名,必须带有包名的。
思考:mapper标签的作用是指定SqlMapper.xml文件的路径,指定接口名有什么用呢?
<mapper class="com.powernode.mybatis.mapper.CarMapper"/>
如果你class指定是:com.powernode.mybatis.mapper.CarMapper
那么mybatis框架会自动去com/powernode/mybatis/mapper目录下查找CarMapper.xml文件。
注意:也就是说:如果你采用这种方式,那么你必须保证CarMapper.xml文件和CarMapper接口必须在同一个目录下。并且名字一致。
CarMapper接口-> CarMapper.xml
LogMapper接口-> LogMapper.xml
....
提醒!!!!!!!!!!!!!!!!!!!!!!!
在IDEA的resources目录下新建多重目录的话,必须是这样创建:
com/powernode/mybatis/mapper
不能这样:
com.powernode.mybatis.mapper
提醒!!!!!!!!!!!!!!!!!!!!!!!
在IDEA的resources目录下新建多重目录的话,必须是这样创建:
com/powernode/mybatis/mapper
不能这样:
com.powernode.mybatis.mapper
10 MyBatis参数处理
模块名:mybatis-007-param
表:t_student
表中现有数据:
pojo类:
Student
package com.powernode.mybatis.pojo;
import java.util.Date;
/**
* 学生类
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class Student {
private Long id;
private String name;
private Integer age;
private Double height;
private Character sex;
private Date birth;
// constructor
// setter and getter
// toString
}
10.1 单个简单类型参数
简单类型包括:
- byte short int long float double char
- Byte Short Integer Long Float Double Character
- String
- java.util.Date
- java.sql.Date
需求:根据name查、根据id查、根据birth查、根据sex查
StudentMapper
package com.powernode.mybatis.mapper;
import com.powernode.mybatis.pojo.Student;
import java.util.Date;
import java.util.List;
/**
* 学生数据Sql映射器
* @author 老杜
* @version 1.0
* @since 1.0
*/
public interface StudentMapper {
/**
* 当接口中的方法的参数只有一个(单个参数),并且参数的数据类型都是简单类型。
* 根据id查询、name查询、birth查询、sex查询
*/
/**
* 根据name查询
* @param name
* @return
*/
List<Student> selectByName(String name);
/**
* 根据id查询
* @param id
* @return
*/
Student selectById(Long id);
/**
* 根据birth查询
* @param birth
* @return
*/
List<Student> selectByBirth(Date birth);
/**
* 根据sex查询
* @param sex
* @return
*/
List<Student> selectBySex(Character sex);
}
StudentMapper.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.powernode.mybatis.mapper.StudentMapper">
<!--
List<Student> selectById(Long id);
List<Student> selectByName(String name);
List<Student> selectByBirth(Date birth);
List<Student> selectBySex(Character sex);
parameterType属性的作用:
告诉mybatis框架,我这个方法的参数类型是什么类型。
mybatis框架自身带有类型自动推断机制,所以大部分情况下parameterType属性都是可以省略不写的。
SQL语句最终是这样的:
select * from t_student where id = ?
JDBC代码是一定要给?传值的。
怎么传值?ps.setXxx(第几个问号, 传什么值);
ps.setLong(1, 1L);
ps.setString(1, "zhangsan");
ps.setDate(1, new Date());
ps.setInt(1, 100);
...
mybatis底层到底调用setXxx的哪个方法,取决于parameterType属性的值。
注意:mybatis框架实际上内置了很多别名。可以参考开发手册。
-->
<select id="selectById" resultType="Student" parameterType="long">
select * from t_student where id = #{id}
</select>
<select id="selectByName" resultType="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="selectBySex" resultType="student">
select * from t_student where sex = #{sex}
</select>
</mapper>
StudentMapperTest
package com.powernode.mybatis.test;
import com.powernode.mybatis.mapper.StudentMapper;
import com.powernode.mybatis.pojo.Student;
import com.powernode.mybatis.utils.SqlSessionUtil;
import org.junit.Test;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
public class StudentMapperTest {
StudentMapper mapper = SqlSessionUtil.openSession().getMapper(StudentMapper.class);
@Test
public void testSelectByName(){
List<Student> students = mapper.selectByName("张三");
students.forEach(student -> System.out.println(student));
}
@Test
public void testSelectById(){
Student student = mapper.selectById(2L);
System.out.println(student);
}
// java.util.Date java.sql.Date,他们都是简单类型。
@Test
public void testSelectByBirth(){
try {
Date birth = new SimpleDateFormat("yyyy-MM-dd").parse("2022-08-16");
List<Student> students = mapper.selectByBirth(birth);
students.forEach(student -> System.out.println(student));
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
@Test
public void testSelectBySex(){
List<Student> students = mapper.selectBySex('男');
students.forEach(student -> System.out.println(student));
}
}
通过测试得知,简单类型对于mybatis来说都是可以自动类型识别的:
- 也就是说对于mybatis来说,它是可以自动推断出ps.setXxxx()方法的。ps.setString()还是ps.setInt()。它可以自动推断。
其实SQL映射文件中的配置比较完整的写法是:
StudentMapper.xml
<select id="selectByName" resultType="student" parameterType="java.lang.String">
select * from t_student where name = #{name, javaType=String, jdbcType=VARCHAR}
</select>
其中sql语句中的javaType,jdbcType,以及select标签中的parameterType属性,都是用来帮助mybatis进行类型确定的。不过这些配置多数是可以省略的。因为mybatis它有强大的自动类型推断机制。
- javaType:可以省略
- jdbcType:可以省略
- parameterType:可以省略
如果参数只有一个的话,#{} 里面的内容就随便写了。对于 ${} 来说,注意加单引号。
10.2 Map参数
需求:根据name和age查询
StudentMapper接口
/**
* 查询学生信息,通过Map参数。以下是单个参数。但是参数的类型不是简单类型。是Map集合。
* 根据name和age查询
* @param paramMap
* @return
*/
List<Student> selectByParamMap(Map<String,Object> paramMap);
StudentMapperTest.testSelectByParamMap
@Test
public void testSelectByParamMap(){
// 准备Map
Map<String,Object> paramMap = new HashMap<>();
paramMap.put("nameKey", "张三");
paramMap.put("ageKey", 20);
List<Student> students = mapper.selectByParamMap(paramMap);
students.forEach(student -> System.out.println(student));
}
StudentMapper.xml
<select id="selectByParamMap" resultType="student">
select * from t_student where name = #{nameKey} and age = #{ageKey}
</select>
测试运行正常。
这种方式是手动封装Map集合,将每个条件以key和value的形式存放到集合中。然后在使用的时候通过#{map集合的key}来取值。
10.3 实体类参数
需求:插入一条Student数据
StudentMapper接口
/**
* 保存学生信息,通过POJO参数。Student是单个参数。但是不是简单类型。
* @param student
* @return
*/
int insertStudentByPOJO(Student student);
StudentMapper.xml
<!--<insert id="insertStudentByPOJO" parameterType="student">-->
<insert id="insertStudentByPOJO">
insert into t_student(id,name,age,sex,birth,height) values(null,#{name},#{age},#{sex},#{birth},#{height})
</insert>
StudentMapperTest.testInsert
@Test
public void testInsertStudentByPOJO(){
SqlSession sqlSession = SqlSessionUtil.openSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
// POJO对象
Student student = new Student();
student.setName("张飞");
student.setAge(50);
student.setSex('女');
student.setBirth(new Date());
student.setHeight(10.0);
mapper.insertStudentByPOJO(student);
sqlSession.commit();
sqlSession.close();
}
运行正常,数据库中成功添加一条数据。
这里需要注意的是:#{} 里面写的是属性名字。这个属性名其本质上是:set/get方法名去掉set/get之后的名字。
10.4 多参数
需求:通过name和sex查询
StudentMapper接口
/**
* 这是多参数。
* 根据name和sex查询Student信息。
* 如果是多个参数的话,mybatis框架底层是怎么做的呢?
* mybatis框架会自动创建一个Map集合。并且Map集合是以这种方式存储参数的:
* map.put("arg0", name);
* map.put("arg1", sex);
* map.put("param1", name);
* map.put("param2", sex);
*
* @param name
* @param sex
* @return
*/
List<Student> selectByNameAndSex(String name, Character sex);
StudentMapper.xml
<select id="selectByNameAndSex" resultType="student">
select * from t_student where name = #{name} and sex = #{sex}
</select>
StudentMapperTest.testSelectByNameAndSex
@Test
public void testSelectByNameAndSex(){
List<Student> students = mapper.selectByNameAndSex("张三", '女');
students.forEach(student -> System.out.println(student));
}
执行结果:
异常信息描述了:name参数找不到,可用的参数包括[arg1, arg0, param1, param2]
修改StudentMapper.xml配置文件:尝试使用[arg1, arg0, param1, param2]去参数
StudentMapper.xml
<select id="selectByNameAndSex" resultType="student">
<!--select * from t_student where name = #{name} and sex = #{sex}-->
select * from t_student where name = #{arg0} and sex = #{arg1}
</select>
运行结果:
再次尝试修改StudentMapper.xml文件
StudentMapper.xml
<!--
注意:低版本的mybatis中,使用的是:#{0}和#{1},以及#{2}...
高版本的mybatis中,使用的是:
#{arg0}
#{arg1}
#{arg2}
#{arg3}
#{arg4}
#{param1}
#{param2}
#{param3}
#{param4}
-->
<select id="selectByNameAndSex" resultType="student">
<!--select * from t_student where name = #{name} and sex = #{sex}-->
<!--select * from t_student where name = #{arg0} and sex = #{arg1}-->
<!--select * from t_student where name = #{param1} and sex = #{param2}-->
select * from t_student where name = #{arg0} and sex = #{param2}
</select>
通过测试可以看到:
- arg0 是第一个参数
- param1是第一个参数
- arg1 是第二个参数
- param2是第二个参数
实现原理:实际上在mybatis底层会创建一个map集合,以arg0/param1为key,以方法上的参数为value,例如以下代码:
mybatis部分源码
Map<String,Object> map = new HashMap<>();
map.put("arg0", name);
map.put("arg1", sex);
map.put("param1", name);
map.put("param2", sex);
// 所以可以这样取值:#{arg0} #{arg1} #{param1} #{param2}
// 其本质就是#{map集合的key}
注意:使用mybatis3.4.2之前的版本时:要用#{0}和#{1}这种形式。
10.5 @Param注解(命名参数)
可以不用arg0 arg1 param1 param2吗?这个map集合的key我们自定义可以吗?当然可以。使用@Param注解即可。这样可以增强可读性。
需求:根据name和age查询
StudentMapper接口
/**
* mybatis框架底层的实现原理:
* map.put("name", name);
* map.put("sex", sex);
*
* 根据name和age查询
* @param name
* @param age
* @return
*/
List<Student> selectByNameAndAge(@Param(value="name") String name, @Param("age") int age);
StudentMapperTest.testSelectByNameAndAge
@Test
public void testSelectByNameAndAge(){
List<Student> stus = mapper.selectByNameAndAge("张三", 20);
stus.forEach(student -> System.out.println(student));
}
StudentMapper.xml
<!--使用了@Param注解之后,arg0和arg1失效了-->
<!--select * from t_student where name = #{arg0} and age = #{arg1}-->
<!--使用了@Param注解之后,param1和param2还可以用-->
<!--select * from t_student where name = #{param1} and age = #{param2}-->
<select id="selectByNameAndAge" resultType="student">
select * from t_student where name = #{name} and age = #{age}
</select>
通过测试,一切正常。
核心:@Param("这里填写的其实就是map集合的key")
10.6 @Param源码分析
004-Param注解源码分析
11 MyBatis查询语句专题
模块名:mybatis-008-select
打包方式:jar
引入依赖:mysql驱动依赖、mybatis依赖、logback依赖、junit依赖。
引入配置文件:jdbc.properties、mybatis-config.xml、logback.xml
创建pojo类:Car
创建Mapper接口:CarMapper
创建Mapper接口对应的映射文件:com/powernode/mybatis/mapper/CarMapper.xml
创建单元测试:CarMapperTest
拷贝工具类:SqlSessionUtil
11.1 返回Car
com/powernode/mybatis/pojo/Car.java
package com.powernode.mybatis.pojo;
public class Car {
// 数据库表当中的字段应该和pojo类的属性一一对应。
// 建议使用包装类,这样可以防止null的问题。
private Long id;
private String carNum;
private String brand;
private Double guidePrice;
private String produceTime;
private String carType;
// constructor
// setter and getter
// toString
}
当查询的结果,有对应的实体类,并且查询结果只有一条时:
CarMapper.selectById
package com.powernode.mybatis.mapper;
import com.powernode.mybatis.pojo.Car;
/**
* Car SQL映射器
* @author 老杜
* @version 1.0
* @since 1.0
*/
public interface CarMapper {
/**
* 根据id主键查询:结果最多只有一条
* @param id
* @return
*/
Car selectById(Long id);
}
CarMapper.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.powernode.mybatis.mapper.CarMapper">
<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>
</mapper>
CarMapperTest.testSelectById
package com.powernode.mybatis.test;
import com.powernode.mybatis.mapper.CarMapper;
import com.powernode.mybatis.pojo.Car;
import com.powernode.mybatis.utils.SqlSessionUtil;
import org.junit.Test;
public class CarMapperTest {
@Test
public void testSelectById(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
Car car = mapper.selectById(35L);
System.out.println(car);
sqlSession.close();
}
}
执行结果:
查询结果是一条的话可以使用List集合接收吗?当然可以。
CarMapper.selectByIdToList
/**
* 根据id主键查询:结果最多只有一条,可以放到List集合中吗?
* @return
*/
List<Car> selectByIdToList(Long id);
CarMapper.xml
<select id="selectByIdToList" resultType="Car">
select id,car_num carNum,brand,guide_price guidePrice,produce_time produceTime,car_type carType from t_car where id = #{id}
</select>
CarMapperTest.testSelectByIdToList
@Test
public void testSelectByIdToList(){
CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
List<Car> cars = mapper.selectByIdToList(35L);
System.out.println(cars);
}
执行结果:
11.2 返回List<Car>
当查询的记录条数是多条的时候,必须使用集合接收。如果使用单个实体类接收会出现异常。
CarMapper.selectAll
/**
* 查询所有的Car
* @return
*/
List<Car> selectAll();
CarMapper.xml
<select id="selectAll" resultType="Car">
select id,car_num carNum,brand,guide_price guidePrice,produce_time produceTime,car_type carType from t_car
</select>
CarMapperTest.testSelectAll
@Test
public void testSelectAll(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
List<Car> cars = mapper.selectAll();
cars.forEach(car -> System.out.println(car));
sqlSession.close();
}
如果返回多条记录,采用单个实体类接收会怎样?
CarMapper
/**
* 根据品牌进行模糊查询。
* 模糊查询的结果可能有多个,但是我采用一个POJO对象来接收会有问题吗?
* @param brand
* @return
*/
Car selectByBrandLike(String brand);
CarMapper.xml
<select id="selectByBrandLike" resultType="car">
select
id,
car_num as carNum,
brand,
guide_price as guidePrice,
produce_time as produceTime,
car_type as carType
from t_car
where
brand like "%"#{brand}"%"
</select>
CarMapperTest.testSelectByBrandLike
@Test
public void testSelectByBrandLike(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
// TooManyResultsException
// 什么意思?你期望的结果是返回一条记录。但是实际的SQL语句在执行的时候,返回的记录条数不是一条,是多条。
Car car = mapper.selectByBrandLike("比亚迪");
System.out.println(car);
sqlSession.close();
}
执行结果:
11.3 返回Map
当返回的数据,没有合适的实体类对应的话,可以采用Map集合接收。字段名做key,字段值做value。
查询如果可以保证只有一条数据,则返回一个Map集合即可。
CarMapper.selectByIdRetMap
/**
* 根据id获取汽车信息。将汽车信息放到Map集合中。
* +-----+---------+----------+-------------+--------------+----------+
* | id | car_num | brand | guide_price | produce_time | car_type |
* +-----+---------+----------+-------------+--------------+----------+
* | 158 | 1111 | 比亚迪汉 | 3.00 | 2000-10-10 | 新能源 |
* +-----+---------+----------+-------------+--------------+----------+
*
* Map<String, Object>
* k v
* -----------------------
* "id" 158
* "car_num" 1111
* "brand" 比亚迪汉
* ....
*
* @param id
* @return
*/
Map<String, Object> selectByIdRetMap(Long id);
CarMapper.xml
<!--resultType="java.util.Map"有别名:map-->
<select id="selectByIdRetMap" resultType="map">
select id,car_num carNum,brand,guide_price guidePrice,produce_time produceTime,car_type carType from t_car where id = #{id}
</select>
resultMap="map",这是因为mybatis内置了很多别名。【参见mybatis开发手册】
CarMapperTest.testSelectByIdRetMap
@Test
public void testSelectByIdRetMap(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
Map<String, Object> car = mapper.selectByIdRetMap(35L);
System.out.println(car);
sqlSession.close();
}
执行结果:
当然,如果返回一个Map集合,可以将Map集合放到List集合中吗?当然可以,这里就不再测试了。
反过来,如果返回的不是一条记录,是多条记录的话,只采用单个Map集合接收,这样同样会出现之前的异常:TooManyResultsException
11.4 返回List<Map>
查询结果条数大于等于1条数据,则可以返回一个存储Map集合的List集合。List<Map>等同于List<Car>
CarMapper接口
/**
* 查询所有的Car,返回一个List集合。List集合中存储的是Map集合。
* @return
*/
List<Map<String,Object>> selectAllRetListMap();
CarMapper.xml
<!--这个resultType不是list,是map-->
<select id="selectAllRetListMap" resultType="map">
select id,car_num carNum,brand,guide_price guidePrice,produce_time produceTime,car_type carType from t_car
</select>
CarMapperTest.testSelectAllRetListMap
@Test
public void testSelectAllRetListMap(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
List<Map<String, Object>> maps = mapper.selectAllRetListMap();
maps.forEach(map -> System.out.println(map));
sqlSession.close();
}
执行结果:
[
{carType=燃油车, carNum=103, guidePrice=50.30, produceTime=2020-10-01, id=33, brand=奔驰E300L},
{carType=电车, carNum=102, guidePrice=30.23, produceTime=2018-09-10, id=34, brand=比亚迪汉},
{carType=燃油车, carNum=103, guidePrice=50.30, produceTime=2020-10-01, id=35, brand=奔驰E300L},
{carType=燃油车, carNum=103, guidePrice=33.23, produceTime=2020-10-11, id=36, brand=奔驰C200},
......
]
11.5 返回Map<String.Map>
拿Car的id做key,以后取出对应的Map集合时更方便。
CarMapper接口
/**
* 获取所有的Car,返回一个Map集合。
* Map集合的key是Car的id。(Map集合的key是每条记录的主键值)
* Map集合的value是对应Car。(Map集合的value是每条记录)
* @return
*/
@MapKey("id") // 将查询结果的id值作为整个大Map集合的key。
Map<Long,Map<String,Object>> selectAllRetMap();
CarMapper.xml
<select id="selectAllRetMap" resultType="map">
select id,car_num carNum,brand,guide_price guidePrice,produce_time produceTime,car_type carType from t_car
</select>
CarMapperTest.testSelectAllRetMap
@Test
public void testSelectAllRetMap(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
Map<Long, Map<String, Object>> map = mapper.selectAllRetMap();
System.out.println(map);
sqlSession.close();
}
执行结果:
{
64={carType=燃油车, carNum=133, guidePrice=50.30, produceTime=2020-01-10, id=64, brand=丰田霸道},
66={carType=燃油车, carNum=133, guidePrice=50.30, produceTime=2020-01-10, id=66, brand=丰田霸道},
67={carType=燃油车, carNum=133, guidePrice=50.30, produceTime=2020-01-10, id=67, brand=丰田霸道},
69={carType=燃油车, carNum=133, guidePrice=50.30, produceTime=2020-01-10, id=69, brand=丰田霸道},
......
}
11.6 resultMap结果映射
查询结果的列名和java对象的属性名对应不上怎么办?
- 第一种方式:as 给列起别名
- 第二种方式:使用resultMap进行结果映射
- 第三种方式:是否开启驼峰命名自动映射(配置settings)
11.6.1 使用resultMap进行结果映射
CarMapper接口
/**
* 查询所有Car,使用resultMap进行结果映射
* @return
*/
List<Car> selectAllByResultMap();
CarMapper.xml
<!--
1.专门定义一个结果映射,在这个结果映射当中指定数据库表的字段名和Java类的属性名的对应关系。
2. type属性:用来指定POJO类的类名。可以使用别名。
3. id属性:指定resultMap的唯一标识。这个id将来要在select标签中使用,作为resultMap属性的值
-->
<resultMap id="carResultMap" type="Car">
<!--如果数据库表中有主键,一般都是有主键,要不然不符合数据库设计第一范式。-->
<!--如果有主键,建议这里配置一个id标签,注意:这不是必须的。但是官方的解释是什么呢?这样的配置可以让mybatis提高效率。-->
<id property="id" column="id"/>
<!--<result property="id" column="id"/>-->
<!--property后面填写POJO类的属性名-->
<!--column后面填写数据库表的字段名-->
<!--javaType用来指定属性类型。jdbcType用来指定列类型。一般可以省略。-->
<result property="carNum" column="car_num" javaType="java.lang.String" jdbcType="VARCHAR"/>
<!--如果column和property是一样的,这个可以省略。-->
<!--<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>
<!--select标签的resultMap属性,用来指定使用哪个结果映射。resultMap后面的值是resultMap的id-->
<select id="selectAllByResultMap" resultMap="carResultMap">
select * from t_car
</select>
CarMapperTest.testSelectAllByResultMap
@Test
public void testSelectAllByResultMap(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
List<Car> cars = mapper.selectAllByResultMap();
cars.forEach(car -> System.out.println(car));
sqlSession.close();
}
执行结果正常。
11.6.2 是否开启驼峰命名自动映射
使用这种方式的前提是:属性名遵循Java的命名规范,数据库表的列名遵循SQL的命名规范。
Java命名规范:首字母小写,后面每个单词首字母大写,遵循驼峰命名方式。
SQL命名规范:全部小写,单词之间采用下划线分割。
比如以下的对应关系:
实体类中的属性名 | 数据库表的列名 |
carNum | car_num |
carType | car_type |
produceTime | produce_time |
如何启用该功能,在mybatis-config.xml文件中进行配置:
mybatis-config.xml
<!--放在properties标签后面-->
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
CarMapper接口
/**
* 查询所有Car,启用驼峰命名自动映射
* @return
*/
List<Car> selectAllByMapUnderscoreToCamelCase();
CarMapper.xml
<select id="selectAllByMapUnderscoreToCamelCase" resultType="Car">
select * from t_car
</select>
CarMapperTest.testSelectAllByMapUnderscoreToCamelCase
@Test
public void testSelectAllByMapUnderscoreToCamelCase(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
List<Car> cars = mapper.selectAllByMapUnderscoreToCamelCase();
cars.forEach(car -> System.out.println(car));
sqlSession.close();
}
执行结果正常。
11.7 返回总记录条数
需求:查询总记录条数
select count(*),count(1)都可以统计总记录条数;但是count(具体某个字段),统计该字段中不为空的记录数,会自动去除NULL
CarMapper接口
/**
* 获取总记录条数
* @return
*/
Long selectTotal();
CarMapper.xml
<!--long是别名,可参考mybatis开发手册。-->
<!--<select id="selectTotal" resultType="java.lang.Long">-->
<select id="selectTotal" resultType="long">
select count(*) from t_car
</select>
CarMapperTest.testSelectTotal
@Test
public void testSelectTotal(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
Long total = mapper.selectTotal();
System.out.println("总记录条数:" + total);
sqlSession.close();
}
12 动态SQL
有的业务场景,也需要SQL语句进行动态拼接,例如:
- 批量删除
delete from t_car where id in(1,2,3,4,5,6,......这里的值是动态的,根据用户选择的id不同,值是不同的);
- 多条件查询
select * from t_car where brand like '丰田%' and guide_price > 30 and .....;
创建模块:mybatis-009-dynamic-sql
打包方式:jar
引入依赖:mysql驱动依赖、mybatis依赖、junit依赖、logback依赖
pojo:com.powernode.mybatis.pojo.Car
mapper接口:com.powernode.mybatis.mapper.CarMapper
引入配置文件:mybatis-config.xml、jdbc.properties、logback.xml
mapper配置文件:com/powernode/mybatis/mapper/CarMapper.xml
编写测试类:com.powernode.mybatis.test.CarMapperTest
拷贝工具类:SqlSessionUtil
12.1 if标签
需求:多条件查询。
可能的条件包括:品牌(brand)、指导价格(guide_price)、汽车类型(car_type)
CarMapper
package com.powernode.mybatis.mapper;
import com.powernode.mybatis.pojo.Car;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface CarMapper {
/**
* 根据多条件查询Car
* @param brand 品牌
* @param guidePrice 指导价
* @param carType 汽车类型
* @return
*/
List<Car> selectByMultiCondition(@Param("brand") String brand, @Param("guidePrice") Double guidePrice, @Param("carType") String carType);
}
CarMapper.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.powernode.mybatis.mapper.CarMapper">
<select id="selectByMultiCondition" resultType="Car">
select * from t_car where 1 = 1
<!--
1. if标签中test属性是必须的。
2. if标签中test属性的值是false或者true。
3. 如果test是true,则if标签中的sql语句就会拼接。反之,则不会拼接。
4. test属性中可以使用的是:
当使用了@Param注解,那么test中要出现的是@Param注解指定的参数名。@Param("brand"),那么这里只能使用brand
当没有使用@Param注解,那么test中要出现的是:param1 param2 param3 arg0 arg1 arg2....
当使用了POJO,那么test中出现的是POJO类的属性名。
5. 在mybatis的动态SQL当中,不能使用&&,只能使用and。
-->
<if test="brand != null and brand != ''">
and brand like "%"#{brand}"%"
</if>
<if test="guidePrice != null and guidePrice != ''">
and guide_price > #{guidePrice}
</if>
<if test="carType != null and carType != ''">
and car_type = #{carType}
</if>
</select>
</mapper>
CarMapperTest.testSelectByMultiCondition
package com.powernode.mybatis.test;
import com.powernode.mybatis.mapper.CarMapper;
import com.powernode.mybatis.pojo.Car;
import com.powernode.mybatis.utils.SqlSessionUtil;
import org.junit.Test;
import java.util.List;
public class CarMapperTest {
@Test
public void testSelectByMultiCondition(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
List<Car> cars = mapper.selectByMultiCondition("丰田", 20.0, "燃油车");
cars.forEach(car -> System.out.println(car));
sqlSession.close();
}
}
执行结果:
如果第一个条件为空,剩下两个条件不为空,会是怎样呢?
CarMapperTest.testSelectByMultiCondition
List<Car> cars = mapper.selectByMultiCondition("", 20.0, "燃油车");
执行结果:
报错了,SQL语法有问题,where后面出现了and。这该怎么解决呢?
- 可以where后面添加一个恒成立的条件。
执行结果:
如果三个条件都是空,有影响吗?
CarMapperTest.testSelectByMultiCondition
List<Car> cars = mapper.selectByMultiCondition("", null, "");
执行结果:
三个条件都不为空呢?
CarMapperTest.testSelectByMultiCondition
List<Car> cars = mapper.selectByMultiCondition("丰田", 20.0, "燃油车");
执行结果:
12.2 where标签
where标签的作用:让where子句更加动态智能。
- 所有条件都为空时,where标签保证不会生成where子句。
- 自动去除某些条件前面多余的and或or。
继续使用if标签中的需求。
CarMapper.selectByMultiConditionWithWhere
/**
* 根据多条件查询Car,使用where标签
* @param brand
* @param guidePrice
* @param carType
* @return
*/
List<Car> selectByMultiConditionWithWhere(@Param("brand") String brand, @Param("guidePrice") Double guidePrice, @Param("carType") String carType);
CarMapper.xml
<select id="selectByMultiConditionWithWhere" resultType="car">
select * from t_car
<where>
<if test="brand != null and brand != ''">
and brand like #{brand}"%"
</if>
<if test="guidePrice != null and guidePrice != ''">
and guide_price >= #{guidePrice}
</if>
<if test="carType != null and carType != ''">
and car_type = #{carType}
</if>
</where>
</select>
CarMapperTest.testSelectByMultiConditionWithWhere
@Test
public void testSelectByMultiConditionWithWhere(){
CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
List<Car> cars = mapper.selectByMultiConditionWithWhere("丰田", 20.0, "燃油车");
System.out.println(cars);
}
运行结果:
如果所有条件都是空呢?
CarMapperTest.testSelectByMultiConditionWithWhere
List<Car> cars = mapper.selectByMultiConditionWithWhere("", null, "");
运行结果:
它可以自动去掉前面多余的and,那可以自动去掉前面多余的or吗?
CarMapperTest.testSelectByMultiConditionWithWhere
List<Car> cars = mapper.selectByMultiConditionWithWhere("丰田", 20.0, "燃油车");
CarMapper.xml
<select id="selectByMultiConditionWithWhere" resultType="car">
select * from t_car
<where>
<if test="brand != null and brand != ''">
or brand like #{brand}"%"
</if>
<if test="guidePrice != null and guidePrice != ''">
and guide_price >= #{guidePrice}
</if>
<if test="carType != null and carType != ''">
and car_type = #{carType}
</if>
</where>
</select>
执行结果:
它可以自动去掉前面多余的and,那可以自动去掉后面多余的and吗?
CarMapper.xml
<select id="selectByMultiConditionWithWhere" resultType="car">
select * from t_car
<where>
<if test="brand != null and brand != ''">
brand like #{brand}"%" and
</if>
<if test="guidePrice != null and guidePrice != ''">
guide_price >= #{guidePrice} and
</if>
<if test="carType != null and carType != ''">
car_type = #{carType}
</if>
</where>
</select>
CarMapperTest.testSelectByMultiConditionWithWhere
// 让最后一个条件为空
List<Car> cars = mapper.selectByMultiConditionWithWhere("丰田", 20.0, "");
运行结果:
很显然,后面多余的and是不会被去除的。
12.3 trim标签
trim标签的属性:
- prefix:在trim标签中的语句前添加内容
- suffix:在trim标签中的语句后添加内容
- prefixOverrides:前缀覆盖掉(去掉)
- suffixOverrides:后缀覆盖掉(去掉)
CarMapper接口
/**
* 根据多条件查询Car,使用trim标签
* @param brand
* @param guidePrice
* @param carType
* @return
*/
List<Car> selectByMultiConditionWithTrim(@Param("brand") String brand, @Param("guidePrice") Double guidePrice, @Param("carType") String carType);
CarMapper.xml
<select id="selectByMultiConditionWithTrim" resultType="Car">
select * from t_car
<!--
prefix:加前缀
suffix:加后缀
prefixOverrides:删除前缀
suffixOverrides:删除后缀
-->
<!--prefix="where" 是在trim标签所有内容的前面添加 where-->
<!--suffixOverrides="and|or" 把trim标签中内容的后缀and或or去掉-->
<trim prefix="where" suffixOverrides="and|or">
<if test="brand != null and brand != ''">
brand like "%"#{brand}"%" or
</if>
<if test="guidePrice != null and guidePrice != ''">
guide_price > #{guidePrice} and
</if>
<if test="carType != null and carType != ''">
car_type = #{carType}
</if>
</trim>
</select>
CarMapperTest.testSelectByMultiConditionWithTrim
@Test
public void testSelectByMultiConditionWithTrim(){
CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
List<Car> cars = mapper.selectByMultiConditionWithTrim("丰田", 20.0, "");
System.out.println(cars);
}
如果所有条件为空,where会被加上吗?
CarMapperTest.testSelectByMultiConditionWithTrim
List<Car> cars = mapper.selectByMultiConditionWithTrim("", null, "");
执行结果:
12.4 set标签
主要使用在update语句当中,用来生成set关键字,同时去掉最后多余的“,”
比如我们只更新提交的不为空的字段,如果提交的数据是空或者"",那么这个字段我们将不更新。
CarMapper接口
/**
* 更新信息,使用set标签
* @param car
* @return
*/
int updateWithSet(Car car);
CarMapper.xml
不使用动态sql
<update id="updateById">
update t_car set
car_num = #{carNum},
brand = #{brand},
guide_price = #{guidePrice},
produce_time = #{produceTime},
car_type = #{carType}
where
id = #{id}
</update>
使用动态sql
<update id="updateWithSet">
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>
CarMapperTest.testUpdateWithSet
@Test
public void testUpdateWithSet(){
CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
Car car = new Car(38L,"1001","丰田霸道2",10.0,"",null);
int count = mapper.updateWithSet(car);
System.out.println(count);
SqlSessionUtil.openSession().commit();
}
执行结果:
12.5 choose when otherwise
这三个标签是在一起使用的:
语法格式
<choose>
<when></when>
<when></when>
<when></when>
<otherwise></otherwise>
</choose>
等同于:
if(){
}else if(){
}else if(){
}else if(){
}else{
}
只有一个分支会被选择!!!!
需求:先根据品牌查询,如果没有提供品牌,再根据指导价格查询,如果没有提供指导价格,就根据生产日期查询。
CarMapper接口
/**
* 使用choose when otherwise标签查询
* @param brand
* @param guidePrice
* @param produceTime
* @return
*/
List<Car> selectWithChoose(@Param("brand") String brand, @Param("guidePrice") Double guidePrice, @Param("produceTime") String produceTime);
CarMapper.xml
<select id="selectWithChoose" resultType="car">
select * from t_car
<where>
<choose>
<when test="brand != null and brand != ''">
brand like "%"#{brand}"%"
</when>
<when test="guidePrice != null and guidePrice != ''">
guide_price >= #{guidePrice}
</when>
<otherwise>
produce_time >= #{produceTime}
</otherwise>
</choose>
</where>
</select>
CarMapperTest.testSelectWithChoose
@Test
public void testSelectWithChoose(){
CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
//List<Car> cars = mapper.selectWithChoose("丰田霸道", 20.0, "2000-10-10");
//List<Car> cars = mapper.selectWithChoose("", 20.0, "2000-10-10");
//List<Car> cars = mapper.selectWithChoose("", null, "2000-10-10");
List<Car> cars = mapper.selectWithChoose("", null, "");
System.out.println(cars);
}
12.6 foreach标签
循环数组或集合,动态生成sql,比如这样的SQL:
批量删除
delete from t_car where id in(1,2,3);
delete from t_car where id = 1 or id = 2 or id = 3;
批量添加
insert into t_car values
(null,'1001','凯美瑞',35.0,'2010-10-11','燃油车'),
(null,'1002','比亚迪唐',31.0,'2020-11-11','新能源'),
(null,'1003','比亚迪宋',32.0,'2020-10-11','新能源')
12.6.1 批量删除
用in来删除
CarMapper接口
/**
* 通过foreach完成批量删除
* @param ids
* @return
*/
int deleteBatchByForeach(@Param("ids") Long[] ids);
CarMapper.xml
<!--
foreach标签的属性:
collection:指定数组或者集合
item:代表数组或集合中的元素
separator:循环之间的分隔符
open: foreach循环拼接的所有sql语句的最前面以什么开始。
close: foreach循环拼接的所有sql语句的最后面以什么结束。
collection="ids" 第一次写这个的时候报错了,错误信息是:
Available parameters are [array, arg0]
什么意思?
底层有一个Map集合:
map.put("array", 数组);
map.put("arg0", 数组);
-->
<delete id="deleteBatchByForeach">
<!--
delete from t_car where id in(
<foreach collection="ids" item="aaaaaaa" separator=",">
#{aaaaaaa}
</foreach>
)
-->
delete from t_car where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</delete>
CarMapperTest.testDeleteBatchByForeach
@Test
public void testDeleteBatchByForeach(){
CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
int count = mapper.deleteBatchByForeach(new Long[]{40L, 41L, 42L});
System.out.println("删除了几条记录:" + count);
SqlSessionUtil.openSession().commit();
}
执行结果:
用or来删除
CarMapper接口
/**
* 通过foreach完成批量删除
* @param ids
* @return
*/
int deleteBatchByForeach2(@Param("ids") Long[] ids);
CarMapper.xml
<delete id="deleteBatchByForeach2">
delete from t_car where
<foreach collection="ids" item="id" separator="or">
id = #{id}
</foreach>
</delete>
CarMapperTest.testDeleteBatchByForeach2
@Test
public void testDeleteBatchByForeach2(){
CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
int count = mapper.deleteBatchByForeach2(new Long[]{40L, 41L, 42L});
System.out.println("删除了几条记录:" + count);
SqlSessionUtil.openSession().commit();
}
执行结果:
12.6.2 批量添加
CarMapper接口
/**
* 批量添加,使用foreach标签
* @param cars
* @return
*/
int insertBatchByForeach(@Param("cars") List<Car> cars);
CarMapper.xml
<insert id="insertBatchByForeach">
insert into t_car values
<foreach collection="cars" item="car" separator=",">
(null,#{car.carNum},#{car.brand},#{car.guidePrice},#{car.produceTime},#{car.carType})
</foreach>
</insert>
CarMapperTest.testInsertBatchByForeach
@Test
public void testInsertBatchByForeach(){
CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
Car car1 = new Car(null, "2001", "兰博基尼", 100.0, "1998-10-11", "燃油车");
Car car2 = new Car(null, "2001", "兰博基尼", 100.0, "1998-10-11", "燃油车");
Car car3 = new Car(null, "2001", "兰博基尼", 100.0, "1998-10-11", "燃油车");
List<Car> cars = Arrays.asList(car1, car2, car3);
int count = mapper.insertBatchByForeach(cars);
System.out.println("插入了几条记录" + count);
SqlSessionUtil.openSession().commit();
}
执行结果:
12.7 sql标签与include标签
sql标签用来声明sql片段
include标签用来将声明的sql片段包含到某个sql语句当中
作用:代码复用。易维护。
mybatis-007-select模块中的CarMapper.xml
<!--声明一个SQL片段-->
<sql id="carCols">
id,
car_num carNum,
brand,
guide_price guidePrice,
produce_time produceTime,
car_type carType
</sql>
<select id="selectAllRetMap" resultType="map">
select
<!--将声明的sql片段包含进来。-->
<include refid="carCols"/>
from t_car
</select>
<select id="selectAllRetListMap" resultType="map">
select <include refid="carCols"/> carType from t_car
</select>
<select id="selectByIdRetMap" resultType="map">
select <include refid="carCols"/> from t_car where id = #{id}
</select>
13 MyBatis的高级映射及延迟加载
模块名:mybatis-010-advanced-mapping
打包方式:jar
依赖:mybatis依赖、mysql驱动依赖、junit依赖、logback依赖
配置文件:mybatis-config.xml、logback.xml、jdbc.properties
拷贝工具类:SqlSessionUtil
准备数据库表:一个班级对应多个学生。班级表:t_clazz。学生表:t_student
创建pojo:Student、Clazz
Student
package com.powernode.mybatis.pojo;
/**
* 学生类
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class Student {
private Integer sid;
private String sname;
//......
}
Clazz
package com.powernode.mybatis.pojo;
/**
* 班级类
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class Clazz {
private Integer cid;
private String cname;
//......
}
创建mapper接口:StudentMapper、ClazzMapper
创建mapper映射文件:StudentMapper.xml、ClazzMapper.xml
13.1 多对一
一对多,两张表,多的表加外键
多种方式,常见的包括三种:
- 第一种方式:一条SQL语句,级联属性映射。
- 第二种方式:一条SQL语句,association。
- 第三种方式:两条SQL语句,分步查询。(这种方式常用:优点一是可复用。优点二是支持懒加载。)
005-多对一的理解
13.1.1 第一种方式:级联属性映射
pojo类Student中添加一个属性:Clazz clazz; 表示学生关联的班级对象。
Student
package com.powernode.mybatis.pojo;
/**
* 学生类
* @author 老杜
* @version 1.0
* @since 1.0
*/
public class Student {
private Integer sid;
private String sname;
private Clazz clazz;
public Clazz getClazz() {
return clazz;
}
public void setClazz(Clazz clazz) {
this.clazz = clazz;
}
@Override
public String toString() {
return "Student{" +
"sid=" + sid +
", sname='" + sname + '\'' +
", clazz=" + clazz +
'}';
}
public Student() {
}
public Student(Integer sid, String sname) {
this.sid = sid;
this.sname = sname;
}
public Integer getSid() {
return sid;
}
public void setSid(Integer sid) {
this.sid = sid;
}
public String getSname() {
return sname;
}
public void setSname(String sname) {
this.sname = sname;
}
}
StudentMapper接口
/**
* 根据id获取学生信息。同时获取学生关联的班级信息。
* @param id 学生的id
* @return 学生对象,但是学生对象当中含有班级对象。
*/
Student selectBySid(Integer id);
StudentMapper.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.powernode.mybatis.mapper.StudentMapper">
<!--多对一映射的第一种方式:一条SQL语句,级联属性映射。-->
<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="selectBySid" resultMap="studentResultMap">
select
s.sid,s.sname,c.cid,c.cname
from
t_stu s left join t_clazz c on s.cid = c.cid
where
sid = #{sid}
</select>
</mapper>
StudentMapperTest.testSelectBySid
package com.powernode.mybatis.test;
import com.powernode.mybatis.mapper.StudentMapper;
import com.powernode.mybatis.pojo.Student;
import com.powernode.mybatis.utils.SqlSessionUtil;
import org.junit.Test;
public class StudentMapperTest {
@Test
public void testSelectBySid(){
StudentMapper mapper = SqlSessionUtil.openSession().getMapper(StudentMapper.class);
Student student = mapper.selectBySid(1);
System.out.println(student);
}
}
执行结果:
13.1.2 第二种方式:association
其他位置都不需要修改,只需要修改resultMap中的配置:association即可。
StudentMapper接口
/**
* 一条SQL语句,association
* @param id
* @return
*/
Student selectByIdAssociation(Integer id);
StudentMapper.xml
<!--一条SQL语句,association。-->
<resultMap id="studentResultMapAssociation" type="Student">
<id property="sid" column="sid"/>
<result property="sname" column="sname"/>
<!--
association:翻译为关联。一个Student对象关联一个Clazz对象
property:提供要映射的POJO类的属性名。
javaType:用来指定要映射的java类型。
-->
<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 left join t_clazz c on s.cid = c.cid
where
s.sid = #{sid}
</select>
association翻译为:关联。
学生对象关联一个班级对象。
13.1.3 第三种方式:分步查询
其他位置不需要修改,只需要修改以及添加以下三处:
第一处:association中select位置填写sqlId。sqlId=namespace+id。其中column属性作为这条子sql语句的条件。
StudentMapper接口
/**
* 分步查询第一步:先根据学生的sid查询学生的信息。
* @param sid
* @return
*/
Student selectByIdStep1(Integer sid);
StudentMapper.xml
<!--
分步查询的优点:
第一:复用性增强。可以重复利用。(大步拆成N多个小碎步。每一个小碎步更加可以重复利用。)
第二:采用这种分步查询,可以充分利用他们的延迟加载/懒加载机制。
什么是延迟加载(懒加载),有什么用?
延迟加载的核心原理是:用的时候再执行查询语句。不用的时候不查询。
作用:提高性能。尽可能的不查,或者说尽可能的少查。来提高效率。
在mybatis当中怎么开启延迟加载呢?
association标签中添加fetchType="lazy"
注意:默认情况下是没有开启延迟加载的。需要设置:fetchType="lazy"
这种在association标签中配置fetchType="lazy",是局部的设置,只对当前的association关联的sql语句起作用。
在实际的开发中,大部分都是需要使用延迟加载的,所以建议开启全部的延迟加载机制:
在mybatis核心配置文件中添加全局配置:lazyLoadingEnabled=true
实际开发中的模式:
把全局的延迟加载打开。
如果某一步不需要使用延迟加载,请设置:fetchType="eager"
-->
<!--两条SQL语句,完成多对一的分步查询。-->
<!--这里是第一步:根据学生的id查询学生的所有信息。这些信息当中含有班级id(cid)-->
<resultMap id="studentResultMapByStep" type="Student">
<id property="sid" column="sid"/>
<result property="sname" column="sname"/>
<association property="clazz"
select="com.powernode.mybatis.mapper.ClazzMapper.selectByIdStep2"
column="cid"
fetchType="eager"/>
</resultMap>
<select id="selectByIdStep1" resultMap="studentResultMapByStep">
select sid,sname,cid from t_stu where sid = #{sid}
</select>
第二处:在ClazzMapper接口中添加方法
ClazzMapper接口
package com.powernode.mybatis.mapper;
import com.powernode.mybatis.pojo.Clazz;
/**
* Clazz映射器接口
*/
public interface ClazzMapper {
/**
* 分步查询第二步:根据cid获取班级信息。
* @param cid
* @return
*/
Clazz selectByIdStep2(Integer cid);
}
第三处:在ClazzMapper.xml文件中进行配置
ClazzMapper.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.powernode.mybatis.mapper.ClazzMapper">
<!--分步查询第二步:根据cid获取班级信息。-->
<select id="selectByIdStep2" resultType="Clazz">
select cid,cname from t_clazz where cid = #{cid}
</select>
</mapper>
StudentMapperTest.testSelectByIdStep1
@Test
public void testSelectByIdStep1(){
SqlSession sqlSession = SqlSessionUtil.openSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
Student student = mapper.selectByIdStep1(5);
System.out.println(student);
sqlSession.close();
}
执行结果,可以很明显看到先后有两条sql语句执行:
分步优点:
- 第一个优点:代码复用性增强。
- 第二个优点:支持延迟加载。【暂时访问不到的数据可以先不查询。提高程序的执行效率。】
13.2 多对一延迟加载
要想支持延迟加载,非常简单,只需要在association标签中添加fetchType="lazy"即可。
修改StudentMapper.xml文件:
StudentMapper.xml
<resultMap id="studentResultMapByStep" type="Student">
<id property="sid" column="sid"/>
<result property="sname" column="sname"/>
<association property="clazz"
select="com.powernode.mybatis.mapper.ClazzMapper.selectByCid"
column="cid"
fetchType="lazy"/>
</resultMap>
我们现在只查询学生名字,修改测试程序:
StudentMapperTest.testSelectByIdStep1
@Test
public void testSelectByIdStep1(){
SqlSession sqlSession = SqlSessionUtil.openSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
Student student = mapper.selectByIdStep1(5);
//System.out.println(student);
// 只需要看学生的名字
System.out.println(student.getSname());
sqlSession.close();
}
如果后续需要使用到学生所在班级的名称,这个时候才会执行关联的sql语句,修改测试程序:
StudentMapperTest.testSelectByIdStep1
@Test
public void testSelectByIdStep1(){
SqlSession sqlSession = SqlSessionUtil.openSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
Student student = mapper.selectByIdStep1(5);
//System.out.println(student);
// 只需要看学生的名字
System.out.println(student.getSname());
// 程序执行到这里了,我想看看班级的名字
System.out.println(student.getClazz().getCname());
sqlSession.close();
}
通过以上的执行结果可以看到,只有当使用到班级名称之后,才会执行关联的sql语句,这就是延迟加载。
在mybatis中如何开启全局的延迟加载呢?需要setting配置,如下:
mybatis-config.xml
<settings>
<!--延迟加载的全局开关。默认值false不开启。-->
<!--什么意思:所有只要但凡带有分步的,都采用延迟加载。-->
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
把fetchType="lazy"去掉。
执行以下程序:
StudentMapperTest.testSelectBySid
public class StudentMapperTest {
@Test
public void testSelectByIdStep1(){
StudentMapper mapper = SqlSessionUtil.openSession().getMapper(StudentMapper.class);
Student student = mapper.selectBySid(1);
//System.out.println(student);
// 只获取学生姓名
String sname = student.getSname();
System.out.println("学生姓名:" + sname);
// 到这里之后,想获取班级名字了
String cname = student.getClazz().getCname();
System.out.println("学生的班级名称:" + cname);
}
}
通过以上的测试可以看出,我们已经开启了全局延迟加载策略。
开启全局延迟加载之后,所有的sql都会支持延迟加载,如果某个sql你不希望它支持延迟加载怎么办呢?将fetchType设置为eager:
StudentMapper.xml
<resultMap id="studentResultMapByStep" type="Student">
<id property="sid" column="sid"/>
<result property="sname" column="sname"/>
<association property="clazz"
select="com.powernode.mybatis.mapper.ClazzMapper.selectByIdStep2"
column="cid"
fetchType="eager"/>
</resultMap>
这样的话,针对某个特定的sql,你就关闭了延迟加载机制。
后期我们要不要开启延迟加载机制,主要看实际的业务需求是怎样的。
13.3 一对多
006一对多关系映射原理
一对多,两张表,多的表加外键
一对多的实现,通常是在一的一方中有List集合属性。
在Clazz类中添加List<Student> stus; 属性。
Clazz
public class Clazz {
private Integer cid;
private String cname;
private List<Student> stus;
// set get方法
// 构造方法
// toString方法
}
一对多的实现通常包括两种实现方式:
- 第一种方式:collection
- 第二种方式:分步查询
13.3.1 第一种方式:collection
ClazzMapper接口
package com.powernode.mybatis.mapper;
import com.powernode.mybatis.pojo.Clazz;
/**
* Clazz映射器接口
* @author 老杜
* @version 1.0
* @since 1.0
*/
public interface ClazzMapper {
/**
* 根据cid获取Clazz信息
* @param cid
* @return
*/
Clazz selectByCid(Integer cid);
/**
* 根据班级编号查询班级信息。同时班级中所有的学生信息也要查询。
* @param cid
* @return
*/
Clazz selectClazzAndStusByCid(Integer cid);
}
ClazzMapper.xml
<resultMap id="clazzResultMap" type="Clazz">
<id property="cid" column="cid"/>
<result property="cname" column="cname"/>
<!--一对多,这里是collection。collection是集合的意思。-->
<!--ofType 属性用来指定集合当中的元素类型。-->
<collection property="stus" ofType="Student">
<id property="sid" column="sid"/>
<result property="sname" column="sname"/>
</collection>
</resultMap>
<select id="selectClazzAndStusByCid" resultMap="clazzResultMap">
select c.cid,c.cname,s.sid,s.sname
from t_clazz c left join t_stu s on c.cid = s.cid
where c.cid = #{cid}
</select>
注意是ofType,表示“集合中的类型”。
ClazzMapperTest.testSelectClazzAndStusByCid
package com.powernode.mybatis.test;
import com.powernode.mybatis.mapper.ClazzMapper;
import com.powernode.mybatis.pojo.Clazz;
import com.powernode.mybatis.utils.SqlSessionUtil;
import org.junit.Test;
public class ClazzMapperTest {
@Test
public void testSelectClazzAndStusByCid() {
ClazzMapper mapper = SqlSessionUtil.openSession().getMapper(ClazzMapper.class);
Clazz clazz = mapper.selectClazzAndStusByCid(1001);
System.out.println(clazz);
}
}
执行结果:
13.3.2 第二种方式:分步查询
修改以下四个位置即可:
ClazzMapper接口
package com.powernode.mybatis.mapper;
import com.powernode.mybatis.pojo.Clazz;
/**
* Clazz映射器接口
*/
public interface ClazzMapper {
/**
* 分步查询。第一步:根据班级编号获取班级信息。
* @param cid 班级编号
* @return
*/
Clazz selectByStep1(Integer cid);
}
ClazzMapper.xml
<!--分步查询第一步:根据班级的cid获取班级信息。-->
<resultMap id="clazzResultMapStep" type="Clazz">
<id property="cid" column="cid"/>
<result property="cname" column="cname"/>
<!--主要看这里-->
<collection property="stus"
select="com.powernode.mybatis.mapper.StudentMapper.selectByCidStep2"
column="cid" fetchType="eager" />
</resultMap>
<select id="selectByStep1" resultMap="clazzResultMapStep">
select cid,cname from t_clazz where cid = #{cid}
</select>
StudentMapper接口
/**
* 根据班级编号查询学生信息。
* @param cid
* @return
*/
List<Student> selectByCidStep2(Integer cid);
StudentMapper.xml
<select id="selectByCidStep2" resultType="Student">
select * from t_stu where cid = #{cid}
</select>
ClazzMapperTest.testSelectByStep1
@Test
public void testSelectByStep1(){
SqlSession sqlSession = SqlSessionUtil.openSession();
ClazzMapper mapper = sqlSession.getMapper(ClazzMapper.class);
Clazz clazz = mapper.selectByStep1(1000);
//System.out.println(clazz);
// 只访问班级名字。
System.out.println(clazz.getCname());
// 只有用到的时候才会去执行第二步SQL
//System.out.println(clazz.getStus());
sqlSession.close();
}
执行结果:
13.4 一对多延迟加载
一对多延迟加载机制和多对一是一样的。同样是通过两种方式:
- 第一种:fetchType="lazy"
- 第二种:修改全局的配置setting,lazyLoadingEnabled=true,如果开启全局延迟加载,想让某个sql不使用延迟加载:fetchType="eager"
14 MyBatis的缓存
007-对缓存的理解
缓存:cache
缓存的作用:通过减少IO的方式,来提高程序的执行效率。
mybatis的缓存:将select语句的查询结果放到缓存(内存)当中,下一次还是这条select语句的话,直接从缓存中取,不再查数据库。一方面是减少了IO。另一方面不再执行繁琐的查找算法。效率大大提升。
mybatis缓存包括:
- 一级缓存:将查询到的数据存储到SqlSession中。
- 二级缓存:将查询到的数据存储到SqlSessionFactory中。
- 或者集成其它第三方的缓存:比如EhCache【Java语言开发的】、Memcache【C语言开发的】等。
缓存只针对于DQL语句,也就是说缓存机制只对应select语句。
14.1 一级缓存
一级缓存默认是开启的。不需要做任何配置。
原理:只要使用同一个SqlSession对象执行同一条SQL语句,就会走缓存。
模块名:mybatis-011-cache
CarMapper接口
package com.powernode.mybatis.mapper;
import com.powernode.mybatis.pojo.Car;
public interface CarMapper {
/**
* 根据id获取Car信息。
* @param id
* @return
*/
Car selectById(Long id);
}
CarMapper.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.powernode.mybatis.mapper.CarMapper">
<select id="selectById" resultType="Car">
select * from t_car where id = #{id}
</select>
</mapper>
CarMapperTest.testSelectById
package com.powernode.mybatis.test;
import com.powernode.mybatis.mapper.CarMapper;
import com.powernode.mybatis.pojo.Car;
import com.powernode.mybatis.utils.SqlSessionUtil;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;
public class CarMapperTest {
// 思考:什么时候不走缓存?
// SqlSession对象不是同一个,肯定不走缓存。
// 查询条件不一样,肯定也不走缓存。
// 思考:什么时候一级缓存失效?
// 第一次DQL和第二次DQL之间你做了以下两件事中的任意一件,都会让一级缓存清空:
// 1. 执行了sqlSession的clearCache()方法,这是手动清空缓存。
// 2. 执行了INSERT或DELETE或UPDATE语句。不管你是操作哪张表的,都会清空一级缓存。
@Test
public void testSelectById() throws Exception{
// 注意:如果要获取不同的SqlSession对象,不能使用我们封装的SqlSessionUtil工具类。
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = builder.build(Resources.getResourceAsStream("mybatis-config.xml"));
SqlSession sqlSession1 = sqlSessionFactory.openSession();
CarMapper mapper1 = sqlSession1.getMapper(CarMapper.class);
Car car1 = mapper1.selectById(83L);
System.out.println(car1);
CarMapper mapper2 = sqlSession1.getMapper(CarMapper.class);
Car car2 = mapper2.selectById(83L);
System.out.println(car2);
SqlSession sqlSession2 = sqlSessionFactory.openSession();
CarMapper mapper3 = sqlSession2.getMapper(CarMapper.class);
Car car3 = mapper3.selectById(83L);
System.out.println(car3);
CarMapper mapper4 = sqlSession2.getMapper(CarMapper.class);
Car car4 = mapper4.selectById(83L);
System.out.println(car4);
}
}
执行结果:
什么情况下不走缓存?
- 第一种:不同的SqlSession对象。
- 第二种:查询条件变化了。
一级缓存失效情况包括两种:
- 第一种:第一次查询和第二次查询之间,手动清空了一级缓存。
清空一级缓存
sqlSession.clearCache();
- 第二种:第一次查询和第二次查询之间,执行了增删改操作。【这个增删改和哪张表没有关系,只要有insert delete update操作,一级缓存就失效。】
CarMapper接口
/**
* 保存账户信息
*/
void insertAccount();
CarMapper.xml
<insert id="insertAccount">
insert into t_act values(3, 'act003', 10000)
</insert>
执行结果:
14.2 二级缓存
二级缓存的范围是SqlSessionFactory。
使用二级缓存需要具备以下几个条件:
- <setting name="cacheEnabled" value="true"> 全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。默认就是true,无需设置。
- 在需要使用二级缓存的SqlMapper.xml文件中添加配置:<cache />
- 使用二级缓存的实体类对象必须是可序列化的,也就是必须实现java.io.Serializable接口
- SqlSession对象关闭或提交之后,一级缓存中的数据才会被写入到二级缓存当中。此时二级缓存才可用。
测试二级缓存:
CarMapper.xml
<!--
默认情况下,二级缓存机制是开启的。
只需要在对应的SqlMapper.xml文件中添加以下标签。用来表示“我”使用该二级缓存。
-->
<cache/>
Car类
public class Car implements Serializable {
//......
}
CarMapper接口
/**
* 测试二级缓存
* @param id
* @return
*/
Car selectById2(Long id);
CarMapper.xml
<select id="selectById2" resultType="Car">
select * from t_car where id = #{id}
</select>
CarMapperTest.testSelectById2
@Test
public void testSelectById2() throws Exception{
// 这里只有一个SqlSessionFactory对象。二级缓存对应的就是SqlSessionFactory。
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
CarMapper mapper1 = sqlSession1.getMapper(CarMapper.class);
CarMapper mapper2 = sqlSession2.getMapper(CarMapper.class);
// 这行代码执行结束之后,实际上数据是缓存到一级缓存当中了。(sqlSession1是一级缓存。)
Car car1 = mapper1.selectById2(83L);
System.out.println(car1);
// 如果这里不关闭SqlSession1对象的话,二级缓存中还是没有数据的。
// 如果执行了这行代码,sqlSession1的一级缓存中的数据会放到二级缓存当中。
sqlSession1.close();
// 这行代码执行结束之后,实际上数据会缓存到一级缓存当中。(sqlSession2是一级缓存。)
Car car2 = mapper2.selectById2(83L);
System.out.println(car2);
// 程序执行到这里的时候,会将sqlSession1这个一级缓存中的数据写入到二级缓存当中。
//sqlSession1.close();
// 程序执行到这里的时候,会将sqlSession2这个一级缓存中的数据写入到二级缓存当中。
sqlSession2.close();
}
二级缓存的失效:只要两次查询之间出现了增删改操作。二级缓存就会失效。【一级缓存也会失效】
二级缓存的相关配置:
1、eviction(驱逐):指定从缓存中移除某个对象的淘汰算法。默认采用LRU策略。
a) LRU:Least Recently Used。最近最少使用。优先淘汰在间隔时间内使用频率最低的对象。(其实还有一种淘汰算法LFU,最不常用。)
b) FIFO:First In First Out。一种先进先出的数据缓存器。先进入二级缓存的对象最先被淘汰。
c) SOFT:软引用。淘汰软引用指向的对象。具体算法和JVM的垃圾回收算法有关。
d) WEAK:弱引用。淘汰弱引用指向的对象。具体算法和JVM的垃圾回收算法有关。
2、flushInterval(刷新间隔):
a) 二级缓存的刷新时间间隔。单位毫秒。如果没有设置。就代表不刷新缓存,只要内存足够大,一直会向二级缓存中缓存数据。除非执行了增删改。
3、readOnly:
a) true:多条相同的sql语句执行之后返回的对象是共享的同一个。性能好。但是多线程并发可能会存在安全问题。
b) false:多条相同的sql语句执行之后返回的对象是副本,调用了clone方法。性能一般。但安全。
4、size:
a) 设置二级缓存中最多可存储的java对象数量。默认值1024。
14.3 MyBatis继承EhCache
集成EhCache是为了代替mybatis自带的二级缓存。一级缓存是无法替代的。
mybatis对外提供了接口,也可以集成第三方的缓存组件。比如EhCache、Memcache等。都可以。
EhCache是Java写的。Memcache是C语言写的。所以mybatis集成EhCache较为常见,按照以下步骤操作,就可以完成集成:
第一步:引入mybatis整合ehcache的依赖。
pom.xml
<!--mybatis集成ehcache的组件-->
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.2.2</version>
</dependency>
第二步:在类的根路径下新建echcache.xml文件,并提供以下配置信息。
ehcache.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="e:/ehcache"/>
<!--defaultCache:默认的管理策略-->
<!--eternal:设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断-->
<!--maxElementsInMemory:在内存中缓存的element的最大数目-->
<!--overflowToDisk:如果内存中数据超过内存限制,是否要缓存到磁盘上-->
<!--diskPersistent:是否在磁盘上持久化。指重启jvm后,数据是否有效。默认为false-->
<!--timeToIdleSeconds:对象空闲时间(单位:秒),指对象在多长时间没有被访问就会失效。只对eternal为false的有效。默认值0,表示一直可以访问-->
<!--timeToLiveSeconds:对象存活时间(单位:秒),指对象从创建到失效所需要的时间。只对eternal为false的有效。默认值0,表示一直可以访问-->
<!--memoryStoreEvictionPolicy:缓存的3 种清空策略-->
<!--FIFO:first in first out (先进先出)-->
<!--LFU:Less Frequently Used (最少使用).意思是一直以来最少被使用的。缓存的元素有一个hit 属性,hit 值最小的将会被清出缓存-->
<!--LRU:Least Recently Used(最近最少使用). (ehcache 默认值).缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存-->
<defaultCache eternal="false" maxElementsInMemory="1000" overflowToDisk="false" diskPersistent="false"
timeToIdleSeconds="0" timeToLiveSeconds="600" memoryStoreEvictionPolicy="LRU"/>
</ehcache>
第三步:修改SqlMapper.xml文件中的<cache/>标签,添加type属性。
CarMapper.xml
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
第四步:编写测试程序使用。
CarMapperTest.testSelectById2
@Test
public void testSelectById2() throws Exception{
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
SqlSession sqlSession1 = sqlSessionFactory.openSession();
CarMapper mapper1 = sqlSession1.getMapper(CarMapper.class);
Car car1 = mapper1.selectById(83L);
System.out.println(car1);
sqlSession1.close();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
CarMapper mapper2 = sqlSession2.getMapper(CarMapper.class);
Car car2 = mapper2.selectById(83L);
System.out.println(car2);
}
15 MyBatis的逆向工程
所谓的逆向工程是:根据数据库表逆向生成Java的pojo类,SqlMapper.xml文件,以及Mapper接口类等。
要完成这个工作,需要借助别人写好的逆向工程插件。
思考:使用这个插件的话,需要给这个插件配置哪些信息?
- pojo类名、包名以及生成位置。
- SqlMapper.xml文件名以及生成位置。
- Mapper接口名以及生成位置。
- 连接数据库的信息。
- 指定哪些表参与逆向工程。
- ......
15.1 逆向工程配置与生成
15.1.1 第一步:基础环境准备
新建模块:mybatis-012-generator1(简单版),mybatis-013-generator2(增强版)
打包方式:jar
15.1.2 第二步:在pom中添加逆向工程插件
pom.xml
<!--配置mybatis逆向工程的插件-->
<!--定制构建过程-->
<build>
<!--可配置多个插件-->
<plugins>
<!--其中的一个插件:mybatis逆向工程插件-->
<plugin>
<!--插件的GAV坐标-->
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.4.1</version>
<!--允许覆盖-->
<configuration>
<overwrite>true</overwrite>
</configuration>
<!--插件的依赖-->
<dependencies>
<!--mysql驱动依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
15.1.3 第三步:配置generatorConfig.xml
该文件名必须叫做:generatorConfig.xml
该文件必须放在类的根路径下。
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/powernode"
userId="root"
password="root">
</jdbcConnection>
<!-- 生成pojo包名和位置 -->
<javaModelGenerator targetPackage="com.powernode.mybatis.pojo" targetProject="src/main/java">
<!--是否开启子包-->
<property name="enableSubPackages" value="true"/>
<!--是否去除字段名的前后空白-->
<property name="trimStrings" value="true"/>
</javaModelGenerator>
<!-- 生成SQL映射文件的包名和位置 -->
<sqlMapGenerator targetPackage="com.powernode.mybatis.mapper" targetProject="src/main/resources">
<!--是否开启子包-->
<property name="enableSubPackages" value="true"/>
</sqlMapGenerator>
<!-- 生成Mapper接口的包名和位置 -->
<javaClientGenerator
type="xmlMapper"
targetPackage="com.powernode.mybatis.mapper"
targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
</javaClientGenerator>
<!-- 表名和对应的实体类名-->
<table tableName="t_car" domainObjectName="Car"/>
</context>
</generatorConfiguration>
15.1.4 第四步:运行插件
15.2 测试逆向工程生成的是否好用
15.2.1 第一步:环境准备
- 依赖:mybatis依赖、mysql驱动依赖、junit依赖、logback依赖
- jdbc.properties
- mybatis-config.xml
- logback.xml
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.10</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
</dependencies>
15.2.2 第二步:编写测试程序
GeneratorTest.testGenerator
QBC 风格:Query By Criteria 一种查询方式,比较面向对象,看不到sql语句。
封装条件,通过CarExample对象来封装查询条件
package com.powernode.mybatis.test;
import com.powernode.mybatis.mapper.CarMapper;
import com.powernode.mybatis.pojo.Car;
import com.powernode.mybatis.pojo.CarExample;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;
import java.math.BigDecimal;
import java.util.List;
public class GeneratorTest {
@Test
public void testGenerator() throws Exception{
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
SqlSession sqlSession = sqlSessionFactory.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
// 增
/*Car car = new Car();
car.setCarNum("1111");
car.setBrand("比亚迪唐");
car.setGuidePrice(new BigDecimal(30.0));
car.setProduceTime("2010-10-12");
car.setCarType("燃油车");
int count = mapper.insert(car);
System.out.println("插入了几条记录:" + count);*/
// 删
/*int count = mapper.deleteByPrimaryKey(83L);
System.out.println("删除了几条记录:" + count);*/
// 改
// 根据主键修改
/*Car car = new Car();
car.setId(89L);
car.setGuidePrice(new BigDecimal(20.0));
car.setCarType("新能源");
int count = mapper.updateByPrimaryKey(car);
System.out.println("更新了几条记录:" + count);*/
// 根据主键选择性修改
/*car = new Car();
car.setId(89L);
car.setCarNum("3333");
car.setBrand("宝马520Li");
car.setProduceTime("1999-01-10");
count = mapper.updateByPrimaryKeySelective(car);
System.out.println("更新了几条记录:" + count);*/
// 执行查询
// 1. 查询一个
Car car = mapper.selectByPrimaryKey(165L);
System.out.println(car);
// 2. 查询所有(selectByExample,根据条件查询,如果条件是null表示没有条件。)
List<Car> cars = mapper.selectByExample(null);
cars.forEach(car1 -> System.out.println(car1));
System.out.println("=========================================");
// 3. 按照条件进行查询
// QBC 风格:Query By Criteria 一种查询方式,比较面向对象,看不到sql语句。
// 封装条件,通过CarExample对象来封装查询条件
CarExample carExample = new CarExample();
// 调用carExample.createCriteria()方法来创建查询条件
carExample.createCriteria()
.andBrandLike("帕萨特")
.andGuidePriceGreaterThan(new BigDecimal(20.0));
// 添加or
carExample.or().andCarTypeEqualTo("燃油车");
// 执行查询
List<Car> cars2 = mapper.selectByExample(carExample);
cars2.forEach(car2 -> System.out.println(car2));
sqlSession.commit();
sqlSession.close();
}
}
16 MyBatis使用PageHelper
008-分页查询的原理
16.1 limit分页
mysql的limit后面两个数字:
- 第一个数字:startIndex(起始下标。下标从0开始。)
- 第二个数字:pageSize(每页显示的记录条数)
假设已知页码pageNum,还有每页显示的记录条数pageSize,第一个数字可以动态的获取吗?
- startIndex = (pageNum - 1) * pageSize
所以,标准通用的mysql分页SQL:
MySQL标准通用的分页SQL
select
*
from
tableName ......
limit
(pageNum - 1) * pageSize, pageSize
使用mybatis应该怎么做?
模块名:mybatis-014-page
CarMapper接口
package com.powernode.mybatis.mapper;
import com.powernode.mybatis.pojo.Car;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface CarMapper {
/**
* 通过分页的方式获取Car列表
* @param startIndex 起始下标
* @param pageSize 每页显示记录条数
* @return
*/
List<Car> selectByPage(@Param("startIndex") Integer startIndex, @Param("pageSize") Integer pageSize);
}
CarMapper.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.powernode.mybatis.mapper.CarMapper">
<select id="selectAll" resultType="Car">
select * from t_car limit #{startIndex},#{pageSize}
</select>
</mapper>
CarMapperTest.testSelectByPage
package com.powernode.mybatis.test;
import com.powernode.mybatis.mapper.CarMapper;
import com.powernode.mybatis.pojo.Car;
import com.powernode.mybatis.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import java.util.List;
public class CarMapperTest {
@Test
public void testSelectByPage(){
// 获取每页显示的记录条数
int pageSize = 3;
// 显示第几页:页码
int pageNum = 3;
// 计算开始下标
int startIndex = (pageNum - 1) * pageSize;
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
List<Car> cars = mapper.selectByPage(startIndex, pageSize);
cars.forEach(car -> System.out.println(car));
sqlSession.close();
}
}
执行结果:
获取数据不难,难的是获取分页相关的数据比较难。可以借助mybatis的PageHelper插件。
16.2 PageHelper插件
使用PageHelper插件进行分页,更加的便捷。
16.2.1 第一步:引入依赖
pom.xml
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.3.1</version>
</dependency>
16.2.2 第二步:在mybatis-config.xml文件中配置插件
typeAliases标签下面进行配置:
mybatis-config.xml
<!--mybatis分页的拦截器-->
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
</plugins>
16.2.3 第三步:编写Java代码
CarMapper接口
/**
* 查询所有的Car,通过分页查询插件PageHelper完成。
* @return
*/
List<Car> selectAll();
CarMapper.xml
<select id="selectAll" resultType="Car">
select * from t_car
</select>
关键点:
- 在查询语句之前开启分页功能。
- 在查询语句之后封装PageInfo对象。(PageInfo对象将来会存储到request域当中。在页面上展示。)
CarMapperTest.testSelectAll
@Test
public void testSelectAll() throws Exception{
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
SqlSession sqlSession = sqlSessionFactory.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
// 一定一定一定要注意:在执行DQL语句之前。开启分页功能。
int pageNum = 2;
int pageSize = 3;
// 开启分页
PageHelper.startPage(pageNum, pageSize);
// 执行查询语句
List<Car> cars = mapper.selectAll();
//cars.forEach(car -> System.out.println(car));
// 封装分页信息对象new PageInfo()
// PageInfo对象是PageHelper插件提供的,用来封装分页相关的信息的对象。
PageInfo<Car> pageInfo = new PageInfo<>(cars, 3);//navigatePages:3 导航页数:3
System.out.println(pageInfo);
sqlSession.close();
}
执行结果:
PageInfo{pageNum=2, pageSize=2, size=2, startRow=3, endRow=4, total=6, pages=3, list=Page{count=true, pageNum=2, pageSize=2, startRow=2, endRow=4, total=6, pages=3, reasonable=false, pageSizeZero=false}[Car{id=86, carNum='1234', brand='丰田霸道', guidePrice=50.5, produceTime='2020-10-11', carType='燃油车'}, Car{id=87, carNum='1234', brand='丰田霸道', guidePrice=50.5, produceTime='2020-10-11', carType='燃油车'}], prePage=1, nextPage=3, isFirstPage=false, isLastPage=false, hasPreviousPage=true, hasNextPage=true, navigatePages=5, navigateFirstPage=1, navigateLastPage=3, navigatepageNums=[1, 2, 3]}
对执行结果进行格式化:
PageInfo{pageNum=2, pageSize=3, size=3, startRow=4, endRow=6, total=7, pages=3,
list=Page{count=true, pageNum=2, pageSize=3, startRow=3, endRow=6, total=7, pages=3, reasonable=false, pageSizeZero=false}
[Car{id=168, carNum='1204', brand='奥迪Q7', guidePrice=3.0, produceTime='2009-10-11', carType='燃油车'},
Car{id=169, carNum='1205', brand='朗逸', guidePrice=4.0, produceTime='2001-10-11', carType='新能源'},
Car{id=170, carNum='1206', brand='奔驰E300L', guidePrice=50.0, produceTime='2003-02-03', carType='新能源'}],
prePage=1, nextPage=3, isFirstPage=false, isLastPage=false, hasPreviousPage=true, hasNextPage=true,
navigatePages=3, navigateFirstPage=1, navigateLastPage=3, navigatepageNums=[1, 2, 3]}
17 MyBatis的注解式开发
mybatis中也提供了注解式开发方式,采用注解可以减少Sql映射文件的配置。
当然,使用注解式开发的话,sql语句是写在java程序中的,这种方式也会给sql语句的维护带来成本。
官方是这么说的:
使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java 注解不仅力不从心,还会让你本就复杂的 SQL 语句更加混乱不堪。 因此,如果你需要做一些很复杂的操作,最好用 XML 来映射语句。
使用注解编写复杂的SQL是这样的:
原则:简单sql可以注解。复杂sql使用xml。
模块名:mybatis-015-annotation
打包方式:jar
依赖:mybatis,mysql驱动,junit,logback
配置文件:jdbc.properties、mybatis-config.xml、logback.xml
pojo:com.powernode.mybatis.pojo.Car
mapper接口:com.powernode.mybatis.mapper.CarMapper
17.1 @Insert
CarMapper接口
package com.powernode.mybatis.mapper;
import com.powernode.mybatis.pojo.Car;
import org.apache.ibatis.annotations.Insert;
public interface CarMapper {
@Insert(value="insert into t_car values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})")
int insert(Car car);
}
AnnotationTest.testInsert
package com.powernode.mybatis.test;
import com.powernode.mybatis.mapper.CarMapper;
import com.powernode.mybatis.pojo.Car;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;
public class AnnotationTest {
@Test
public void testInsert() throws Exception{
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
SqlSession sqlSession = sqlSessionFactory.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
Car car = new Car(null, "1112", "卡罗拉", 30.0, "2000-10-10", "燃油车");
int count = mapper.insert(car);
System.out.println("插入了几条记录:" + count);
sqlSession.commit();
sqlSession.close();
}
}
17.2 @Delete
CarMapper接口
@Delete("delete from t_car where id = #{id}")
int deleteById(Long id);
AnnotationTest.testDelete
@Test
public void testDelete() throws Exception{
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
SqlSession sqlSession = sqlSessionFactory.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
mapper.deleteById(89L);
sqlSession.commit();
sqlSession.close();
}
17.3 @Update
CarMapper接口
@Update("update t_car set car_num=#{carNum},brand=#{brand},guide_price=#{guidePrice},produce_time=#{produceTime},car_type=#{carType} where id=#{id}")
int update(Car car);
AnnotationTest.testUpdate
@Test
public void testUpdate() throws Exception{
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
SqlSession sqlSession = sqlSessionFactory.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
Car car = new Car(88L,"1001", "凯美瑞", 30.0,"2000-11-11", "新能源");
mapper.update(car);
sqlSession.commit();
sqlSession.close();
}
17.4 @Select & @Results
CarMapper接口
@Select("select * from t_car where id = #{id}")
@Results({
@Result(column = "id", property = "id", id = true),
@Result(column = "car_num", property = "carNum"),
@Result(column = "brand", property = "brand"),
@Result(column = "guide_price", property = "guidePrice"),
@Result(column = "produce_time", property = "produceTime"),
@Result(column = "car_type", property = "carType")
})
Car selectById(Long id);
AnnotationTest.testSelectById
@Test
public void testSelectById() throws Exception{
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
SqlSession sqlSession = sqlSessionFactory.openSession();
CarMapper carMapper = sqlSession.getMapper(CarMapper.class);
Car car = carMapper.selectById(88L);
System.out.println(car);
}
执行结果: