目录
- MyBatis(一)入门
- 简介
- MyBatis 入门
- Lombok
- MyBatis 基础操作
- 数据准备
- 删除
- 预编译
- 新增
- 更新
- 查询
- XML 映射文件
MyBatis(一)入门
简介
MyBatis 是一款 优秀的持久层框架,它支持 自定义 SQL、存储过程以及高级映射,是 Java 开发中连接数据库的常用工具之一,属于 ORM(对象关系映射)框架 的一种实现形式。
MyBatis 最初是由 Apache 团队开发的 iBatis 项目,后来由 Google Code 迁移到 GitHub 并更名为 MyBatis。它是一个半自动化的 ORM 框架,开发者自己写 SQL,MyBatis 负责将 SQL 的执行结果与 Java 对象进行自动映射。
MyBatis 的核心特点:
- SQL 编写自由:开发者可以完全控制 SQL,实现灵活的数据库操作
- 简单易用:学习成本低、配置清晰
- 支持映射关系:支持一对一、一对多等对象映射
- 动态 SQL:支持 if、choose、where 等标签,动态拼接 SQL
- 与 Spring 整合:配合 Spring Boot 使用非常方便
- 缓存支持:内置一级缓存,支持二级缓存插件扩展
MyBatis 工作原理:
- Java 调用 Mapper 接口
- MyBatis 根据配置 XML/注解
- 执行 SQL
- 映射结果
- 返回 Java 对象
MyBatis 入门
步骤:
- 准备工作(创建工程、数据库表、实体类)
- 引入 MyBatis 相关依赖,配置 MyBatis
- 编写 SQL 语句(注解/XML)
创建工程除了添加 Spring Web 依赖,还要添加 MyBatis Framework 和 MySQL Driver 依赖:
连接数据源,选择 MySQL:
填写用户名、密码和要连接的数据库名,点击测试连接,成功即可应用:
在配置文件 application.properties 中配置数据库信息:
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
spring.datasource.username=root
spring.datasource.password=123456
创建数据库及表:
create database if not exists `mybatis`;
use `mybatis`;
create table if not exists user (id int primary key auto_increment,name varchar(20),age tinyint,gender tinyint comment '1-male, 2-female',phone varchar(20)
)comment '用户表';
insert into user (name, age, gender, phone) values('赵刚', 18, 1, '12345678901'),('王芳', 19, 2, '12345678902'),('林伟', 20, 1, '12345678903'),('马丽', 21, 2, '12345678904'),('孙浩', 22, 1, '12345678905');
对应的实体类:
public class User {private Integer id;private String name;private Short age;private Short sex;private String phone;public User() {}public User(Integer id, String name, Short age, Short sex, String phone) {this.id = id;this.name = name;this.age = age;this.sex = sex;this.phone = phone;}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 Short getAge() {return age;}public void setAge(Short age) {this.age = age;}public Short getSex() {return sex;}public void setSex(Short sex) {this.sex = sex;}public String getPhone() {return phone;}public void setPhone(String phone) {this.phone = phone;}@Overridepublic String toString() {return "User{" +"id=" + id +", name='" + name + '\'' +", age=" + age +", sex=" + sex +", phone='" + phone + '\'' +'}';}
}
创建 mapper 接口(原来的 dao 层):
import com.example.demo.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;@Mapper //在运行时会自动生成该接口的实现类对象(代理对象),并且将该对象交给Spring的IOC容器管理
public interface UserMapper {@Select("select * from user")public List<User> list();
}
在测试类中编写测试代码并运行:
import com.example.demo.mapper.UserMapper;
import com.example.demo.pojo.User;
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 Demo2ApplicationTests {@Autowiredprivate UserMapper userMapper;@Testpublic void testListUser() {List<User> userList = userMapper.list();for (User user : userList) {System.out.println(user);}}
}
控制台显示:
Lombok
Lombok 是一个实用的 Java 类库,能通过注解的形式自动生成构造器、getter/setter、equals、hashcode、toString 等方法,并可以自动化生成日志变量,简化 Java 开发、提高效率。
注解 | 作用 |
---|---|
@Getter/@Setter | 为所有的属性提供 get/set 方法 |
@ToString | 会给类自动生成易阅读的 toString 方法 |
@EqualsAndHashCode | 根据类所拥有的非静态字段自动重写 equals 方法和 hashCode 方法 |
@Data | 提供了更综合的生成代码功能(@Getter + @Setter + @ToString + @EqualsAndHashCode) |
@NoArgsConstructor | 为实体类生成无参的构造器方法 |
@AllArgsConstructor | 为实体类生成除了 static 修饰的字段之外带有各参数的构造器方法。 |
Lombok 依赖:
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId>
</dependency>
在一些 IDEA 老的版本中没有集成 Lombok 插件,需要自行前往插件市场安装应用。
MyBatis 基础操作
数据准备
创建数据库表:
-- 部门管理
create table dept(id int unsigned primary key auto_increment comment '主键ID',name varchar(10) not null unique comment '部门名称',create_time datetime not null comment '创建时间',update_time datetime not null comment '修改时间'
) comment '部门表';insert into dept (id, name, create_time, update_time) values(1,'学工部',now(),now()),(2,'教研部',now(),now()),(3,'咨询部',now(),now()),(4,'就业部',now(),now()),(5,'人事部',now(),now());-- 员工管理
create table emp (id int unsigned primary key auto_increment comment 'ID',username varchar(20) not null unique comment '用户名',password varchar(32) default '123456' comment '密码',name varchar(10) not null comment '姓名',gender tinyint unsigned not null comment '性别, 说明: 1 男, 2 女',job tinyint unsigned comment '职位, 说明: 1 班主任,2 讲师, 3 学工主管, 4 教研主管, 5 咨询师',entrydate date comment '入职时间',dept_id int unsigned comment '部门ID',create_time datetime not null comment '创建时间',update_time datetime not null comment '修改时间'
) comment '员工表';INSERT INTO emp(id, username, password, name, gender, job, entrydate,dept_id, create_time, update_time) VALUES(1,'zhangwei','123456','张伟',1,4,'2000-01-01',2,now(),now()),(2,'liqiang','123456','李强',1,2,'2015-01-01',2,now(),now()),(3,'wangjun','123456','王军',1,2,'2008-05-01',2,now(),now()),(4,'liuyang','123456','刘洋',1,2,'2007-01-01',2,now(),now()),(5,'chenming','123456','陈明',1,2,'2012-12-05',2,now(),now()),(6,'humin','123456','胡敏',2,3,'2013-09-05',1,now(),now()),(7,'zhuyan','123456','朱妍',2,1,'2005-08-01',1,now(),now()),(8,'guoyan','123456','郭燕',2,1,'2014-11-09',1,now(),now()),(9,'linling','123456','林玲',2,1,'2011-03-11',1,now(),now()),(10,'heqian','123456','何倩',2,1,'2013-09-05',1,now(),now()),(11,'gaoxiang','123456','高翔',1,5,'2007-02-01',3,now(),now()),(12,'liangchao','123456','梁超',1,5,'2008-08-18',3,now(),now()),(13,'luoyi','123456','罗毅',1,5,'2012-11-01',3,now(),now()),(14,'mahui','123456','马辉',1,2,'2002-08-01',2,now(),now()),(15,'huangyong','123456','黄勇',1,2,'2011-05-01',2,now(),now()),(16,'wupeng','123456','吴鹏',1,2,'2010-01-01',2,now(),now()),(17,'zhenlei','123456','郑磊',1,NULL,'2015-03-21',NULL,now(),now());
创建实体类:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.time.LocalDate;
import java.time.LocalDateTime;@Data
@NoArgsConstructor
@AllArgsConstructor
public class Emp {private Integer id;private String username;private String password;private String name;private Short gender;private Short job;private LocalDate entrydate;private Integer deptid;private LocalDateTime createTime;private LocalDateTime updateTime;
}
mapper 接口:
import org.apache.ibatis.annotations.Mapper;@Mapper
public interface EmpMapper {}
后面的操作都是按照以上数据进行
删除
在 mapper 接口中编写删除操作的代码:
//根据ID删除数据
@Delete("delete from emp where id=#{id}") // #{} 是 MyBatis 中动态获取数据的占位符
public int deleteById(Integer id);
在测试类中编写测试方法的代码:
@Autowired
private EmpMapper empMapper;
@Test
public void testDelete(){empMapper.deleteById(17);
}
一般这样写是没有返回值,如果需要看是否删除了数据,可以写成以下形式;
@Test
public void testDelete(){int deleteNum =empMapper.deleteById(17);System.out.println("删除了"+deleteNum+"行数据");
}
运行结果如下:
预编译
虽然前面的操作成功执行了,但是我们无法知道底层到底是怎么进行的,这个时候可以通过配置 MyBatis 日志来了解
在配置文件 application.properties 中加入以下配置即可开启 MyBatis 日志,并输出到控制台中:
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
运行测试方法就可以在控制台得到以下信息:
这样我们就能了解到底层是怎么进行的了
==> Preparing: delete from emp where id=?
==> Parameters: 17(Integer)
这是 SQL 中的预编译(Prepared Statement)是数据库编程中一种常见的优化和防注入方式。它将 SQL 语句的结构与数据参数分开处理,大大提高了执行效率并增强了安全性。
SQL 预编译是将 SQL 语句在数据库执行前先进行编译,生成执行计划,在实际执行时只需提供参数即可。预编译过程主要包括:
- 解析 SQL 结构;
- 检查语法和语义;
- 生成执行计划;
- 缓存该语句(可重复使用)。
使用 #{}
的方式,MyBatis 会生成使用 ?
占位符的预编译 SQL,数据库只需预编译一次并可复用执行计划,后续的不同参数只需要替换 ?
即可;而写死参数的 SQL,每次都是新的语句,数据库必须重新预编译,效率低。
预编译的优势:
- 性能优化:SQL 结构只编译一次,多次执行复用执行计划,效率更高
- 防 SQL 注入:参数绑定,不会直接拼接SQL字符串,防止恶意注入
- 代码更简洁:统一结构 + 参数替换,代码更清晰
那么什么又是 SQL 注入呢?用以下场景来演示:
-
用户登录功能就是在数据库表中查找是否有对应的用户名和密码
//根据用户名和密码查询用户 @Select("select * from emp where username='zhangwei' and password='123456'") public Emp getEmpByUsernameAndPassword();
结果如下,登录成功:
-
而现在用户名随便输入,密码输入“
'or'1'='1
”://根据用户名和密码查询用户 @Select("select count(*) from emp where username='asfgasgasf' and password=''or'1'='1'") public int getEmpByUsernameAndPassword();
结果也是登录成功:
这种情况就称为 SQL 注入,出现这种情况的原因是:
- 在 mapper 接口中写的 SQL 语句
@Select("select count(*) from emp where username='asfgasgasf' and password=''or'1'='1'")
在数据库中解析为SELECT count(*) FROM emp WHERE username='asfgasgasf' AND password='' OR '1'='1'
,因为'1'='1'
永远为真,整个 WHERE 条件恒为真,导致查询返回整张表的总行数。因此,如果后台通过count > 0
判断用户是否存在,就会错误地认为登录验证通过,从而实现 SQL 注入攻击。
而用了预处理的代码如下:
//根据用户名和密码查询用户
@Select("select count(*) from emp where username=#{username} and password=#{password}")
public int getEmpByUsernameAndPassword(String username,String password);@Test
public void testGetEmpByUsernameAndPassword(){int count = empMapper.getEmpByUsernameAndPassword("zhangsan","'or'1'='1");System.out.println(count);
}
运行测试结果如下:
显而易见,'or'1'='1
是以一个整体来替换 ?
,不会当作 SQL 语法解析,也就无法注入了。
参数占位符;
语法格式 | 特点及说明 | 使用时机 |
---|---|---|
#{...} | 执行 SQL 时,会将#{...} 替换为? ,生成预编译 SQL,会自动设置参数值,可有效防止 SQL 注入 | 参数传递场景,一般参数传递都使用#{...} |
${...} | 拼接 SQL,直接将参数拼接到 SQL 语句中,存在 SQL 注入问题 | 对表名、列名进行动态设置等场景(需谨慎,做好校验避免注入风险 ) |
新增
mapper 接口的代码:
//新增员工
@Insert("insert into emp(username,password,name,gender,job,entrydate,dept_id,create_time,update_time) " +"values(#{username},#{password},#{name},#{gender},#{job},#{entryDate},#{deptId},#{createTime},#{updateTime})")
public void insert(Emp emp);
测试类中的代码:
@Test
public void testInsert(){Emp emp = new Emp();emp.setUsername("张三");emp.setPassword("123456");emp.setName("张三");emp.setGender((short) 1);emp.setJob((short) 1);emp.setEntryDate(LocalDate.now());emp.setDeptId(1);emp.setCreateTime(LocalDateTime.now());emp.setUpdateTime(LocalDateTime.now());empMapper.insert(emp);
}
运行结果如下:
主键返回:在数据添加成功后,需要获取插入数据库数据的主键。如:添加套餐数据时,还需要维护套餐菜品关系表数据
只需要加上 @Options 注解即可:
//新增员工
@Options(useGeneratedKeys = true,keyProperty = "id")
@Insert("insert into emp(username,password,name,gender,job,entrydate,dept_id,create_time,update_time) " +"values(#{username},#{password},#{name},#{gender},#{job},#{entryDate},#{deptId},#{createTime},#{updateTime})")
public void insert(Emp emp);
运行结果如下:
更新
mapper 接口的代码:
//更新员工
@Update("update emp set username=#{username},password=#{password},name=#{name},gender=#{gender},job=#{job}, entrydate=#{entryDate},dept_id=#{deptId},update_time=#{updateTime} where id=#{id}")
public void update(Emp emp);
测试类中的代码:
@Test
public void testUpdate(){Emp emp = new Emp();emp.setId(19);emp.setUsername("lisi(update)");emp.setPassword("123456");emp.setName("李四(update)");emp.setGender((short) 1);emp.setJob((short) 2);emp.setEntryDate(LocalDate.now());emp.setDeptId(1);emp.setCreateTime(LocalDateTime.now());emp.setUpdateTime(LocalDateTime.now());empMapper.update(emp);
}
运行结果如下:
查询
mapper 接口的代码:
//根据ID查询员工
@Select("select * from emp where id=#{id}")
public Emp getById(Integer id);
测试类中的代码:
@Test
public void testGetById(){Emp emp = empMapper.getById(1);System.out.println(emp);
}
运行结果如下:
从结果中会发现,最后三个字段在数据库表中明明是有数据,却没有被获取到,这是因为 MyBatis 数据封装的原因。
数据封装:
- 实体类属性名和数据库表查询返回的字段名一致,MyBatis 会自动封装。
- 如果实体类属性名和数据库表查询返回的字段名不一致,不能自动封装。
解决方法:
-
给字段起别名:
//根据ID查询员工 @Select("select id, username, password, name, gender, job, entrydate, dept_id deptId, create_time createTime, update_time updateTime from emp where id=#{id}") public Emp getById(Integer id);
-
通过 @Results,@Result 注解手动映射封装
//根据ID查询员工 @Results({@Result(column = "dept_id", property = "deptId"),@Result(column = "create_time", property = "createTime"),@Result(column = "update_time", property = "updateTime") }) @Select("select * from emp where id=#{id}") public Emp getById(Integer id);
-
开启 MyBatis 的驼峰命名自动映射开关(推荐,但是类中的属性名必须要是驼峰命名,数据库表字段名必须要是
_
命名)//application.properties mybatis.configuration.map-underscore-to-camel-case=true
运行结果如下:
以上根据 ID 查询较为简单,而下面的条件查询则较为复杂。
mapper 接口的代码:
//条件查询员工
@Select("select * from emp where name like '%${name}%' and gender=#{gender} and entrydate between #{begin} and #{end} order by update_time desc")
public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end);
测试类中的代码:
@Test
public void testList(){List<Emp> list = empMapper.list("张", (short) 1, LocalDate.of(2000, 1, 1), LocalDate.now());System.out.println(list);
}
运行结果如下:
但是接口代码中使用的是 ${}
,存在 SQL 注入的问题,可以使用 SQL 中的 concat
函数来进行拼接:
//条件查询员工
@Select("select * from emp where name like concat('%',#{name},'%') and gender=#{gender} and entrydate between #{begin} and #{end} order by update_time desc")
public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end);
这样就能使用 #{}
来解决 SQL 注入的问题。
XML 映射文件
XML 映射文件规范:
- XML 映射文件的名称与 mapper 接口名称一致,并且将 XML 映射文件和 mapper 接口放置在相同包下(同包同名)
- XML 映射文件的 namespace 属性为 mapper 接口全限定名一致
- XML 映射文件中 SQL 语句的 id 与 mapper 接口中的方法名一致,并保持返回类型一致
在 resources 包下创建 mapper 接口的同名包:
在新创建的包下创建 mapper 接口的同名 XML 文件 EmpMapper.xml,并添加以下约束:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
mapper 接口中的方法为:
public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end);
EmpMapper.xml 中写 SQL 语句:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.EmpMapper"><select id="list" resultType="com.example.demo.pojo.Emp"><!--resultType 是单条记录封装的类型,要用对应实体类的全类名-->select * from emp where name like concat('%',#{name},'%') and gender=#{gender} and entrydate between #{begin} and #{end} order by update_time desc</select>
</mapper>
EmpMapper.xml 中的各项属性要与 mapper 接口中的一致:
如果有不一致的,则 XML 映射文件无法匹配上对应的 mapper 接口方法。