当前位置: 首页 > news >正文

【Spring Boot 快速入门】四、MyBatis

目录

  • MyBatis(一)入门
    • 简介
    • MyBatis 入门
    • Lombok
    • MyBatis 基础操作
      • 数据准备
      • 删除
      • 预编译
      • 新增
      • 更新
      • 查询

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 工作原理:

  1. Java 调用 Mapper 接口
  2. MyBatis 根据配置 XML/注解
  3. 执行 SQL
  4. 映射结果
  5. 返回 Java 对象

MyBatis 入门

步骤:

  1. 准备工作(创建工程、数据库表、实体类)
  2. 引入 MyBatis 相关依赖,配置 MyBatis
  3. 编写 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 语句在数据库执行前先进行编译,生成执行计划,在实际执行时只需提供参数即可。预编译过程主要包括:

  1. 解析 SQL 结构
  2. 检查语法和语义
  3. 生成执行计划
  4. 缓存该语句(可重复使用)

使用 #{} 的方式,MyBatis 会生成使用 ? 占位符的预编译 SQL,数据库只需预编译一次并可复用执行计划,后续的不同参数只需要替换 ? 即可;而写死参数的 SQL,每次都是新的语句,数据库必须重新预编译,效率低。

预编译的优势:

  1. 性能优化:SQL 结构只编译一次,多次执行复用执行计划,效率更高
  2. 防 SQL 注入:参数绑定,不会直接拼接SQL字符串,防止恶意注入
  3. 代码更简洁:统一结构 + 参数替换,代码更清晰

那么什么又是 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 会自动封装。
  • 如果实体类属性名和数据库表查询返回的字段名不一致,不能自动封装。

解决方法:

  1. 给字段起别名:

    //根据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);
    
  2. 通过 @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);
    
  3. 开启 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 注入的问题。

http://www.dtcms.com/a/305782.html

相关文章:

  • Nestjs框架: 关于 OOP / FP / FRP 编程
  • 关于神经网络CNN的搭建过程以及图像卷积的实现过程学习
  • OSS-服务端签名Web端直传+STS获取临时凭证+POST签名v4版本开发过程中的细节
  • 修改Windows鼠标滚轮方向
  • 《计算机组成原理与汇编语言程序设计》实验报告六 存储器实验
  • mangoDB面试题及详细答案 117道(071-095)
  • LeetCode 160:相交链表
  • 使用es实现全文检索并且高亮显示
  • 利用SQL文件上传注入植入WebShell
  • Linux->动静态库
  • UniSeg3D:A Unified Framework for 3D Scene Understanding
  • 如何读懂 火山方舟 API 部分的内容
  • yolo8+阿里千问图片理解(华为简易版小艺看世界)
  • PostgreSQL 与 Oracle 数据库字段类型的详细对比
  • CSS 工作原理
  • Qt知识点2『Ubuntu24.04.2安装Qt5.12.9各种报错』
  • git报failed to connect to github.com port 443 after 21064
  • 项目文档太多、太混乱怎么解决
  • Qt 在 ARM 平台上的移植与优化
  • 中国高铁从追赶到领跑的破壁之路
  • 15.11 单卡训练770M参数模型!DeepSpeed ZeRO-3实战:RTX 4090显存直降6.8GB
  • 接口自动化测试
  • 深入剖析 StarRocks 与 Hive 的区别、使用场景及协同方案实践
  • 【IDEA】JavaWeb自定义servlet模板
  • 手机定位和IP属地究竟有何不同
  • 【Lambda】flatMap使用案例
  • Redis 面试全解析:从数据结构到集群架构(含实战解决方案)
  • 《Java 程序设计》第 10 章 - 接口与 Lambda 表达式
  • #C语言——学习攻略:深挖指针路线(四续)——函数指针数组--转移表
  • 【支持Ubuntu22】Ambari3.0.0+Bigtop3.2.0——Step4—时间同步(Chrony)