1. 环境说明
数据库:MySQL
连接用户名:root
连接密码:123
库名:mybatis
导入以下测试数据:
-- ----------------------------
-- Table structure for employee
-- ----------------------------
DROP TABLE IF EXISTS `employee`;
CREATE TABLE `employee` (
`id` int(10) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(255) DEFAULT NULL COMMENT '姓名',
`position` varchar(255) DEFAULT NULL COMMENT '职位',
`salary` double(10,2) DEFAULT NULL COMMENT '薪资',
`department` varchar(255) DEFAULT NULL COMMENT '部门',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of employee
-- ----------------------------
BEGIN;
INSERT INTO `employee` VALUES (1, '张三', 'web开发工程师', 15000.00, '研发软件部');
INSERT INTO `employee` VALUES (2, '李四', '嵌入式Linux开发工程师', 14000.00, '研发软件部');
INSERT INTO `employee` VALUES (3, '王五', 'QT开发工程师', 13000.00, '研发软件部');
INSERT INTO `employee` VALUES (4, '赵明', '硬件工程师', 13000.00, '研发硬件部');
COMMIT;
Employee类代码如下
public class Employee {
private Integer id;
private String name;
private String position;
private Double salary;
private String department;
public Employee() {
}
public Employee(String name, String position, Double salary, String department) {
this.name = name;
this.position = position;
this.salary = salary;
this.department = department;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPosition() {
return position;
}
public void setPosition(String position) {
this.position = position;
}
public Double getSalary() {
return salary;
}
public void setSalary(Double salary) {
this.salary = salary;
}
public String getDepartment() {
return department;
}
public void setDepartment(String department) {
this.department = department;
}
@Override
public String toString() {
return "Employee{" +
"id=" + id +
", name='" + name + '\'' +
", position='" + position + '\'' +
", salary=" + salary +
", department='" + department + '\'' +
'}';
}
}
2. 整合Mybatis注解版
新建项目时,勾选以下依赖
2.1 配置Mybatis
spring:
datasource:
username: root
password: 123
#mysql8以上的驱动包需要指定以下时区
url: jdbc:mysql://127.0.0.1:23306/mybatis?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
注:
[1] 使用MySQL8以上的驱动包需要使用serverTimezone指定时区,否则会报错
[2] 指定字符编码的目的是为了防止中文乱码。
[3] 使用MySQL8以上的驱动包时,driver-class-name需要指定为com.mysql.cj.jdbc.Driver,而不是原来的com.mysql.jdbc.Driver
2.2 编写接口,实现增删改查操作
新创建接口文件mapper.EmployeeMapper
@Mapper
public interface EmployeeMapper {
//根据id查找单个
@Select("select * from employee where id = #{id}")
Employee getById(Integer id);
//查找全部
@Select("select * from employee")
List<Employee> getAll();
//添加
//使用自增主键
@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert("insert into employee(name,position,salary,department) values(#{name},#{position},#{salary},#{department})")
int add(Employee employee);
@Update("update employee set name = #{name},position = #{position},salary = #{salary}, department = #{department} where id = #{id}")
int update(Employee employee);
@Delete("delete from employee where id = #{id}")
int delete(Integer id);
}
@Mapper注解:标识这个接口是数据库映射接口,同时将其注入到SpringIOC容器
@Insert、@Delete、@Update、@Select实现增删该查操作
@Options指定useGeneratedKeys = true,代表自动生成主键;用keyProperty指定主键是哪一个
2.3 编写测试类,测试代码
@SpringBootTest
class EmployeeMapperTest {
@Autowired
EmployeeMapper employeeMapper;
@Test
void getById() {
Employee employee = employeeMapper.getById(1);
System.out.println(employee);
}
@Test
void getAll() {
List<Employee> employees = employeeMapper.getAll();
System.out.println(employees);
}
@Test
void add() {
Employee employee = new Employee("晓张","射频工程师",14000.00,"研发硬件部");
employeeMapper.add(employee);
}
@Test
void update() {
Employee employee = new Employee("张三","web开发工程师",15000.00,"服务端开发部");
employee.setId(1);
employeeMapper.update(employee);
}
@Test
void delete() {
employeeMapper.delete(5);
}
}
可以看到全部测试通过
从打印的日志可以看出,SpringBoot底层采用的是HikariPool连接池。
2.4 编写service,调用Mapper
接口编写:
public interface IEmployeeService {
Employee getById(Integer id);
List<Employee> getAll();
int add(Employee employee);
int update(Employee employee);
int delete(Integer id);
}
接口实现:
@Service
public class EmployeeService implements IEmployeeService {
@Autowired
EmployeeMapper employeeMapper;
@Override
public Employee getById(Integer id) {
return employeeMapper.getById(id);
}
@Override
public List<Employee> getAll() {
return employeeMapper.getAll();
}
@Override
public int add(Employee employee) {
return employeeMapper.add(employee);
}
@Override
public int update(Employee employee) {
return employeeMapper.update(employee);
}
@Override
public int delete(Integer id) {
return employeeMapper.delete(id);
}
}
2.5 编写Controller调用Service
@RestController
public class EmployeeController {
@Autowired
IEmployeeService employeeService;
@GetMapping("/employees")
public List<Employee> getAllEmployee()
{
return employeeService.getAll();
}
}
成功获取到数据
3. 整合阿里Druid连接池
在开始之前先说一下为什么要使用阿里Druid连接池,它有什么优点?
- 提供强大的监控功能(监控执行的SQL语句以及SQL语句的执行状态、监控Session、查看连接配置等等)
- 运行稳定,在高并发场景中得到了验证
3.1 引入Druid依赖
在Maven仓库中直接以Druid为关键字进行搜索,Druid Spring Boot Starter就是我们要找的依赖
将其添加进项目中
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.21</version>
</dependency>
至此,成功引入了Druid依赖
3.2 切换连接池为Druid连接池
直接指定spring.datasource.type即可,将其指定为DruidDataSource
spring:
datasource:
username: root
password: 123
#mysql8以上的驱动包需要指定以下时区
url: jdbc:mysql://127.0.0.1:23306/mybatis?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
3.3 配置Druid
通过配置spring.datasource.druid属性配置Druid
druid:
# 初始连接个数
initial-size: 5
# 最小连接池数量
min-idle: 5
# 最大连接池数量
max-active: 20
# 获取连接时最大等待时间,单位为毫秒
max-wait: 5000
# 状态监控
filter:
stat:
# 使能状态监控
enabled: true
db-type: mysql
log-slow-sql: true
slow-sql-millis: 2000
# 监控过滤器
web-stat-filter:
enabled: true
# 配置过滤器不拦截哪些uri
exclusions:
- "*.js"
- "*.gif"
- "*.jpg"
- "*.png"
- "*.css"
- "*.ico"
- "/druid/*"
# druid 监控页面
stat-view-servlet:
enabled: true
# 配置监控界面uri
url-pattern: /druid/*
reset-enable: false
# 配置登陆用户名
login-username: root
# 配置登陆密码
login-password: root
在上面的配置文件中:
- 配置druid.filter.stat的目的是为了实现监控统计功能
- 监控界面的uri为/druid
- 登陆用户名为root、登陆密码为root
3.4 数据监控
访问Druid监控页面
可以看到数据库配置信息
监控web应用
监控执行的SQL语句
至此,Druid整合完毕。
4. 整合Mybatis配置文件版
Mybatis配置文件版与注解版在整合上的区别:
- 不在Mapper类中写SQL语句,SQL语句写在映射文件中
- 指定Mybatis核心配置文件的路径和映射文件路径
4.1 改写一下映射接口
@Mapper
public interface EmployeeMapper {
//根据id查找单个
Employee getById(Integer id);
//查找全部
List<Employee> getAll();
//添加
int add(Employee employee);
int update(Employee employee);
int delete(Integer id);
}
4.2 创建Mybatis核心配置文件
在resources下新建mybatis文件夹,并在mybatis文件夹下新创建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>
</configuration>
至此,mybatis核心配置文件创建完毕
4.3 创建Mybatis映射接口配置文件
在resources/mybatis目录下创建mapper文件夹,专门存放映射配置文件,并在mapper目录下创建employeeMapper.xml文件:
employee文件中填入以下内容:
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--
这里的namespace对应接口的全路径名,这样Mapper配置文件和Mapper接口会自动绑定
-->
<mapper namespace="com.springboot.mapper.EmployeeMapper">
<!--
数据库中的字段名和对象的属性名一样时,可以省略resultMap
-->
<resultMap type="com.springboot.entity.Employee" id="employeeMap">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="position" property="position"/>
<result column="salary" property="salary"/>
<result column="department" property="department"/>
</resultMap>
<!--
id和接口方法名写成一致,这样可以自动绑定
-->
<select id="getById" resultMap="employeeMap">
select * from employee where id = #{id}
</select>
<select id="getAll" resultMap="employeeMap">
select * from employee
</select>
<insert id="add">
insert into employee(name,position,salary,department) values(#{name},#{position},#{salary},#{department})
</insert>
<update id="update">
update employee set name = #{name},position = #{position},salary = #{salary}, department = #{department} where id = #{id}
</update>
<delete id="delete">
delete from employee where id = #{id}
</delete>
</mapper>
- mapper配置文件的namespace指定为映射接口的全路径名,则两者将自动绑定
- 数据库中的字段名和对象的属性名一样时,可以省略resultMap,此时需要将resultMap="employeeMap"改为resultType=“Employee对象的全类名”
- 映射语句的id和接口方法名写成一致,这样便可以自动绑定
4.4 指定Mybatis配置文件路径
#配置Mybatis相关映射文件路径
mybatis:
#映射配置文件路径
mapper-locations: classpath:mybatis/mapper/*.xml
#核心配置文件路径
config-location: classpath:mybatis/mybatis-config.xml
全部测试通过,完成!
5. 事务
5.1 什么是事务,为什么要使用事务
以转账为例,A给B转账10元:
第一步:先从数据库中把A的余额-10
第二步:再从数据库中把B的余额+10
假设,执行第一步成功了,但执行第二步失败了(或执行第一步和第二步中间的某条语句时失败了,导致根本执行不到第二步)。那将会造成A的余额减少了,但B的余额没有增加的问题。
因为中间某条可能发生的错误,导致整个业务逻辑没有正确执行完。之前成功操作的数据并不可靠,需要将其回退。
事务的作用就是为了保证用户的每一个操作都是可靠的,事务中的每一步操作都必须成功执行,只要有发生异常就回退到事务开始未进行操作的状态。
我们在往数据库中写数据时,一般都要用到事务管理,Spring对此提供了良好的支持。
5.2 使用事务
使用Spring事务管理只需以下两个步骤:
- 在Spring启动类使用@EnableTransactionManagement开启注解事务管理
- 在 Service层方法上添加 @Transactional 进行事务管理
开启注解事务管理
@SpringBootApplication
@EnableTransactionManagement
public class SpringMybatis01Application {
public static void main(String[] args) {
SpringApplication.run(SpringMybatis01Application.class, args);
}
}
进行事务管理
@Transactional
public void addMulEmployee()
{
//假设下面这几个员工必须要一起添加
Employee employee1 = new Employee("name1","position1",10000D,"department1");
Employee employee2 = new Employee("name2","position1",10000D,"department1");
Employee employee3 = new Employee("name3","position1",10000D,"department1");
Employee employee4 = new Employee("name4","position1",10000D,"department1");
Employee employee5 = new Employee("name5","position1",10000D,"department1");
employeeMapper.add(employee1);
employeeMapper.add(employee2);
employeeMapper.add(employee3);
employeeMapper.add(employee4);
employeeMapper.add(employee5);
}
中间发生异常,前面添加的数据将会回滚,employee1、employee2、employee3将不会出现在数据库中。
employeeMapper.add(employee1);
employeeMapper.add(employee2);
employeeMapper.add(employee3);
//模拟异常情况
int i = 1 / 0;
employeeMapper.add(employee4);
employeeMapper.add(employee5);
注:针对MySQL而言,InnoDB引擎支持事务,MyISAM引擎不支持。
5.3 事务隔离级别
隔离级别是指在发生并发的事务之间的隔离程度,与我们开发时候主要相关的场景包括:脏读、不可重复读、幻读。
- 脏度:事务B读取了事务A未提交的数据,结果事务A回滚了,这就导致事务B读到了不正确的数据,这就是脏读。
- 不可重复读:事务A读取了数据,然后执行一些逻辑。在此过程中,事务B修改了事务A刚刚读取的数据。当事务A再次读取时,发现数据不匹配了,这就是不可重复读。
- 幻读:事务A根据查询条件读到了N条数据,事务B插入了M条符合事务A查询条件的数据,导致事务A再次查询就有N+M条数据了,这就是幻读。
接下来看一看Spring为我们提供的几种事务隔离级别
public enum Isolation {
DEFAULT(-1),
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);
private final int value;
private Isolation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
- DEFAULT:使用数据库的默认隔离级别,大部分数据库使用的隔离级别是READ_COMMITTED
- READ_UNCOMMITTED:该级别表示事务A可以读取事务B修改但未提交的数据,该隔离级别不能防止脏读和不可重复读。
- READ_COMMITTED:事务A只能读取事务B已提交的数据,该隔离级别可以防止脏读。
- REPEATABLE_READ:该隔离级别表示在一个事务执行期间可以多次执行某个相同的查询,每次返回的内容都一样,即使数据有变更,该隔离级别可以防止不可重复读和幻读。
- SERIALIZABLE:所有事务依次执行,各个事务之间就不可能存在冲突了,该隔离级别可以防止脏读、幻读、不可重复读,但性能很差,一般不用它。
综合考虑,绝大多数情况下使用READ_COMMITTED级别就好。设置方式:
@Transactional(isolation = Isolation.READ_COMMITTED)
对于大多数数据库而言,不用设置就可以,因为默认就是READ_COMMITTED级别
5.4 事务传播行为
概念:在开始一个事务之前,已经存在一个事务。此时可以指定这个要开始的事务的执行行为。
Spring为我们提供了以下事务传播行为:
public enum Propagation {
REQUIRED(0),
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3),
NOT_SUPPORTED(4),
NEVER(5),
NESTED(6);
private final int value;
private Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
- REQUIRED:默认,如果当前存在事务,则加入该事务,否则新创建一个事务。
- SUPPORTS:如果当前存在事务,则加入该事务,不存在事务,则以非事务方式运行(一般不用)
- MANDATORY:如果当前存在事务,则加入该事务,如果不存在,则抛出异常。
- REQUIRES_NEW:创建一个新事务,如果当前存在其他事务,将其他事务挂起(一般不用)。
- NOT_SUPPORTED:以非事务方式运行,如果当前存在其他事务,则将其他事务挂起(一般不用)。
- NEVER:以非事务方式运行,如果当前存在事务,则抛出异常(一般不用)。
- NESTED:如果当前存在事务,则创建一个新事务,并和原来存在的事务嵌套运行。如果当前不存在事务,则创建一个新事务运行。
事务传播行为采用默认方式最好。如果想要指定,可以通过以下方式指定:
@Transactional(propagation = Propagation.REQUIRED)