目录
1.MyBatis是什么?
2.为什么要学习MyBatis?
3.怎么学MyBatis?
4.搭建MyBatis开发环境
4.0.准备工作:创建数据库和表
4.1.添加MyBatis框架支持
4.1.1.老项目添加MyBatis(对自己之前的Spring项目进行升级)
4.1.2.新项目添加MyBatis(创建一个全新的MyBatis和Spring Boot的项目)
4.2.配置数据库的连接字符串和MyBatis的XML保存路径
4.2.1.在配置文件中配置数据库的连接字符串(系统的,key固定,value不固定)
4.2.2.配置MyBatis的XML保存路径(要使用XML的方式来操作MyBatis)
5.使用MyBatis模式操作数据库(添加业务代码)实现CRUD中的查询操作
PS:MyBatis模式
5.1.添加实体类
5.2.创建@Mapper接口文件
5.3.使用XML实现@Mapper接口的方法
5.4.验收环节(测试所写代码是否有效)
5.4.1.使用常规的请求方法(创建controller和service层,补全层级结构,让UserController调用UserService,UserService调用UserMapper,再把结果层层返回)
--->PS:关于Idea专业版UserService类中@Autowired报错但不影响执行的问题的原因分析:(社区版功能简陋,不会报错)
--->PS:MyBatis在整个框架中的定位/框架交互流程图:
5.4.2.使用Spring Boot单元测试
6.使用MyBatis模式操作数据库(添加业务代码)实现CRUD中的增/删/改操作
6.1.添加功能实现
6.1.1.传2个参数,返回1个受影响的行数
6.1.2.传1个对象,返回1个添加的用户的id(自增id)
6.2.修改功能实现
6.3.删除功能实现
7.更复杂的查询操作
7.1.单表查询
7.1.1.参数占位符 #{} 和 ${}
7.1.2.${} 优点
7.1.3.SQL注入问题
7.1.4.like查询
--->PS:当报错信息不明确时,可以在配置文件application.xml中进行配置,打印MyBatis最终执行的SQL,这样有助于排查问题。
7.2.多表查询
7.2.1.返回类型/结果对象:resultType
7.2.2.返回字典映射:resultMap
7.2.3.多表查询之一对一:一篇文章一个用户
7.2.4.多表查询之一对多:一个用户多篇文章
8.复杂情况:动态SQL使用
8.1.if标签
8.2.trim标签
8.3.where标签
8.4.set标签
8.5.foreach标签
前言:前面已经学习了Spring,Spring Boot,Spring MVC这3个框架,接下来学习第4个框架MyBatis(国内用的多):将前端传递的数据存储起来(前身IBatis)或者查询数据库里面的数据。
MyBatis是一款优秀的持久层框架(ORM框架),它支持自定义SQL,存储过程 (以程序里写方法的方式来写SQL,问题:无法一步步调试。企业里几乎不用) 以及高级映射。MyBatis去除了几乎所有的JDBC代码以及设置参数和获取结果集的工作。MyBatis可以通过简单的XML或注解来配置和映射原始类型,接口和JavaPOJO (Plain Old Java Objects,普通老式Java对象) 为数据库中的记录。
简单来说MyBatis是更简单完成程序和数据库交互的工具,也是更简单的操作和读取数据库工具/框架。
优点:支持中文,灵活度高;缺点:跨数据库方面很差。
MyBatis官网https://mybatis.org/mybatis-3/zh/index.htmlorg -> 开源框架域名(非盈利性的公益组织)
MyBatis支持的操作方式:
①3.1版本之前:支持XML的操作方式(主流90%企业在用)重点是写SQL自由度灵活度大;
②3.1版本之后:添加了使用注解实现数据库的操作方式(企业里几乎不用)实现特殊业务时会非常复杂。
PS:不同版本号区别
- 3.5.1 -> 3.5的第一个版本
- 3.5.10 -> 3.5的第十个版本
对于后端开发来说,程序是由①后端程序②数据库这两部分组成的。
而这两部分要通讯,就要依赖数据库连接工具(有JDBC,MyBatis)。
JDBC操作流程:
JDBC 操作示例回顾:通过 JDBC 的 API 向数据库中添加⼀条记录,修改⼀条记录,查询⼀条记录:
-- 创建数据库
create database if not exists `library` default character set utf8mb4;
-- 使⽤数据库
use library;
-- 创建表
create table if not exists `soft_bookrack` (
`book_name` varchar(32) NOT NULL,
`book_author` varchar(32) NOT NULL,
`book_isbn` varchar(32) NOT NULL primary key
) ;
import lombok.Data;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class SimpleJdbcOperation {
private final DataSource dataSource;
public SimpleJdbcOperation(DataSource dataSource) {
this.dataSource = dataSource;
}
/**
* 添加⼀本书
*/
public void addBook() {
Connection connection = null;
PreparedStatement stmt = null;
try {
//获取数据库连接
connection = dataSource.getConnection();
//创建语句
stmt = connection.prepareStatement("insert into soft_bookrack (book_name, book_author, book_isbn) values (?,?,?);");
//参数绑定
stmt.setString(1, "Spring in Action");
stmt.setString(2, "Craig Walls");
stmt.setString(3, "9787115417305");
//执⾏语句
stmt.execute();
} catch (SQLException e) {
//处理异常信息
} finally {
//清理资源
try {
if (stmt != null) {
stmt.close();
}
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
//
}
}
}
/**
* 更新⼀本书
*/
public void updateBook() {
Connection connection = null;
PreparedStatement stmt = null;
try {
//获取数据库连接
connection = dataSource.getConnection();
//创建语句
stmt = connection.prepareStatement("update soft_bookrack set book_author=? where book_isbn=?;");
//参数绑定
stmt.setString(1, "张卫滨");
stmt.setString(2, "9787115417305");
//执⾏语句
stmt.execute();
} catch (SQLException e) {
//处理异常信息
} finally {
//清理资源
try {
if (stmt != null) {
stmt.close();
}
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
//
}
}
}
/**
* 查询⼀本书
*/
public void queryBook() {
Connection connection = null;
PreparedStatement stmt = null;
ResultSet rs = null;
Book book = null;
try {
//获取数据库连接
connection = dataSource.getConnection();
//创建语句
stmt = connection.prepareStatement("select book_name, book_author, book_isbn from soft_bookrack where book_isbn =?");
//参数绑定
stmt.setString(1, "9787115417305");
//执⾏语句
rs = stmt.executeQuery();
if (rs.next()) {
book = new Book();
book.setName(rs.getString("book_name"));
book.setAuthor(rs.getString("book_author"));
book.setIsbn(rs.getString("book_isbn"));
}
System.out.println(book);
} catch (SQLException e) {
//处理异常信息
} finally {
//清理资源
try {
if (rs != null) {
rs.close();
}
if (stmt != null) {
stmt.close();
}
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
//
}
}
}
@Data
public static class Book {
private String name;
private String author;
private String isbn;
}
}
从上述代码和操作流程可以看出JDBC操作⾮常繁琐,不但要拼接每⼀个参数,⽽且还要按照模板代码的⽅式,⼀步步操作数据库,并且在每次操作完,还要⼿动关闭连接等,⽽所有的这些操作步骤都需要在每个⽅法中重复书写。
而MyBatis可以帮助我们更简单,更⽅便,更快速地操作数据库。
MyBatis 学习分为两部分:
-- 创建数据库
drop database if exists mycnblog;
create database mycnblog default character set utf8mb4;
-- 使用数据数据
use mycnblog;
-- 创建用户表
drop table if exists userinfo;
create table userinfo(
id int primary key auto_increment,
username varchar(100) not null,
password varchar(32) not null,
photo varchar(500) default '',
createtime datetime default now(),
updatetime datetime default now(),
`state` int default 1 -- 预留字段,后面可以做账号冻结
) default charset 'utf8mb4';
-- 创建文章表
drop table if exists articleinfo;
create table articleinfo(
id int primary key auto_increment,
title varchar(100) not null,
content text not null,
createtime datetime default now(),
updatetime datetime default now(),
uid int not null,
rcount int not null default 1,
`state` int default 1
) default charset 'utf8mb4';
-- 创建视频表
drop table if exists videoinfo;
create table videoinfo(
vid int primary key,
`title` varchar(250),
`url` varchar(1000),
createtime datetime default now(),
updatetime datetime default now(),
uid int
) default charset 'utf8mb4';
-- 添加一个用户信息
insert into `mycnblog`.`userinfo` (`id`, `username`, `password`, `photo`, `createtime`, `updatetime`, `state`)
values (1, 'admin', 'admin', '', '2021-12-06 17:10:48', '2021-12-06 17:10:48', 1);
-- 文章添加测试数据
insert into articleinfo(title,content,uid)
values ('Java','Java正文',1);
-- 添加视频
insert into videoinfo(vid,title,url,uid)
values (1,'javatitle','http://www.baidu.com',1);
删除4个无效文件后就是ssm项目了。
注:针对Idea社区版,此时因为没有配置连接的数据库服务器地址,所以如果启动项目就会报错。而专业版会帮助我们自动生成关于数据库的配置信息。
使用yml写法更简单:
# 配置数据库的连接字符串
spring:
datasource:
url: jdbc:mysql://127.0.0.1/mycnblog?characterEncoding=utf8mb4 #utf8不支持一些复杂的中文
username: root
password: 12345678
driver-class-name: com.mysql.cj.jdbc.Driver #底层驱动的名称(8.0之前版本不加.cj,8.0之后版本加.cj)
MyBatis的XML中保存的是查询数据库的具体操作SQL。
# 配置mybatis xml的文件路径,在resource包下建立mybatis(其命名自定义)包,在resource/mybatis创建所有表的xml文件
mybatis:
mapper-locations: classpath:mybatis/**Mapper.xml
PS:MyBatis模式
使用MyBatis的XML的操作方式:@Mapper文件(定义方法,没有方法实现)+XML会实现@Mapper的方法体。
其每一个功能都必须要去操作2部分:
- Interface(方法定义):就是普通的接口,会在这个接口里面定义需要实现的方法(有方法名称和传递的参数,没有方法体),需要通过一个注解@Mapper来将Inteface中的方法映射到一个XML文件中。(当然在xml文件中也需要表明是哪个方法的映射)
【@Mapper注解:来自ibatis(它是mybatis的前身(改名了),二者是一个东西)。
数据持久层是一个层级,底下有很多种实现,@Repository(仓库)只是其中的一种实现;而现在的实现是MyBatis(实现程序和数据库的一个映射,将数据库的某一张表对应到程序中的某一个类中,进而实现增删改查),它是通过@Mapper(映射)来实现数据持久层的】
【此时就学了7个类注解了:@Controller,@Service,@Repository,@Component,
@Configuration,@RestController,@Mapper。
它们都能把当前的类在Spring Boot项目启动时直接进行初始化/实例化】
- XML(方法实现):是Interface实现的一个子类,具体的操作SQL就是在XML中实现的。(为什么要使用一个xml来实现一个interface?因为在MyBatis里面写的SQL放在类里只能是String类型,看起来很别扭;而放在xml里看起来会协调很多)在xml里,只需配置要实现的Interface是谁,要实现的SQL是啥。
- mybatis会将Interface和XML里的内容合在一起,生成一个实例对象,其方法是含方法体的完善的方法。最终生成要执行的SQL。
- 通过这个SQL语句,去操作数据库。将操作数据库得到的结果直接返回给Interface中方法的返回值,再返回给服务层;服务层,再返回给控制层;控制层,再把结果交给用户。这样就完成了一次交互。
之所以可以这样,是因为MyBatis是一个ORM框架,是一个对象和数据库映射的框架。
查询单条数据:
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
* 实体类
*/
@Setter
@Getter //这两个注解可以用@Data一个注解代替
@ToString //这样打印对象的时候打印的是对象里的属性和值而不是对象的地址
public class UserInfo {
//实体类里的字段和数据库创建的字段一定要保证一致。一些特殊情况下也可以不一致,而mybatis也是可以处理这种情况的。
private int id;
private String username;
private String password;
private String photo;
private String createtime;
private String updatetime;
private int state;
}
import com.example.demo.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper {
//查询方法定义完成
public UserInfo getUserById(Integer id);
}
数据持久层的实现mybatis的固定xml格式的配置文件(不用记,保存起来即可):
--->PS:关于Idea专业版UserService类中@Autowired报错但不影响执行的问题的原因分析:(社区版功能简陋,不会报错)
专业版:
社区版:
注:在UserController类中,使用@Autowired和@Resource都不会报错。
- 因为在UserService类中调用的是UserMapper接口,它使用的是@Mapper注解;而UserController类中调用的是UserService类,它使用的是@Service注解。
- @Autowired和@Service都是Spring框架的注解,能互相认识;而@Mapper是MyBatis框架的注解,Spring框架的@Autowired不能识别,所以会报错。
- 不同的框架互相不认!
- @Resource是jdk注解,jdk是官方的,对这些知名的框架是兼容的,海纳百川~
①添加Service服务层代码
import com.example.demo.mapper.UserMapper;
import com.example.demo.model.UserInfo;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class UserService {
@Resource //①可以通过注解的方式注入一个接口
private UserMapper userMapper;
/**
* 根据id查询用户对象
*
* @param id
* @return
*/
public UserInfo getUserInfoById(Integer id) {
return userMapper.getUserById(id); //②直接调用接口中的方法声明而没有方法体也可以成功返回结果
}
}
②添加Controller控制层代码
import com.example.demo.model.UserInfo;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/getuserbyid")
public UserInfo getUserById(Integer id) {
if (id != null && id > 0) { //前端传递的参数有效
return userService.getUserInfoById(id);
} else {
return new UserInfo(); //这里不返回null,就是表示对象可以拿到,但里面的属性没有赋值,主要是为了前端好处理
}
}
}
# 配置数据库的连接字符串
spring:
datasource:
url: jdbc:mysql://127.0.0.1/mycnblog?characterEncoding=utf8
username: root
password: 12345678
driver-class-name: com.mysql.cj.jdbc.Driver #底层驱动的名称(8.0之前版本不加.cj,8.0之后版本加.cj)
成功实现了ssm项目的互联互通!
--->PS:MyBatis在整个框架中的定位/框架交互流程图:
MyBatis 也是⼀个 ORM 框架,ORM(Object Relational Mapping),即对象关系映射。在⾯向对象编程语⾔中,将关系型数据库中的数据与对象建⽴起映射关系,进⽽⾃动的完成数据与对象的互相转换:
将输⼊数据(即传⼊对象)+ SQL映射成原生SQL
将结果集映射为返回对象,即输出对象
ORM 把数据库映射为对象:
数据库表(table)--> 类(class)
记录(record,⾏数据)--> 对象(object)
字段(field) --> 对象的属性(attribute)
⼀般的 ORM 框架,会将数据库模型的每张表都映射为⼀个 Java 类。
也就是说使⽤ MyBatis 可以像操作对象⼀样来操作数据库中的表,可以实现对象和数据库表之间的转换。
之前的连接是单连接,每次执行完会关闭;而现在使用的是连接池(就像线程池)。
①什么是单元测试?
单元测试(unit testing),是指对软件(项目)中的最小可测试单元进行检查和验证的过程。
【对于 Spring Boot 来说,最小可测试单元就是方法,每一个方法都代表一个相应的功能。方法不可再被分割,不能说去测试方法中的某一段程序,或者某一个属性,这叫调试。
方法中可能会调用另一个方法来“辅助”自身的运行。如果方法中还有方法,单元测试就需要分为多个了。首先是对这个方法中所有依赖的方法做一个单元测试,再对本身的方法进行单元测试。】
单元测试是开发者编写的一小段代码,⽤于检验被测代码的⼀个很⼩的、很明确的(代码)功能是否正确。执⾏单元测试就是为了证明某段代码的执⾏结果是否符合我们的预期。如果测试结果符合我们的预期,称之为测试通过,否则就是测试未通过(或者叫测试失败)。
②单元测试有哪些好处?
- 单元测试不用启动tomcat。
- 如果中途改动了代码,在项目打包时会发现错误,因为打包时会自动执行单元测试,单元测试错误就会发现。
- 能够非常及时地得到反馈,判断功能是否正确。
- 在Spring Boot的单元测试里支持既能进行功能的测试,还不把测试数据(无效数据)保存到数据库里,不会“污染”数据库。单元测试的类或方法上加上@Transactional注解,表示当前测试的数据不会影响数据库,可以重复无限执行。
【MySQL 默认是处于开启事务的状态,也就说开启了事务的自动提交,然后开始执行单元测试,对数据库中的数据进行操作。在验证完功能有没有问题之后,就会进行事务的回滚操作,还原到还没有测试的情况,将数据还原到初始状态。】③Spring Boot单元测试使用
Spring Boot项目创建时会默认自动帮我们生成单元测试框架spring-boot-test,它主要依赖另一个著名的测试框架JUnit实现的。
org.springframework.boot spring-boot-starter-test test scope作用域test表示测试框架只在单元测试阶段起作用,在package之后的生命周期会将这个包剔除掉。
接下来进行具体的单元测试实现步骤:
1).生成单元测试类和方法:
在当前需要测试的类里面右键:
2).添加单元测试代码:
①在类上添加Spring Boot框架测试注解@SpringBootTest,标识当前测试的类的运行环境为spring boot。
②添加单元测试业务代码。
简单的断言说明:
使用断言能判断最终结果是否符合预期,一般建议去写断言,因为写断言之后,打包时会走这个断言,如果断言失败了,打包就会失败,能判断这个方法到底是成功还是失败。
也可以不执行断言,将结果直接打印出来,会少了对结果的约束。然后运行方法,看打印结果是否符合预期。这样会打印出结果,但至于结果对不对,需要人为参与判断。而断言不需要人为参与,断言会去判断。
JUnit提供Assertions类,Assertions类提供以下方法。
不使用断言:
import com.example.demo.model.UserInfo;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
//1.标识当前测试的类的运行环境为spring boot
@SpringBootTest
class UserMapperTest {
//2.补充测试的业务代码
@Resource
private UserMapper userMapper;
@Test //每一个添加@Test注解的方法都可以单独执行的
void getUserById() {
//测试的具体业务
UserInfo userInfo = userMapper.getUserById(1);
//也可以不执行断言,将结果直接打印出来
System.out.println(userInfo);
}
}
使用断言:
import com.example.demo.model.UserInfo;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
//1.标识当前测试的类的运行环境为spring boot
@SpringBootTest
class UserMapperTest {
//2.补充测试的业务代码
@Resource
private UserMapper userMapper;
@Test //每一个添加@Test注解的方法都可以单独执行的
void getUserById() {
//测试的具体业务
UserInfo userInfo = userMapper.getUserById(1);
//使用断言:如果结果不等于null表示是成功的
Assertions.assertNotNull(userInfo);
}
}
尝试在Maven里打包:
单元测试有时会存在误报(自己可以确定自己写的方法正确)或不需要时,那么在打包时可以关闭单元测试,这样打包速度会非常快。
一个测试类中可以有多个单元测试方法,在每一个单元方法上添加@Test注解(相当于main()启动方法,一个类中只能有一个main()方法),就可以单独运行。
import com.example.demo.model.UserInfo;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
//1.标识当前测试的类的运行环境为spring boot
@SpringBootTest
class UserMapperTest {
//2.补充测试的业务代码
@Resource
private UserMapper userMapper;
@Test //每一个添加@Test注解的方法都可以单独执行的
void getUserById() {
//测试的具体业务
UserInfo userInfo = userMapper.getUserById(1);
//使用断言:如果结果不等于null表示是成功的
Assertions.assertNotNull(userInfo);
}
@Test
void save() {
System.out.println("你好,单元测试。");
}
}
运行save():
查询多条数据:
在UserMapper接口中添加方法声明:
import com.example.demo.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface UserMapper {
//查询方法定义完成
public UserInfo getUserById(Integer id);
//查询多条数据
public List getAll();
}
在UserMapper.xml中添加select标签(一个Interface对应创建一个xml文件即可):
返回的类型是List集合,但最终返回的还是UserInfo对象。
SQL在xml中(不需要在句末加";")不会报错,启动程序也不会报错。所以先去将写好的SQL在MySQL数据库中运行(需要在句末加";"),没问题了再复制回来。
再向数据库中添加一条数据:
生成第2个单元测试方法:
import com.example.demo.model.UserInfo;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.List;
//1.标识当前测试的类的运行环境为spring boot
@SpringBootTest
class UserMapperTest {
//2.补充测试的业务代码
@Resource
private UserMapper userMapper;
@Test //每一个添加@Test注解的方法都可以单独执行的
void getUserById() {
//测试的具体业务
UserInfo userInfo = userMapper.getUserById(1);
//使用断言:如果结果不等于null表示是成功的
Assertions.assertNotNull(userInfo);
}
@Test
void save() {
System.out.println("你好,单元测试。");
}
@Test
void getAll() {
List list = userMapper.getAll();
System.out.println(list); //条数是动态改变的,这里不写断言
}
}
或者将controller,service,mapper对应类中所有的代码补充完,单元测试可以测试UserController类。
对应MyBatis的标签:
import com.example.demo.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface UserMapper {
//查询方法定义完成
public UserInfo getUserById(Integer id);
//查询多条数据
public List getAll();
//添加用户,默认情况返回一个受影响的行数
//save配合find使用;add配合get使用
public int add(String username, String password);
}
insert into userinfo(username,password)
values(#{name},#{password})
import com.example.demo.model.UserInfo;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.List;
//1.标识当前测试的类的运行环境为spring boot
@SpringBootTest
class UserMapperTest {
//2.补充测试的业务代码
@Resource
private UserMapper userMapper;
@Test //每一个添加@Test注解的方法都可以单独执行的
void getUserById() {
//测试的具体业务
UserInfo userInfo = userMapper.getUserById(1);
//使用断言:如果结果不等于null表示是成功的
Assertions.assertNotNull(userInfo);
}
@Test
void save() {
System.out.println("你好,单元测试。");
}
@Test
void getAll() {
List list = userMapper.getAll();
System.out.println(list); //条数是动态改变的,这里不写断言
}
@Test
void add() {
int result = userMapper.add("李四", "123");
Assertions.assertEquals(1, result);
}
}
大多数时候数据库里的字段名和方法中对象的属性名还是保持一致好,会减少不必要的麻烦。
主要的区别在xml中:
useGeneratedKeys:这会令 MyBatis 使⽤ JDBC 的 getGeneratedKeys ⽅法来取出由数据库内部⽣成的主键(⽐如:像 MySQL 和 SQL Server 这样的关系型数据库管理系统的⾃动递增字段),默认值:false。
keyColumn:设置⽣成键值在表中的列名,在某些数据库(像 PostgreSQL)中,当主键列不是表中的第⼀列的时候,是必须设置的。如果⽣成列不⽌⼀个,可以⽤逗号分隔多个属性名称。
keyProperty:指定能够唯⼀识别对象的属性,MyBatis 会使⽤ getGeneratedKeys 的返回值或 insert 语句的 selectKey ⼦元素设置它的值,默认值:未设置(unset)。如果⽣成列不⽌⼀个,可以⽤逗号分隔多个属性名称。
//添加用户2,返回自增id,id不能当作返回值返回回去,但可以把id赋值到对象里的某一个属性里
public void add2(UserInfo userInfo); //此处返回值用int(返回一个受影响的行数)也行,但没啥用而且容易出现歧义
insert into userinfo(username,password)
values(#{username},#{password})
@Test
void add2() {
UserInfo userInfo = new UserInfo();
userInfo.setUsername("老6");
userInfo.setPassword("123");
userMapper.add2(userInfo); //此行代码执行完没有任何返回值为void
System.out.println("自增id:" + userInfo.getId()); //没设置之前id是等于0或null的
}
在UserMapper接口中添加方法声明:
//修改操作,根据id修改username
//方法返回值int默认也是返回一个受影响的行数
//方法命名update随便命
//方法传参随便传
public int update(@Param("id") int id, @Param("username") String username);
在UserMapper.xml中添加对应
update userinfo set username=#{username} where id=#{id}
在UserMapperTest中添加测试方法:
@Test
void update() {
int result = userMapper.update(5, "老五");
Assertions.assertEquals(1, result);
}
//删除方法,返回受影响的行数
public int del(@Param("id") int id);
delete from userinfo where id=#{id}
@Transactional //单元测试的类或方法上加上此注解,表示当前测试的数据不会影响数据库
@Test
void del() {
int result = userMapper.del(5);
Assertions.assertEquals(1, result);
}
二者在UserMapper.xml中执行效果一样。
①#{}:预编译处理。MyBatis在处理#{}时,会将SQL中的#{}替换为?号,使用PreparedStatement的set方法来赋值。
String sql = "select * from userinfo where id=?";
statement.setInt(1, id); //设置占位符
会认为是一个参数,而不是一个SQL语句。?里即使有不安全的SQL脚本也执行不了。调用有参的mysql方法。
没有安全问题/漏洞。
②${}:字符直接替换。MyBatis在处理${}时,就是把${}替换成变量的值。
String sql = "select * from userinfo where id="+id;
是进行拼接的,会认为拼接后的整体就是一个SQL语句,所以当id传的不是一个参数值,而是一个危险的SQL脚本时,会原样执行拼接好的SQL。调用无参的mysql方法。
存在安全漏洞。
在UserMapper中写方法声明://根据名称查询用户对象(非模糊查询,全字查询) public UserInfo getUserByName(@Param("username") String username);
在UserMapper.xml中写对应
在UserMapperTest中写测试方法:
二者区别(头等舱和经济舱乘机分离的故事):
- 在坐⻜机的时候头等舱和经济舱的区别是很⼤的,⼀般航空公司乘机都是头等舱和经济舱分离的,头等舱的⼈先登机,登机完之后,封闭头等舱,然后再让经济舱的乘客登机,这样的好处是可以避免经济舱的⼈混到头等舱的情况。这就相当于预处理,可以解决程序中不安全(越权处理)的问题。
- ⽽直接替换的情况相当于,头等舱和经济舱不分离的情况,这样经济舱的乘客在通过安检之后可能越权摸到头等舱。这就相当于参数直接替换,它的问题是可能会带来越权查询和操作数据等问题,⽐如 SQL 注⼊问题。
//根据时间排序查询所有的用户
public List getAllOrderByCreateTime(@Param("order") String order); //不怕方法名字长,就怕方法语义不明确
@Test
void getAllOrderByCreateTime() {
List list = userMapper.getAllOrderByCreateTime("desc");
System.out.println(list);
}
使用${}
${}使用场景:当传递的参数是一个SQL语句(而非一个某个参数的值)(的一部分)时,只能使用${}的形式。比如传递排序的desc或asc时,它是SQL语句的一部分,而非某一个参数的值,只能用${},用#{}会报错。
//登录方法
public UserInfo login(@Param("username") String username, @Param("password") String password);
@Test
void login() {
String username = "admin";
String password = "admin";
UserInfo userInfo = userMapper.login(username, password);
System.out.println(userInfo);
}
若输入错误的密码:
@Test
void login() {
String username = "xxx";
String password = "' or 1='1"; //SQL注入
UserInfo userInfo = userMapper.login(username, password);
System.out.println(userInfo);
}
不仅可以在用户名和密码都乱输入的情况下查询到用户所有信息,还可以注入SQL语句删库!非常危险!
原因:
select * from userinfo where username='${username}' and password='${password}'
直接替换为:
select * from userinfo where username='xxx' and password='' or 1='1'
select * from userinfo where username='xxx' and password='' or 1='1'
解决:将${}换成#{}。
结论:用于查询的字段,尽量使用#{}预查询的方式;若非要使用${}业务要传SQL脚本,一定要对参数进行效验,将要传的值规定为固定的某一/几个值,效验通过执行,否则不执行。
①使用#{}拼接:执行报错,不可使用。
//根据名称进行模糊查询
public List getUserByLikeName(@Param("username") String username);
@Test
void getUserByLikeName() {
String username = "admin";
List list = userMapper.getUserByLikeName(username);
System.out.println(list);
}
--->PS:当报错信息不明确时,可以在配置文件application.xml中进行配置,打印MyBatis最终执行的SQL,这样有助于排查问题。
# 配置mybatis xml的文件路径,在resource包下建立mybatis(其命名自定义)包,在resource/mybatis创建所有表的xml文件 mybatis: mapper-locations: classpath:mybatis/**Mapper.xml configuration: # 配置打印MyBatis最终执行的SQL log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 配置打印MyBatis最终执行的SQL logging: level: com: example: demo: debug
再去执行查看报错:
虽然看起来没问题,但有SQL注入风险;而且需要进行业务效验处理(前提是结果可以穷举,否则无法做SQL注入的判定)。username无法穷举,不能使用。
③使用MySQL提供的内置函数concat()将结果集进行拼接操作。
多表查询分为:一对一,一对多,多对多。但多对多的业务更加复杂,牵扯到3张表,查询时较麻烦,需要进行数据的组装。此处只考虑一对一和一对多。
如果是增,删,改返回受影响的行数,在mapper.xml中可以不设置返回类型;然而查询必须要设置返回的类型,否则会报错。
- id属性:用于标识实现接口中的那个方法。
- 结果映射属性:结果映射有2种实现标签:
和
绝大多数查询场景可以使用resultType进行返回,其优点是使用方便,直接定义到某个实体类即可。
resultMap使用场景:
resultType能实现的功能resultMap都能实现;而resultMap能实现的功能resultType不能实现。
字段名和属性名不同的情况:
创建文章实体类ArticleInfo:
import lombok.Data;
/**
* 文章实体类
*/
@Data
public class ArticleInfo {
private int id;
private String name;
private String content;
private String createtime;
private String updatetime;
private int uid;
private int rcount;
private int state;
}
name属性与title字段不一样。
创建ArticleMapper接口里定义方法声明:
import com.example.demo.model.ArticleInfo;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface ArticleMapper {
public List getAll();
}
创建ArticleMapper.xml里面写对应标签:
创建测试类ArticleMapperTest:
import com.example.demo.model.ArticleInfo;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.List;
@SpringBootTest
class ArticleMapperTest {
@Resource
private ArticleMapper articleMapper;
@Test
void getAll() {
List list = articleMapper.getAll();
System.out.println(list);
}
}
解决:将resultType改为resultMap。
只配置不同的项(title和name),不配置其他项的写法:单表查询没问题,但在多表查询中会有问题:
PS:
import lombok.Data;
/**
* 文章实体类
*/
@Data
public class ArticleInfo {
private int id;
private String name;
private String content;
private String createtime;
private String updatetime;
private int uid;
private int rcount;
private int state;
//所属用户
private UserInfo userInfo;
}
import com.example.demo.model.ArticleInfo;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface ArticleMapper {
//查询articleinfo单张表的信息
public List getAll();
//查询articleinfo + userinfo多表的信息
public List getAll2();
}
import com.example.demo.model.ArticleInfo;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.List;
@SpringBootTest
class ArticleMapperTest {
@Resource
private ArticleMapper articleMapper;
@Test
void getAll() {
List list = articleMapper.getAll();
System.out.println(list);
}
@Test
void getAll2() {
List list = articleMapper.getAll2();
System.out.println(list);
}
}
解决:一对一映射要使用
只配置不同的项(title和name),不配置其他项的写法:单表查询没问题,但在多表查询中会有问题:
在UserMapper.xml中添加:
若不写username:
在ArticleMapper.xml中写:
在UserMapper.xml中写上username:
所以多表查询中所有项都要写完整,缺一不可。补上:
在ArticleMapper.xml中需要写:
这样就看起来没问题了:
但是如果进行改动:
没问题。but:
解决:在ArticleMapper.xml中修改两处内容,即最终代码实现:
在UserInfo中添加字段:
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.util.List;
/**
* 实体类
*/
@Setter
@Getter //这两个注解可以用@Data一个注解代替
@ToString //这样打印对象的时候打印的是对象里的属性和值而不是对象的地址
public class UserInfo {
//实体类里的字段和数据库创建的字段一定要保证一致。一些特殊情况下也可以不一致,而mybatis也是可以处理这种情况的。
private int id;
private String username;
private String password;
private String photo;
private String createtime;
private String updatetime;
private int state;
//多对多添加的字段
List alist;
}
先完善数据库里的数据:
在UserMapper接口中添加方法声明:
//查询所有数据(联表查询userinfo & articleinfo)
public List getAll2();
在UserMapper.xml中添加
一对多需要使用
其中property="alist"表示当前集合针对的属性。
在UserMapperTest测试类中添加测试方法:
@Test
void getAll2() {
List list = userMapper.getAll2();
System.out.println(list);
}
动态sql是MyBatis的强大特性之一,能够完成不同条件下不同sql的拼接,都是去解决非必传项的问题的。下面讲的几种标签解决了CRUD(增删查改)场景。
可参考官方文档:MyBatis动态sqlhttps://mybatis.org/mybatis-3/zh/dynamic-sql.html
场景:做插入/添加数据操作。
那么如果在添加用户时有不确定的字段传入,就需要使用动态标签
【不传的按理应该不设置(性能更好)(会走默认约束:若设置了默认的值,会放默认的值;若没设置默认的值,会放null,null不是表示这个值为null,而是表示目前这个字段没有任何东西。)
而不是设置为空字符串(表示这个字段有值,只不过这个值是空。)
二者意义完全不同】
在UserMapper接口中添加方法声明:
//添加用户3(使用动态sql if)
public int add3(UserInfo userInfo);
在UserMapper.xml中写动态标签:
insert into userinfo(username,
photo,
password
) values (#{username},
#{photo},
#{password})
注:在test中的photo是传入对象中的属性,不是数据库中的字段。
在UserMapperTest中添加测试方法:
先不传图片:(没有非必传项)
@Test
void add3() {
UserInfo userInfo = new UserInfo();
userInfo.setUsername("张三");
userInfo.setPassword("123");
//userInfo.setPhoto(null); //这样写不写执行结果都一样
int result = userMapper.add3(userInfo);
System.out.println("受影响的行数:" + result);
}
传图片:(有非必传项)
@Test
void add3() {
UserInfo userInfo = new UserInfo();
userInfo.setUsername("李四");
userInfo.setPassword("456");
userInfo.setPhoto("lisi.png");
int result = userMapper.add3(userInfo);
System.out.println("受影响的行数:" + result);
}
场景:做插入/添加数据操作。
之前在JQuery中也有使用:清空多余的空白字符。
刚刚的插⼊功能,只是有⼀个 photo 字段可能是选填项,如果所有/多个字段都是非必填项,就考虑使⽤
//添加用户4(使用动态sql trim + if)
public int add4(UserInfo userInfo); //假设所有字段都是可选项
insert into userinfo
username,
password,
photo
values
#{username},
#{password},
#{photo}
@Test
void add4() {
UserInfo userInfo = new UserInfo();
userInfo.setUsername("王五");
userInfo.setPassword("456");
int result = userMapper.add4(userInfo);
}
场景:做where条件查询。会生成where关键字。
情景1/亮点1:当
//根据名称或密码查询用户列表(使用动态sql where)
public List getListByNameOrPwd(String username, String password);
@Test
void getListByNameOrPwd() {
List list = userMapper.getListByNameOrPwd(null, null);
System.out.println(list);
}
情景2:传递前面的一个参数
@Test
void getListByNameOrPwd() {
List list = userMapper.getListByNameOrPwd("admin", null);
System.out.println(list);
}
改动写法:
情景3:传递后一个参数:
@Test
void getListByNameOrPwd() {
List list = userMapper.getListByNameOrPwd(null, "123");
System.out.println(list);
}
亮点2:
以上
@Test
void getListByNameOrPwd() {
List list = userMapper.getListByNameOrPwd(null, null);
System.out.println(list);
}
场景:做更新/修改数据。使用
public int updateById(UserInfo userInfo);
update userinfo
username=#{username},
password=#{password},
photo=#{photo}
where id=#{id}
@Test
void updateById() {
UserInfo userInfo = new UserInfo();
userInfo.setId(9);
userInfo.setUsername("老五");
int result = userMapper.updateById(userInfo);
System.out.println("受影响行数:" + result);
}
以上
update userinfo
username=#{username},
password=#{password},
photo=#{photo}
where id=#{id}
场景:做删除数据。
对集合进⾏遍历时可以使⽤该标签。
public int delByIds(List ids);
delete from userinfo where id in
#{id}
@Test
void delByIds() {
List list = new ArrayList<>();
list.add(6);
list.add(8);
int result = userMapper.delByIds(list);
System.out.println("受影响行数:" + result);
}