【JavaEE】(17) MyBatis 基础
一、什么是 MyBatis
之前我们学习了用 JDBC 操作数据库,但流程复杂且重复:
- 创建数据库连接池,获取数据库连接。(配置数据库连接的 url、用户名、密码)
- 创建带占位符的 SQL 语句。(写 SQL 语句)
- 参数绑定,替换占位符。(设置参数值)
- 执行 SQL 语句返回结果集。
- 处理结果集。(SQL 字段映射为 Java 对象)
- 释放资源。
以上流程中,只有括号中的内容需要个性化设置,其余的操作都是重复的。MyBatis 就是基于 JDBC 封装的用于简化程序与数据库交互的框架(持久层框架)。
二、快速入门
1、添加依赖
注意:Spring Boot 3.X(2.X) 对应 MyBatis 3.X(2.X)
2、建立用户表和实体类
SQL 字段命名标准:全小写(Windows 不区分大小写,Linux 区分,全小写不会出错)、蛇形,如 delete_flag(便于后续按照规律自动把 SQL 字段映射为 Java 属性)。
Java 属性命名标准:小驼峰。
-- 创建数据库
DROP DATABASE IF EXISTS mybatis_test;CREATE DATABASE mybatis_test DEFAULT CHARACTER SET utf8mb4;-- 使用数据数据
USE mybatis_test;-- 创建表[用户表]
DROP TABLE IF EXISTS user_info;
CREATE TABLE `user_info` (`id` INT ( 11 ) NOT NULL AUTO_INCREMENT,`username` VARCHAR ( 127 ) NOT NULL,`password` VARCHAR ( 127 ) NOT NULL,`age` TINYINT ( 4 ) NOT NULL,`gender` TINYINT ( 4 ) DEFAULT '0' COMMENT '1-男 2-女 0-默认',`phone` VARCHAR ( 15 ) DEFAULT NULL,`delete_flag` TINYINT ( 4 ) DEFAULT 0 COMMENT '0-正常, 1-删除',`create_time` DATETIME DEFAULT now(),`update_time` DATETIME DEFAULT now() ON UPDATE now(),PRIMARY KEY ( `id` )
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4; -- 添加用户信息
INSERT INTO mybatis_test.user_info( username, `password`, age, gender, phone )
VALUES ( 'admin', 'admin', 18, 1, '18612340001' );
INSERT INTO mybatis_test.user_info( username, `password`, age, gender, phone )
VALUES ( 'zhangsan', 'zhangsan', 18, 1, '18612340002' );
INSERT INTO mybatis_test.user_info( username, `password`, age, gender, phone )
VALUES ( 'lisi', 'lisi', 18, 1, '18612340003' );
INSERT INTO mybatis_test.user_info( username, `password`, age, gender, phone )
VALUES ( 'wangwu', 'wangwu', 18, 1, '18612340004' );
package com.edu.mybatisdemo.demos;import lombok.Data;import java.util.Date;@Data
public class UserInfo {private Integer id;private String username;private String password;private Integer age;private Integer gender;private String phone;private Integer deleteFlag;private Date createTime;private Date updateTime;
}
3、数据库连接配置
用的时候直接 copy:如果密码是数字,需要用双引号引起来。
spring:datasource:url: jdbc:mysql://127.0.0.1:3306/mybatis_test?characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=trueusername: rootpassword: "123456"driver-class-name: com.mysql.jdbc.Driver
4、持久层实现
- MyBatis 持久层接口,规范命名 xxxMapper。
- @Mapper 注解由 MyBatis 引入,用于将 bean 交给 Spring 管理。
- @Select 表示查询的实现(我们设计成抽象方法,由注解实现方法)。增删改操作同理。
package com.edu.mybatisdemo.demos.mapper;import com.edu.mybatisdemo.demos.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;import java.util.List;@Mapper
public interface UserInfoMapper {// 查询所有用户信息@Select("select * from user_info")public List<UserInfo> selectAll();
}
5、单元测试
写 Controller、Service 提供接口,然后利用浏览器或 postman 进行某个功能的测试很麻烦,我们直接根据某个功能方法写单元测试。
- @SpringBootTest:加载 Spring 环境,包括 IoC 容器。
- @Test:表示可执行的方法。如果写 main 方法测试,则只能有一个测试方法。用 @Test 则可以有多个。
SQL 字段与 Java 类属性名不一样的,没有绑定成功:
自动添加测试类:在需要测试的类中操作。
package com.edu.mybatisdemo.demos.mapper;import com.edu.mybatisdemo.demos.model.UserInfo;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;import java.util.List;@SpringBootTest
class UserInfoMapperTest {@Autowiredprivate UserInfoMapper userInfoMapper;@BeforeEachvoid setUp() {System.out.println("Before each test");}@AfterEachvoid tearDown() {System.out.println("After each test");}@Testvoid selectAll() {List<UserInfo> userInfoList = userInfoMapper.selectAll();System.out.println(userInfoList);}
}
三、基础操作(注解方式)
1、打印日志
mybatis:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
2、查
2.1、字段绑定
SQL 字段命名与 Java 类(返回类型)属性命名不匹配,绑定不上:
(1)方法1
在 SQL 语句中使用 as 重命名字段,与 Java 属性名匹配:
(2)方法2(@Result)
在 Java 中使用 @Results、@Result 注解绑定字段和属性:
(3)方法3(推荐)
配置:自动根据 SQL 字段的小写、蛇形命名,转换为 Java 属性小驼峰命名。
mybatis:configuration:map-underscore-to-camel-case: true # 配置驼峰⾃动转换
2.2、参数传递
- 如果只有一个参数, #{} 里的属性名可以和参数名不一致,但建议一致。
- 如果有多个参数, #{} 里的属性名要么和参数名一致,要么按 param1、param2 的顺序命名(不建议,因为如果再加一个参数,顺序又要重新改)。
- 总之,建议 #{} 里的属性名和参数名一致。
- 可以用 @Param 给参数重命名。如果重命名的参数是对象,那么 #{} 里按 对象参数名.属性 引用。
- 如果命名一模一样,还出错,加 @Param 重命名。
3、增(@Option)
- 返回的是插入成功的条数。
- 如果想插入后立即获取 id(自增项),使用 @Options。 比如创建订单后立即获取订单 id。
4、删
5、改
四、基础操作(XML 方式)
1、配置文件
配置 xml 文件所在路径:
- classpath 表示在 resources 文件下。
- mapper/**Mapper.xml 表示 mapper 文件下所有以 Mapper.xml 结尾的文件,可以自定义。
mybatis:mapper-locations: classpath:mapper/**Mapper.xml
2、快速入门
(1)定义 mapper 接口:
(2)添加 xml 实现方法:
固定格式:
- namespace 表示要实现哪个接口(全限定名)。
<?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.example.demo.mapper.UserInfoMapper"></mapper>
加入 MyBatisX 插件,自动生成部分 xml 代码:方法名最好以功能命名,如 select,便于自动生成代码。
(3)单元测试
3、查
SQL 字段绑定到返回值类型的属性上:
- 驼峰命名自动转换。
- 起别名。
- 指定映射关系。
参数传递规则跟注解方式一样。
4、增、删、改
其它的跟注解方式差不多,只不过换成了在 xml 标签里面写 SQL 语句。
5、多表查询
准备文章表:
-- 创建文章表
DROP TABLE IF EXISTS article_info;CREATE TABLE article_info (id INT PRIMARY KEY auto_increment,title VARCHAR ( 100 ) NOT NULL,content TEXT NOT NULL,uid INT NOT NULL,delete_flag TINYINT ( 4 ) DEFAULT 0 COMMENT '0-正常, 1-删除',create_time DATETIME DEFAULT now(),update_time DATETIME DEFAULT now()
) DEFAULT charset 'utf8mb4';-- 插入测试数据
INSERT INTO article_info ( title, content, uid ) VALUES ( 'Java', 'Java正文', 1 );
文章表的实体类:
package com.edu.mybatisdemo.demos.model;import lombok.Data;import java.util.Date;@Data
public class ArticleInfo {private Integer id;private String title;private String content;private Integer uid;private Integer deleteFlag;private Date createTime;private Date updateTime;// 用户部分信息private String username;private Integer age;
}
多表查询语句:
select ta.*, tu.username, tu.age from article_info ta
left join user_info tu on ta.uid = tu.id where ta.id = 1;
但工作中不会用多表查询,因为太慢了(要做笛卡尔积),通常会拆开:
select * from article_info where id = 1;
从结果集中取出 uid,再根据 uid 在用户表中查询 username 和 age。
对于一次程序与数据库的交互,不能超过 100ms。对于大数据量的多表查询,非常慢,并且很容易导致其它程序无法对数据库进行访问。拆成多条的话,多线程提高效率。
五、#{} 和 ${}
1、预编译 SQL 和即时 SQL
- #{} 是预编译 SQL,输入的参数并没有拼接到 SQL 语句中,而是用 ? 占位。编译时,它对于字符串类型的参数,会自动根据类型添加上 ' '。
- ${} 是即时 SQL,输入的参数会直接拼接到 SQL 语句中。它对于字符串类型的参数,需要手动添加上 ' '。
预编译 SQL:
即时 SQL:直接拼接,对于字符串类型参数,要手动加引号。
2、两者的区别
(1)预编译 SQL 性能更高
SQL 执行流程:
- 语法解析:校验语法对不对。
- 语法优化。
- 语法编译。
- 语法执行。
对于即时 SQL,会直接拼接好 SQL 语句后,执行上述流程。如果对于多条仅参数不同 SQL 语句,会逐步执行上述所有流程(每条 SQL 语句都不同)。
对于预编译 SQL,只有同样的一条 SQL 语句,那么语法解析、优化、编译都是一样的,因此会缓存编译结果。对于不同的参数,在执行前替换占位符即可。
(2)预编译 SQL 更安全(防止 SQL 注入)
SQL 注入:通过设计输入参数,更改预先定义好的 SQL 语句,来攻击服务器。
如,输入参数:' or 1='1
而预编译 SQL 不会被攻击:
(3)预编译 SQL 无法实现排序功能
我们有时希望可以根据参数选择升序、降序排序。但预编译 SQL 会自动根据参数类型加上引号,导致 SQL 语句出错。但即时 SQL 是直接拼接,可以设计为不手动加引号,SQL 语句正确。
但是即时 SQL 不安全,为了避免 SQL 注入,我们需要校验输入参数是否在设计范围之内。
(4)直接使用预编译 SQL 无法实现 like 查询
同样,预编译无法根据输入参数,来进行模糊查询。
但为了防止 SQL 注入,使用 mysql 的内置函数 concat() 来处理:
六、数据库连接池
为了避免重复创建、销毁数据库连接,提高效率,程序启动时就创建一个数据库连接池,里面存放了多个连接对象。需要时就从里面取连接,用完了就放回去。
常见的数据库连接池:Hikari, Druid。
Hikari 是 SpringBoot 默认使用的:
如果想换成其它的,比如 Druid,要在 POM 配置依赖:
// Spring Boot 3.X
<dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-3-starter</artifactId><version>1.2.21</version></dependency>// Spring Boot 2.X
<dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.17</version></dependency>
七、MySQL 企业规范
- 字段名:小写字母或数字。避免数字开头、数字在两个下划线中间。数据库字段名修改代价大。
- 表必备三字段:id、创建时间、修改时间(需要时保证有记录可查)。
- 避免用 *(网络开销大),就算全部字段都要用也建议一个个写全(修改字段后,容易发现不了 ResultMap 没有跟着修改的问题)。