接上次博客:JavaEE进阶(7)Spring Boot 日志(概述、用途、使用:打印日志,框架介绍,SLF4J 框架介绍、更简单的日志输出)-CSDN博客
目录
JDBC 操作示例回顾
-
创建数据库连接池 DataSource: 在使用 JDBC 之前,首先需要创建一个数据库连接池,这可以通过一些数据库连接池实现类来完成,比如 Apache 的 DBCP(DataBase Connection Pooling)或者 HikariCP 等。这个连接池用于管理数据库连接,可以有效地减少连接数据库的开销。
-
通过 DataSource 获取数据库连接 Connection: 一旦有了连接池,就可以从连接池中获取数据库连接。这里的 Connection 对象代表了一个与数据库的连接,通过它可以执行 SQL 语句并处理结果。
-
编写要执行带 ? 占位符的 SQL 语句: 在使用 JDBC 时,通常会使用占位符(?)来代替实际的参数值,这样可以提高 SQL 语句的安全性,并且可以避免 SQL 注入攻击。
-
通过 Connection 及 SQL 创建操作命令对象 Statement: 一旦有了连接并编写了 SQL 语句,接下来就可以创建 Statement 对象,它用于执行 SQL 语句。
-
替换占位符: 如果 SQL 语句中有占位符,需要使用 PreparedStatement 对象来替换这些占位符,指定要替换的数据库字段类型,占位符索引及要替换的值。
-
使用 Statement 执行 SQL 语句: 接下来就是执行 SQL 语句,这可以通过 Statement 对象的 executeQuery()、executeUpdate() 或者 execute() 方法来完成。executeQuery() 用于执行查询操作,返回一个结果集 ResultSet;executeUpdate() 用于执行更新操作,返回更新的数量。
-
处理结果集: 如果执行的是查询操作,就会得到一个 ResultSet 对象(一一映射好的),通过这个结果集可以获取查询到的数据,并对数据进行处理。
-
释放资源: 最后,在不再需要连接和其他资源时,务必要释放它们,以便及时释放数据库连接和其他资源,并且释放资源的顺序应该是逆序释放,即先释放 ResultSet,然后释放 Statement,最后释放 Connection。
从上述代码和操作流程可以看出,对于 JDBC 来说,整个操作流程的繁琐程度不言而喻。我们不但需要拼接每一个参数,还需要按照模板代码的方式,逐步操作数据库。更为繁琐的是,每次操作完成后,都要手动关闭连接等资源,这些重复的操作步骤在每个方法中都需要重复书写。这种重复劳动不仅影响了开发效率,也容易引入错误。因此,我们需要一种更简单、更方便的方式来操作数据库,以提高开发效率并减少出错的可能性。
仔细观察,上述步骤中除了第3条和第5条,剩下的其实都有规律可循,MyBatis 就是将 JDBC 中繁琐的步骤进行了封装和简化,提供了更便捷、灵活和高效的数据库访问方式,使开发者可以更专注于业务逻辑的实现,而不必过多关注底层的数据库操作细节。
什么是MyBatis?
官网:mybatis – MyBatis 3 | Introduction
MyBatis是一款优秀的持久层框架,旨在简化JDBC的开发流程。最初作为Apache的一个开源项目iBatis,后来于2010年迁移到Google Code,并更名为MyBatis。2013年11月,MyBatis进一步迁移到了GitHub上进行维护和开发。
MyBatis的主要目标是提供一种更简单、更直观的方式来与数据库进行交互,避免了传统JDBC编程中繁琐的手动管理连接、拼接SQL语句等操作。通过使用MyBatis,开发人员可以通过XML或注解来定义SQL映射关系,实现了SQL与Java代码的分离,同时提供了丰富的功能和灵活的扩展性,使得数据库操作更加简单、高效和可维护。
MyBatis是一个Java持久层框架,它的主要目的是简化与数据库的交互过程。在传统的JDBC编程中,开发人员需要手动管理数据库连接、编写SQL语句、处理结果集等,这些操作繁琐且容易出错。MyBatis的出现改变了这一现状,它提供了一种更加简洁、高效的方式来进行数据库操作。
-
SQL映射配置: MyBatis允许开发人员使用XML文件或者注解来定义SQL映射关系,将SQL语句与Java方法进行映射。这样的设计使得SQL与Java代码分离,提高了代码的可维护性和可读性。
-
动态SQL支持: MyBatis提供了强大的动态SQL功能,允许在SQL语句中使用条件判断、循环等逻辑,根据不同的情况动态生成SQL语句,从而减少了重复的代码和增加了灵活性。
-
自动映射: MyBatis可以自动将查询结果映射到Java对象上,无需手动编写结果集处理代码,极大地简化了数据的处理过程。
-
缓存机制: MyBatis提供了一级缓存和二级缓存机制,可以有效地减少数据库访问次数,提高系统的性能表现。
-
事务管理: MyBatis提供了完善的事务管理支持,开发人员可以通过注解或者XML配置来声明事务,保证数据的一致性和完整性。
-
插件机制: MyBatis提供了灵活的插件机制,允许开发人员扩展框架的功能,比如自定义类型处理器、拦截器等。
-
与Spring等框架集成: MyBatis可以与Spring等主流框架无缝集成,使得数据库操作与应用程序的其他部分更加紧密地结合在一起。
总的来说,MyBatis是一个功能丰富、灵活性强的持久层框架,它简化了数据库操作的过程,提高了开发效率,是Java开发中常用的持久化解决方案之一。
在上面我们提到⼀个词:持久层。
当我们谈论一个应用程序的体系结构时,持久层通常是指负责数据持久化的部分,也就是数据访问层(DAO,Data Access Object)。在典型的三层架构中,持久层位于应用程序的底层,负责与数据库交互、执行数据访问操作。持久层的主要任务包括将应用程序中的数据存储到数据库中,从数据库中检索数据,并执行各种数据库操作(如插入、更新、删除等)。
正如我们之前所学习的那样,在传统的Java应用程序中,持久层通常是由开发人员手动编写的JDBC代码组成。JDBC(Java Database Connectivity)是Java语言中用于与数据库交互的标准API。通过JDBC,开发人员可以使用Java代码来连接数据库、执行SQL语句并处理结果。然而,使用原始的JDBC编程方式存在一些问题,例如需要手动管理数据库连接、编写大量的重复代码、处理SQL语句与Java对象之间的映射关系等。为了解决这些问题,出现了各种持久化框架,其中MyBatis就是其中之一。MyBatis作为持久化框架,承担了简化数据访问层开发的任务。它提供了一种更加简单、直观的方式来操作数据库,使得开发人员可以更专注于业务逻辑的实现,而无需过多关注与数据库的交互细节。
通过使用MyBatis,开发人员可以通过XML配置文件或者注解来描述SQL语句与Java对象之间的映射关系,使得SQL与Java代码分离,提高了代码的可维护性。同时,MyBatis提供了一些方便的功能,如动态SQL、参数处理、结果集映射等,进一步简化了数据库操作的实现。此外,MyBatis还提供了丰富的扩展机制和插件支持,使得开发人员可以根据实际需求对框架进行灵活的定制和扩展。
简单来说 MyBatis 是更简单完成程序和数据库交互的框架,也就是更简单的操作和读取数据库工具,接下来我们就通过⼀个入门程序,让大家感受⼀下通过Mybatis如何来操作数据库。
MyBatis入门
准备工作
创建工程
创建springboot工程,并导入mybatis的起步依赖、mysql的驱动包:
Mybatis 是⼀个持久层框架, 具体的数据存储和数据操作还是在 MySQL 中操作的, 所以在使用 Mybatis 进行数据库操作时,需要添加 MySQL 驱动。MySQL 驱动负责与 MySQL 数据库建立连接,并提供操作数据库的接口,使得 Mybatis 能够通过 SQL 语句执行数据库操作。
添加 MySQL 驱动通常需要在项目的依赖配置文件中添加相应的依赖项,以确保 Mybatis 能够正常连接和操作 MySQL 数据库。常见的 MySQL 驱动包括 MySQL Connector/J,可以通过 Maven、Gradle 等构建工具来引入依赖。
项目工程创建完成后,自动在pom.xml文件中,导入Mybatis依赖和MySQL驱动依赖。
注意,这里如果你是社区版,版本号可能是3. ……,这对应着JDK17,而我们使用的是JDK8,所以你可以手动更改版本号为2.3.1等等。
或者你也可以在更改完Spring Boot版本号之后,直接删去这个部分,然后右键单击,选择Edit Starters重新创建。
数据准备
创建数据库:
可以使用任意数据库工具,MySQL原生小黑框框也行,我就直接使用IDEA专业版自带的数据库了。
添加数据库原始数据:
-- 创建数据库
DROP DATABASE IF EXISTS mybatis1;
CREATE DATABASE mybatis1 DEFAULT CHARACTER SET utf8mb4;
-- 使用数据库
USE mybatis1;
-- 创建用户表
DROP TABLE IF EXISTS userinfo;
CREATE TABLE `userinfo` (
`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 CURRENT_TIMESTAMP,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4;
-- 添加用户信息
INSERT INTO userinfo (username, `password`, age, gender, phone)
VALUES ('admin', 'admin', 18, 1, '18612340001');
INSERT INTO userinfo (username, `password`, age, gender, phone)
VALUES ('zhangsan', 'zhangsan', 18, 1, '18612340002');
INSERT INTO userinfo (username, `password`, age, gender, phone)
VALUES ('lisi', 'lisi', 18, 1, '18612340003');
INSERT INTO userinfo (username, `password`, age, gender, phone)
VALUES ('wangwu', 'wangwu', 18, 1, '18612340004');
运行代码后,MySQL Command上同时创建好了表:
创建对应的实体类 UserInfo:
实体类的属性名与表中的字段名⼀⼀对应:
package com.example.mybatisstudy.demos.web.model;
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;
}
配置数据库连接字符串
Mybatis中,在pom.xlm中一旦引入数据库的依赖,就一定要进行数据库相关参数配置,这样才能连接数据库:
如果是application.yml文件, 配置内容如下:
# 数据库连接配置
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mybatis_test?characterEncoding=utf8&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
在配置文件中,我们需要指定数据库的连接信息,以便 MyBatis 能够正确连接到数据库并执行相应的 SQL 操作。这些参数配置的准确性和完整性对于应用的正常运行至关重要。
这里面的密码、用户名、url记得根据自己的信息更改。
注意,如果使用 MySQL 是 5.x 之前的,选择的是"com.mysql.jdbc.Driver",如果是大于 5.x的, 选择的 是“com.mysql.cj.jdbc.Driver” 。
如果是application.properties文件, 配置内容如下:
#驱动类名称
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#数据库连接的url
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/mybatis_test?
characterEncoding=utf8&useSSL=false
#连接数据库的⽤⼾名
spring.datasource.username=root
#连接数据库的密码
spring.datasource.password=root
提醒:
如果配置文件乱码,修改字符为UTF-8:
同时修改后续的项目:
写持久层代码
持久层文件一般放在mapper和dao包中,这是一种约定俗成,比较规范:
在项目中, 创建持久层接口UserInfoMapper:
package com.example.mybatisstudy;
import com.example.mybatisstudy.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 访问数据库
*/
@Mapper
public interface UserInfoMapper {
/**
* 返回数据列表
* @return
*/
@Select("select * from userinfo")
List<UserInfo> queryUserList();
}
@Mapper 注解:这个注解标记了该接口是一个 MyBatis 的 Mapper 接口,MyBatis 会扫描带有 @Mapper 注解的接口,并为其生成实现类。
UserInfoMapper 接口定义:这是一个接口,用于定义访问数据库的方法。
queryUserList() 方法:
- 这个方法使用了 @Select 注解,表示执行一个查询操作。
- @Select注解:代表的就是select查询,也就是注解对应方法的具体实现内容。
- 查询的 SQL 语句是 select * from userinfo,用于查询所有的用户信息。
- 返回值是一个 List<UserInfo>,表示返回多个用户信息。
通过这些注解和方法,我们就可以轻松地定义并执行数据库操作,而不需要编写繁琐的 SQL 语句和数据库访问逻辑。
单元测试
在创建出来的SpringBoot工程中,在src下的test目录下,已经自动帮我们创建好了测试类 ,我们可以直接使用这个测试类来进行测试:
测试类上添加了注解 @SpringBootTest,该测试类在运行时,就会自动加载Spring的运行环境。我们通过@Autowired这个注解,注入我们要测试的类,就可以开始进行测试了。
package com.example.mybatisstudy;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class MybatisStudyApplicationTests {
@Autowired
private UserInfoMapper userInfoMapper;
@Test
void contextLoads() {
System.out.println(userInfoMapper.queryUserList());
}
}
使用Idea自动生成测试类
除了手动编写测试类之外,你也可以利用Idea的功能自动生成测试类。
具体步骤如下:
- 在需要进行测试的Mapper接口上,右键点击。
- 选择 "Generate" -> "Test"。
- 在弹出的窗口中,选择要测试的方法。
- 点击确定(OK)按钮。
这样,Idea就会自动生成相应的测试类,并且包含所选方法的测试代码。记得加上@SpringBootTest注解,它是帮助我们加载Spring运行环境的!
package com.example.mybatisstudy.mapper; import lombok.extern.slf4j.Slf4j; 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 static org.junit.jupiter.api.Assertions.*; @Slf4j @SpringBootTest class UserInfoMapperTest { @Autowired private UserInfoMapper userInfoMapper; @BeforeEach void setUp() { log.info("setUp..."); } @AfterEach void tearDown() { log.info("tearDown..."); } @Test void queryUserList() { log.info(userInfoMapper.queryUserList().toString()); } }
综上,你会发现使用MyBatis实现数据库操作真的很简单,它确实简化了与数据库的交互过程:
-
配置数据库信息:
- 在项目中添加 MyBatis 的依赖,例如 Maven 的 pom.xml 文件中添加 MyBatis 的依赖项。
- 在应用的配置文件(如 application.properties 或 application.yml)中配置数据库连接信息,包括数据库 URL、用户名、密码等。
-
创建数据库表:
在数据库中创建相应的表,可以使用 SQL 语句(如提供的 CREATE TABLE 语句)来创建表格。 -
编写 Mapper 接口:
- 创建一个接口来定义数据库操作,这个接口通常包含各种对数据库的操作方法。
- 可以选择使用注解 @Mapper 标记接口,或者将接口方法的实现写在对应的 XML 文件中。
-
定义 SQL 语句:
- 如果选择将 SQL 语句写在 XML 文件中,则需要在 XML 文件中定义 SQL 语句,包括 SELECT、INSERT、UPDATE、DELETE 等操作的语句。
- 如果使用注解方式,则可以在接口方法上使用注解来直接写 SQL 语句。(更简单,所以我们当然选这种方式~)
-
配置 MyBatis:
在配置文件(如 mybatis-config.xml)中指定 Mapper 接口和 XML 文件的位置,以便 MyBatis 能够找到这些接口和 SQL 文件。 -
使用 Mapper 进行数据库操作:
- 在业务逻辑中调用 Mapper 接口中定义的方法,进行数据库的查询、插入、更新、删除等操作。
- MyBatis 将负责将方法调用转换为对应的 SQL 语句,并执行数据库操作。
MyBatis的基础操作
MySQL的建表规约
我们回想一下更改的数据准备工作,里面的有一段MySQL代码我们是这样写的:
你会发现,这里好像和我们之前学习的不大一样?
是的,这段代码遵循了一些常见的建表规约和最佳实践,尤其是在命名、数据类型选择、约束设置和注释等方面:
命名规范:表名和字段名使用了有意义的名称,例如userinfo表示用户信息表,username表示用户名字段等。命名采用了小写字母和下划线的组合,这是一种常见的命名风格,易于阅读和维护。
数据类型选择:
对于id字段,选择了INT类型,用于存储整数值,适用于自增主键。
对于username、password和phone等字段,选择了VARCHAR类型,用于存储字符串。
对于age、gender和delete_flag等字段,选择了TINYINT类型,用于存储较小范围的整数值。
约束设置:
- 对于id字段,设置了NOT NULL约束和PRIMARY KEY主键约束,确保每条记录都有唯一的标识符。
- 对于username、password、age等字段,设置了NOT NULL约束,确保这些字段的值不为空。
- 默认值设置:
- 对于gender和delete_flag等字段,设置了默认值,便于在插入记录时自动填充默认值,确保数据完整性。
注释:为gender和delete_flag字段添加了注释,解释了每个数字代表的含义,提高了代码的可读性和可维护性。
存储引擎和字符集设置:使用了InnoDB存储引擎和utf8mb4字符集,这是一种常见的配置,提供了良好的性能和兼容性。
代码解释:
- AUTO_INCREMENT: 这个属性用于在插入新行时自动为id列生成唯一的递增值。这样,每次插入新记录时,id列会自动递增,避免了手动指定主键值的麻烦。
- TINYINT(4): 这是用于存储整数值的数据类型,但与一般的INT类型不同的是,它指定了字段宽度。在MySQL中,TINYINT类型可以存储范围较小的整数值,指定宽度后可确保存储的值不会超出范围。
- DEFAULT '0' 和 DEFAULT 0: 这些是为字段设置默认值的语法。在这里,gender字段和delete_flag字段都设置了默认值。如果插入行时没有为这些字段提供值,数据库将使用指定的默认值。
- COMMENT: 这个关键字用于为列添加注释,以便在创建表时提供更多的说明或描述。
- DATETIME DEFAULT CURRENT_TIMESTAMP: 这是用于设置字段默认值的常见语法。在这里,create_time和update_time字段都被设置为默认值,分别为当前时间戳。这意味着当插入新记录时,这些字段将自动填充当前时间戳。
- ENGINE=INNODB 和 DEFAULT CHARSET=utf8mb4: 这些是指定表的存储引擎和字符集的选项。在这里,表使用InnoDB引擎和utf8mb4字符集。InnoDB是MySQL的一种常见存储引擎,utf8mb4字符集支持更广泛的字符集,包括 emoji 等。
所以我们这段代码的具体含义其实是这样的:
- id: 是表的主键,使用INT类型,自动递增生成,确保每条记录都有唯一的标识符。
- username、password、age、gender、phone、delete_flag、create_time和update_time:这些是表的字段,分别代表了用户的用户名、密码、年龄、性别、电话号码、删除标志、创建时间和更新时间。
- username、password、age、phone:这些字段使用VARCHAR类型存储字符型数据,age和delete_flag使用TINYINT类型存储整型数据。
- gender:这个字段也是使用TINYINT类型,用于表示性别,其默认值为0,但是通过注释额外解释了每个数字对应的含义(1代表男性,2代表女性,0代表默认)。
- delete_flag:这个字段表示用户是否被删除,是一个删除标志,0表示未删除,1表示已删除,默认为0。数据删除大致分为逻辑删除(update)和物理删除(delete)。
- create_time和update_time:这两个字段使用DATETIME类型存储日期和时间,分别表示用户信息的创建时间和最后更新时间。
- ENGINE=INNODB:指定了表的存储引擎为InnoDB,这是MySQL的一种常见存储引擎,提供了事务支持和行级锁定等功能。
- DEFAULT CHARSET=utf8mb4:指定了表的字符集为utf8mb4,支持更广泛的字符集,包括 emoji 等。
逻辑删除(Logical Deletion):
- 在逻辑删除中,不会直接从数据库中删除数据记录,而是通过修改记录的状态或标志位来表示数据已被删除。
- 典型的做法是在表中添加一个额外的字段,通常称为"delete_flag"或类似的名字,用来标识记录是否被删除。
- 当执行删除操作时,实际上是将这个字段的值设置为一个特定的状态,表示记录已被逻辑删除。
- 逻辑删除保留了数据的历史记录,便于进行数据恢复和审计。
物理删除(Physical Deletion):
- 在物理删除中,直接从数据库中删除数据记录,彻底将记录从数据库中移除。
- 这意味着被删除的数据将不再存在于数据库中,无法恢复。
- 物理删除会释放数据库中的存储空间,但可能会导致数据丢失和不可恢复。
选择逻辑删除还是物理删除取决于具体的业务需求和应用场景。逻辑删除通常用于保留数据的完整性和历史记录,以及防止误删数据。而物理删除通常用于彻底清理不再需要的数据,以释放存储空间并提高数据库性能。在工作中,最常用的是逻辑删除。
关于MySQL的建表规约,感兴趣的可以通过阿里巴巴Java开发手册学习:Java手册页面-阿里云开发者社区-阿里云官网开发者社区_云计算社区
打印日志
我们可以通过配置日志来查看 SQL 语句的执行、执行传递的参数以及执行结果。
在配置文件中进行配置即可:
mybatis:
configuration: # 配置打印 MyBatis⽇志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
如果是application.properties, 配置内容如下:
#指定mybatis输出日志的位置, 输出控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
运行之后发现比先前多了一些SQL方法的内容:
参数传递
我现在希望能够查询到指定ID的人员信息,又不能把这个ID定死,那么我们应该如何实现?
package com.example.mybatisstudy.mapper;
import com.example.mybatisstudy.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 访问数据库
*/
@Mapper
public interface UserInfoMapper {
/**
* 返回数据列表
* @return
*/
@Select("select * from userinfo")
List<UserInfo> queryUserList();
//#{}内容和下方参数名称一致时正常运行,那么不一样会报错吗???
@Select("select * from userinfo where id = #{id}") //这里的#{}内容和下方userId不一样
UserInfo queryUserInfo(Integer userId);
}
更新测试类之后新生成了一个方法,我们可以输入信息,查询指定ID:
queryUserInfo(Integer userId) 方法:
- 这个方法也使用了 @Select 注解,表示执行一个查询操作。
- 查询的 SQL 语句是 select * from userinfo where id = #{id},用于根据用户ID查询单个用户信息。
- 方法参数 userId 使用了 @Param("id") 注解,表示将方法参数与 SQL 语句中的 #{id} 占位符进行映射。
运行一下:
那么如果我们现在想要传递两个参数呢?
package com.example.mybatisstudy.mapper;
import com.example.mybatisstudy.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 访问数据库
*/
@Mapper
public interface UserInfoMapper {
/**
* 返回数据列表
* @return
*/
@Select("select * from userinfo")
List<UserInfo> queryUserList();
@Select("select * from userinfo where id = #{id}")
UserInfo queryUserInfo(Integer userId);
@Select("select * from userinfo where id = #{userId} and delete_flag = #{deleteFlag}")
UserInfo queryUserInfo(Integer userId, Integer deleteFlag);
}
一运行,发现报错了:
这段异常信息表明在 MyBatis 的配置中存在重复的 Mapped Statements,具体是因为在 com.example.mybatisstudy.mapper.UserInfoMapper 接口中的 queryUserInfo 方法已经在 MyBatis 的配置中被定义了两次,导致了冲突。
我们稍作修改:
@Select("select * from userinfo where id = #{id} and delete_flag = #{param2}")
UserInfo queryUserInfoByDF(Integer userId,Integer deleteFlag);
又报错了:
它叫我们去使用 param1、param2。
Mybatis会给参数起一个默认的名字,param1、param2……
当方法参数没有使用 @Param 注解进行命名时,MyBatis 会为参数自动生成默认的参数名,如 param1、param2 等。在 SQL 查询语句中,我们可以直接使用这些默认的参数名作为参数占位符,例如 #{param1}、#{param2} 等
@Select("select * from userinfo where id = #{param1} and delete_flag = #{deleteFlag}")
UserInfo queryUserInfoByDF(Integer userId,Integer deleteFlag);
queryUserInfoByDF(Integer userId, Integer deleteFlag) 方法:
- 这个方法也使用了 @Select 注解,表示执行一个查询操作。
- 查询的 SQL 语句是 select * from userinfo where id = #{param1} and delete_flag = #{deleteFlag},用于根据用户ID和删除标志查询单个用户信息。
- 方法参数 userId 使用了默认的参数位置索引,而 deleteFlag 直接使用了方法参数名。
因此,我们可以得出以下结论:
1、在MyBatis中,获取参数的方式是使用#{参数名称}。当方法只有一个参数时,参数名称可以任意选择,但最好与方法参数的名称保持一致。
2、如果Mapper接口方法只有一个普通类型的参数,那么在#{...}中的属性名可以随意设置,比如#{id}、#{value}等。然而,建议与方法参数名保持一致,以提高代码的可读性和维护性。
3、当接口方法有两个参数时,我们需要在SQL语句中直接使用#{param1}、#{param2}等。这样做是合法的,并且对于简单的情况来说是有效的。参数会直接映射到#{param1}和#{param2}中。
@Select("SELECT * FROM table WHERE column1 = #{param1} AND column2 = #{param2}")
ResultType methodName(Type1 param1, Type2 param2);
在这种情况下,param1和param2直接映射到了#{param1}和#{param2}。
4、对于接口方法有两个参数的情况,除了直接使用#{param1}、#{param2}等作为参数占位符外,我们也可以使用与方法参数名相同的方式来引用参数。
例如:
@Select("select * from userinfo where id = #{id} and delete_flag = #{deleteFlag}")
UserInfo queryUserInfoByDF(Integer id,Integer deleteFlag);
在这种情况下,#{userId}和#{deleteFlag}会与方法参数userId和deleteFlag进行匹配,这样也是合法的,并且是常用的做法。这种方式在提高代码的可读性和维护性方面更具优势,因为它明确地指定了每个参数的含义
但是,如果你觉得直接使用#{param1}、#{param2}等不够明确,或者你想要提高代码的可读性和维护性,就可以选择使用@Param注解来为参数设置别名,然后在SQL语句中使用这些别名。这种方式更加明确和推荐。
通过使用@Param注解,我们可以为参数设置别名。如果使用@Param设置了别名,那么在#{...}中的属性名必须与@Param设置的别名相匹配,这样才能确保参数传递的准确性和正确性。
@Select("select * from userinfo where id = #{id} and delete_flag = #{deleteFlag}")
UserInfo queryUserInfoParam(@Param("id") Integer userId, @Param("deleteFlag") Integer deleteFlag);
queryUserInfoParam(@Param("id")Integer userId, Integer deleteFlag) 方法:
- 这个方法也使用了 @Select 注解,表示执行一个查询操作。
- 查询的 SQL 语句是 select * from userinfo where id = #{id} and delete_flag = #{deleteFlag},用于根据用户ID和删除标志查询单个用户信息。
- 方法参数 userId 使用了 @Param("id") 注解,表示将方法参数与 SQL 语句中的 #{id} 占位符进行映射,而 deleteFlag 直接使用了方法参数名。
当然,使用了@Param重命名之后,参数就绑定了,你这个时候再次将SQL改成原来的#{userId}就会报错了,必须使用重命名之后的名字。
注意事项:
最后,有一个地方要提示一下,我自己也是纠结了好久,最后才知道的。因为前不久Spring官方做了一个升级,无法通过IDEA创建jdk8,所以我们使用阿里云创建的jdk8。
但这里出现一个问题,如果使用阿里云创建:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
它此处做的是依赖的声明而不是引入。
而没有用阿里云的创建的是这样的:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.17</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
后面这样的没有任何问题,但是前者——阿里云,在传参的时候就会不会自动创建参数,所以以下这样写都是会报错的:
@Select("select * from userinfo where id = #{userId} and delete_flag = #{deleteFlag}")
UserInfo queryUserInfoByDF(Integer userId,Integer deleteFlag);
@Select("select * from userinfo where id = #{param1} and delete_flag = #{deleteFlag}")
UserInfo queryUserInfoByDF(Integer userId,Integer deleteFlag);
@Select("select * from userinfo where id = #{userId} and delete_flag = #{param2}")
UserInfo queryUserInfoByDF(Integer userId,Integer deleteFlag);
只能
@Select("select * from userinfo where id = #{param1} and delete_flag = #{param2}")
UserInfo queryUserInfoByDF(Integer userId,Integer deleteFlag);
这样写,或者重命名。
如果你觉得这样很麻烦,可以直接把阿里云的那段依赖删掉,改成第二种依赖就行了。
如何通过URL访问到数据库项目?
package com.example.mybatisstudy.service;
import com.example.mybatisstudy.mapper.UserInfoMapper;
import com.example.mybatisstudy.model.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
public List<UserInfo> queryAllUser() {
return userInfoMapper.queryUserList();
}
}
package com.example.mybatisstudy.controller;
import com.example.mybatisstudy.model.UserInfo;
import com.example.mybatisstudy.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/queryAllUser")
public List<UserInfo> queryAllUser(){
return userService.queryAllUser();
}
}
现在启动程序:
增(Insert)
@Insert("insert into userinfo(username, password,age,gender, phone) " +
"values(#{username},#{password},#{age},#{gender},#{phone})")
Integer insert(UserInfo userInfo);
虽然 insert 方法只有一个参数 UserInfo userInfo,但是该参数是一个对象类型。在 MyBatis 中,当方法参数是一个对象时,MyBatis 会自动将对象的属性与 SQL 语句中的占位符进行匹配,并将对象的属性值设置到 SQL 语句中对应的位置。
具体来说,这里的 SQL 插入语句中使用了 #{} 占位符来表示参数,如 #{username}、#{password} 等。当调用 insert 方法时,MyBatis 会将传入的 UserInfo 对象中的 username、password、age、gender 和 phone 等属性的值分别设置到 SQL 语句中相应的位置,然后执行插入操作。
这种方式使得代码更加简洁和易读,同时也提高了代码的可维护性。我们不需要手动拼接 SQL 语句或者逐个设置参数值,而是直接将对象传入方法中即可完成数据的插入操作。
我们去数据库看看:
添加成功!
那么我们如果稍作修改:
@Insert("insert into userinfo(username, password,age,gender, phone) " +
"values(#{username},#{password},#{age},#{gender},#{phone})")
Integer insertByParam(@Param("userInfo")UserInfo userInfo);
程序报错了,绑定异常。
类似于JS,重命名之后要将前面SQL语句中的 #{……} 全部修改为 #{userInfo. ……}:
@Insert("insert into userinfo(username, password,age,gender, phone) " +
"values(#{userInfo.username},#{userInfo.password},#{userInfo.age},#{userInfo.gender},#{userInfo.phone})")
Integer insertByParam(@Param("userInfo") UserInfo userInfo);
所以,使用对象来进行参数传递时有两种情况:
未使用 @Param 进行重命名:
在这种情况下,可以直接使用对象的属性名来引用参数。例如,如果参数是 UserInfo userInfo,则在 SQL 语句中可以直接使用 #{username}、#{password} 等属性名来获取参数值。
使用 @Param 进行重命名:
如果使用了 @Param 注解为参数重命名,那么在 SQL 语句中需要使用 对象.属性名 的方式来获取参数。例如,如果参数是 UserInfo userInfo,并且使用了 @Param("user") 重命名,那么在 SQL 语句中需要使用 #{user.username}、#{user.password} 等形式来引用参数。
还有,你会发现日志打印里面只显示了这个:
<== Updates: 1: 这表示执行插入操作后,受影响的行数为 1,表明成功插入了一条记录。
这是因为我们的方法的返回类型设置的是Integer。
如果我们想要打印出信息,比如userId:
@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert("insert into userinfo(username, password,age,gender, phone) " +
"values(#{userInfo.username},#{userInfo.password},#{userInfo.age},#{userInfo.gender},#{userInfo.phone})")
Integer insertByParam(@Param("userInfo") UserInfo userInfo);
这段代码中的 @Options 注解用于配置插入操作的一些选项。具体来说:
useGeneratedKeys = true:这个参数告诉 MyBatis 使用数据库自动生成的主键值。通常情况下,当向数据库插入一条记录时,如果表的主键是自增长的(例如 MySQL 中的 AUTO_INCREMENT),数据库会自动生成一个主键值。useGeneratedKeys()默认值是false,所以我们需要修改为true,后面的keyProperty指定打印的值。设置 useGeneratedKeys 为 true 后,MyBatis 将会使用数据库生成的主键值。
keyProperty = "id":这个参数指定了将自动生成的主键值赋给对象中的哪个属性。在这里,id 属性将会被赋予自动生成的主键值。这个属性必须与数据库表中的主键列名一致,并且与实体类中的属性名一致,以便 MyBatis 能够正确地将生成的主键值映射到对象中。
删(Delete)
@Delete("delete from userinfo where id= #{id}")
Integer delete(@Param("id") Integer id);
改(Update)
当要更新的字段不算多的时候:
@Update("update userinfo set password=#{password} where id=#{id}")
Integer update(String password,Integer id);
//阿里云的用下面的:
@Update("update userinfo set password=#{param1} where id=#{param2}")
Integer update(String password,Integer id);
当要更新的字段很多,可以使用对象来传递参数:
@Update("update userinfo set username=#{username}, password=#{password}, age=#{age} where id=#{id}")
Integer updateByOb(UserInfo userInfo);
如果:
@Test
void updateByOb() {
UserInfo userInfo = new UserInfo();
userInfo.setUsername("zhaoliu666666");
userInfo.setPassword("1234566666666");
userInfo.setAge(99);
//userInfo.setId(2);
userInfoMapper.updateByOb(userInfo);
}
@Test
void updateByOb() {
UserInfo userInfo = new UserInfo();
userInfo.setUsername("zhaoliu666666");
userInfo.setPassword("1234566666666");
userInfo.setAge(99);
userInfo.setId(100);
userInfoMapper.updateByOb(userInfo);
}
看似运行成功,实则没有更新:
所以一定要关注处理行数,这也是更新操作经常犯错的地方。
查(Select)
我们其实最早讲到的就是“查询”(MyBatis入门)。我们现在回过头去重新运行一下代码:
@Select("select * from userinfo")
List<UserInfo> queryUserList();
@Select("select * from userinfo where id = #{id}")
UserInfo queryUserInfo(Integer userId);
@Select("select * from userinfo where id = #{param1} and delete_flag = #{param2}")
UserInfo queryUserInfoByDF(Integer userId,Integer deleteFlag);
@Select("select * from userinfo where id = #{id} and delete_flag = #{deleteFlag}")
UserInfo queryUserInfoParam(@Param("id") Integer userId, @Param("deleteFlag") Integer deleteFlag);
为什么会出现这种问题呢?我们需要回到原理部分思考一下。
MyBatis 其实也是借助 JDBC 来与数据库进行交互的,因此它的执行流程与 JDBC 类似,但是 MyBatis 在这个基础上进行了进一步的封装和简化,使得我们可以更方便地进行数据库操作。
之前说过,MyBatis 封装了 JDBC 中繁琐的连接管理、SQL 语句的构建、结果集的处理等步骤,提供了更加简洁、易用的接口和配置方式,从而降低了开发的复杂度和工作量。
那么对于Mybatis来说,上面的每一个步骤,它分别都是如何实现的?:
-
创建数据库连接池 DataSource: MyBatis 使用数据源(DataSource)来管理数据库连接。数据源可以通过配置文件或者直接在代码中进行配置。在配置文件中,可以使用诸如 HikariCP、Apache DBCP 等数据库连接池实现类,并指定连接池的相关参数,如连接池大小、连接超时时间等。在代码中,也可以通过编程方式创建 DataSource 对象,并设置相应的参数。
-
通过 DataSource 获取数据库连接 Connection: MyBatis 使用 DataSource 来获取数据库连接。在配置文件中配置了 DataSource 后,MyBatis 可以直接通过该数据源来获取连接,而无需手动管理连接的创建和释放。MyBatis 会在需要时自动从数据源中获取连接,并在使用完毕后将连接返回给连接池。
-
编写要执行带 ? 占位符的 SQL 语句: 在 MyBatis 中,SQL 语句通常是以字符串的形式直接写在 XML 文件中,或者使用注解的方式写在接口方法上。可以在 SQL 语句中使用 ? 占位符来代替实际的参数值,以增加 SQL 语句的安全性。在执行 SQL 语句时,MyBatis 会将 ? 占位符替换为实际的参数值。
-
通过 Connection 及 SQL 创建操作命令对象 Statement: 在 MyBatis 中,通过配置文件或者注解,可以将 SQL 语句与 Java 方法绑定在一起。MyBatis 会根据配置信息来创建相应的操作命令对象,如 PreparedStatement 或 CallableStatement。这些操作命令对象负责执行 SQL 语句,并处理查询结果。在执行 SQL 语句之前,MyBatis 会根据方法参数的信息,将参数值绑定到 SQL 语句中的占位符上。这样,SQL 语句就变成了一个完整的可执行的 SQL 命令。
-
替换占位符: 在 MyBatis 中,可以使用 #{paramName} 形式的占位符来代替 SQL 语句中的 ? 占位符。在执行 SQL 语句时,MyBatis 会将这些 #{paramName} 替换为相应的参数值,并使用 PreparedStatement 来执行 SQL 语句。
-
使用 Statement 执行 SQL 语句: 在 MyBatis 中,根据配置信息和方法调用来决定使用哪种 Statement 对象执行 SQL 语句的过程是通过注解来进行的。不同的注解会触发不同的 SQL 执行方式,从而使用相应的 Statement 对象。@Select 注解:当使用 @Select 注解执行查询操作时,MyBatis 会使用 PreparedStatement 来执行 SQL 查询。查询结果将会被封装成相应的 Java 对象并返回给调用者。@Update、@Insert、@Delete 注解:对于更新操作,如插入、更新和删除,MyBatis 也会根据注解来决定使用哪种 Statement 对象执行 SQL 更新语句。通常情况下,更新操作会使用 PreparedStatement 或 Statement 来执行 SQL 更新,并返回更新的数量。通过注解的方式,MyBatis 能够根据方法上的注解提示来选择合适的 SQL 执行方式,从而在执行 SQL 时提供更高的灵活性和定制性。
-
处理结果集: 在 MyBatis 中,查询结果会被封装成相应的 Java 对象,并以列表或者单个对象的形式返回给调用方。调用方可以直接对这些结果进行处理,例如遍历列表、获取对象属性等。
-
释放资源: MyBatis 会在执行完 SQL 语句后自动释放资源,包括关闭 Statement、关闭 ResultSet 和释放 Connection。这样可以确保及时释放数据库连接和其他资源,避免资源泄漏和占用过多的数据库连接。
前面的6个步骤我们都已经一一完成了,最后一个步骤 MyBatis 会自动完成,只剩下第7个。
在JDBC中,我们处理结果集是这样的:
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"));
}
- 执行查询操作:首先,通过执行 SQL 查询语句(使用 executeQuery() 方法)向数据库发送查询请求,并获得一个结果集对象(ResultSet)。
- 遍历结果集:通过调用结果集对象的 next() 方法,可以将光标移动到结果集的下一行数据。初始时,光标位于结果集的第一行之前。next() 方法返回一个布尔值,表示是否还有下一行数据。如果有,光标移动到下一行;如果没有,则说明结果集已经遍历完毕。
- 提取数据:一旦光标移动到了结果集的某一行,就可以使用结果集对象的方法(如 getString()、getInt() 等)提取该行的数据。这些方法需要提供列名或者列索引作为参数,以指示要提取的数据位于结果集的哪一列。
- 映射到对象:一般情况下,提取的数据会被映射到相应的 Java 对象中。例如,将结果集中的每一行数据映射到一个 Book 对象中,其中每个属性分别对应于结果集中的某一列数据。
- 重复步骤 2 和步骤 3:在遍历结果集的过程中,通常会使用循环(如 while 循环)重复执行步骤 2 和步骤 3,直到遍历完整个结果集。
在以上过程中,通过逐行遍历结果集并提取数据,就可以将数据库中的查询结果转化为程序中的 Java 对象,方便后续的处理和操作。
那么与之对应的,Mybatis是如何进行映射的?
你会发现,如果Java属性和mysql的字段一样,mybatis会进行自动赋值,如果不一样,则不赋值。
这个时候,对于不一样的字段,就需要由程序猿告诉Mybatis如何进行赋值……
对于不同的字段和 Java 属性的映射关系,MyBatis 提供了多种方式来进行配置:
- 改别名:在 SQL 语句中,可以使用 AS 关键字为字段设置别名,使其与 Java 对象中的属性名一致。例如:SELECT user_id AS userId FROM users,这样查询结果中的 user_id 列会被映射到 Java 对象的 userId 属性中。
- 通过注解来映射:使用 MyBatis 提供的注解(如 @Results、@Result 等)可以在映射文件或者接口方法上直接指定字段和 Java 属性之间的映射关系。通过在查询语句上方的注解中指定 @Result 注解,可以将查询结果中的列与 Java 对象的属性进行映射。
- 配置自动驼峰转换:MyBatis 允许配置自动将数据库列名中的下划线命名规则转换为 Java 属性的驼峰命名规则。通过在 MyBatis 的配置文件中配置 mapUnderscoreToCamelCase 参数为 true,可以启用这个功能。这样,在执行查询时,MyBatis 将自动将数据库列名中的下划线转换为 Java 属性名的驼峰形式,从而实现自动的字段映射。
通过以上方式,可以灵活地配置字段和 Java 属性之间的映射关系,使得查询结果能够正确地映射到 Java 对象中。
改别名
把:
@Select("select * from userinfo")
List<UserInfo> queryUserList();
先改为:
@Select("select id, username, password,age,gender,phone," +
"delete_flag , create_time, update_time from userinfo")
List<UserInfo> queryUserList();
结果是没有变的,因为我们只是把*展开来了:
[UserInfo(id=1, username=admin, password=admin, age=18, gender=1, phone=18612340001, deleteFlag=null, createTime=null, updateTime=null),
UserInfo(id=2, username=zhaoliu666666, password=1234566666666, age=99, gender=1, phone=18612340002, deleteFlag=null, createTime=null, updateTime=null),
UserInfo(id=3, username=lisi, password=lisi, age=18, gender=1, phone=18612340003, deleteFlag=null, createTime=null, updateTime=null),
UserInfo(id=4, username=wangwu, password=wangwu, age=18, gender=1, phone=18612340004, deleteFlag=null, createTime=null, updateTime=null),
UserInfo(id=5, username=zhaoliu, password=123456, age=14, gender=0, phone=15677777888, deleteFlag=null, createTime=null, updateTime=null),
UserInfo(id=6, username=zhaoliu, password=wwwwwwwww, age=14, gender=0, phone=15677777888, deleteFlag=null, createTime=null, updateTime=null)]
现在改别名:
@Select("select id, username, password,age,gender,phone," +
"delete_flag as deleteFlag, create_time as createTime, update_time as updateTime from userinfo")
List<UserInfo> queryUserList();
MySQL中的列名全部改了:
也全部拿到值了:
通过注解来映射
@Results的定义:
@Result的定义:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Repeatable(Results.class)
public @interface Result {
boolean id() default false;
String column() default "";
String property() default "";
Class<?> javaType() default void.class;
JdbcType jdbcType() default JdbcType.UNDEFINED;
Class<? extends TypeHandler> typeHandler() default UnknownTypeHandler.class;
One one() default @One;
Many many() default @Many;
}
所以我们可以把:
@Select("select * from userinfo where id = #{id}")
UserInfo queryUserInfo(Integer userId);
改为:
@Results({
@Result(column = "delete_flag", property = "deleteFlag"),
@Result(column = "create_time", property = "createTime"),
@Result(column = "update_time", property = "updateTime"),
})
@Select("select * from userinfo where id = #{id}")
UserInfo queryUserInfo(Integer userId);
成功拿到值了:
那么难道我们每次都要这样写一大堆吗?可以复用代码吗?
@Results(id = "BaseResult",value = {
@Result(column = "delete_flag", property = "deleteFlag"),
@Result(column = "create_time", property = "createTime"),
@Result(column = "update_time", property = "updateTime"),
})
@Select("select * from userinfo where id = #{id}")
UserInfo queryUserInfo(Integer userId);
我们将这个 @Results 注解定义为一个可复用的配置,并通过 id 属性给它命名,然后在需要使用的地方引用它即可。
@Results(id = "BaseResult")
@Select("select * from userinfo where id = #{param1} and delete_flag = #{param2}")
UserInfo queryUserInfoByDF(Integer userId,Integer deleteFlag);
出错了:
报错的原因是:在 UserInfoMapper 接口中定义的 BaseResult 结果映射已经被添加过了,因此在尝试再次添加时发生了冲突。
这个问题通常是由于在 UserInfoMapper 接口中的多个方法上都引用了相同的 @Results 注解,并且给它们设置了相同的 id。由于 id 必须是唯一的,因此在第二次尝试添加相同 id 的结果映射时就会报错。
所以我们需要:
- 确保每个 @Results 注解的 id 属性都是唯一的。
- 如果多个方法需要相同的结果映射,可以将它们的 @Results 注解合并到一个方法上,并确保其他方法引用这个方法即可。
- 如果多个方法需要相同的结果映射,但有些方法需要额外的映射,可以在其他方法上使用 @ResultMap 注解引用已定义的结果映射,并在需要的地方添加额外的映射。
通过以上方式,就可以避免结果映射重复定义而导致的冲突错误。
- @Results 注解通常用于直接在方法上定义结果映射,它将多个 @Result 注解组合在一起,用于描述如何将查询结果映射到 Java 对象的属性上。这种方式适用于单个方法需要自定义结果映射的情况。
- @ResultMap 注解用于引用已经在 XML 配置文件或其他方法上定义的结果映射。通过在方法上使用 @ResultMap 注解,可以重用已经定义好的结果映射,避免了重复定义相同的结果映射,提高了代码的复用性和可维护性。这种方式适用于多个方法需要共享相同的结果映射的情况。
@ResultMap(value = "BaseResult")
@Select("select * from userinfo where id = #{param1} and delete_flag = #{param2}")
UserInfo queryUserInfoByDF(Integer userId,Integer deleteFlag);
开启驼峰命名(推荐)
在许多数据库中,列名通常使用下划线分隔的蛇形命名法(snake_case)来命名,例如 first_name、last_login_time 等。
而在 Java 中,属性名通常使用驼峰命名法(camelCase)来命名,例如 firstName、lastLoginTime 等。
当我们使用 MyBatis 进行数据操作时,如果数据库列名和 Java 对象的属性名不一致,就需要进行手动的结果映射,即通过 @Results 或 @Result 注解来指定数据库列和 Java 属性之间的映射关系。但是,如果数据库列名和 Java 属性名之间能够自动进行映射,就能够减少这种手动映射的工作量。
为了实现数据库列名和 Java 属性名之间的自动映射,MyBatis 提供了一个配置选项 mapUnderscoreToCamelCase。当设置 mapUnderscoreToCamelCase 为 true 时,MyBatis 会自动将数据库列名中的下划线转换为驼峰命名法,然后与 Java 对象的属性名进行匹配。例如,数据库列 first_name 将自动映射到 Java 对象的属性 firstName,而无需额外的手动映射配置。
这样的设置可以大大简化代码,减少手动映射的工作量,提高开发效率。同时,也能够保持数据库列名和 Java 属性名之间的一致性,使代码更易读和维护。
mybatis:
configuration: # 配置打印 MyBatis⽇志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true #配置驼峰⾃动转换
不改变,直接测试:
MyBatis XML配置文件
MyBatis的开发方式主要分为两种:注解方式和XML方式。
在注解方式中,主要用于实现简单的增删改查功能。通过注解,我们可以直接在Java代码中编写SQL语句,使得开发过程更加简洁和直观。然而,当需要实现复杂的SQL功能时,建议采用XML方式来配置映射语句,即将SQL语句写在XML配置文件中。
采用MyBatis XML方式进行开发通常包括以下两个步骤:
-
配置数据库连接字符串和MyBatis:在XML配置文件中,需要配置数据库连接信息以及MyBatis的相关设置,如数据库驱动、连接池等。
-
写持久层代码:在XML配置文件中,编写SQL语句和映射配置,将SQL语句与Java方法进行映射,并定义参数和结果映射关系。然后在Java代码中,通过MyBatis的API来调用XML配置文件中定义的SQL语句和映射配置,实现持久化操作。
使用XML方式配置映射语句的优势在于可以更清晰地管理和维护SQL语句,同时也提供了更灵活、更强大的功能支持,适用于复杂的业务需求和SQL操作。
接下来我们就进行XML的方式的学习。
配置连接字符串和MyBatis
先引入依赖(前面已经引入)。
进行配置
此步骤需要进行两项设置,数据库连接字符串设置和 MyBatis 的 XML 文件配置。
如果是application.yml文件,配置内容如下:
# 数据库连接配置
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mybatis_test?characterEncoding=utf8&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
# 配置 mybatis xml 的⽂件路径,在 resources/mapper 创建所有表的 xml ⽂件
mybatis:
mapper-locations: classpath:mapper/**Mapper.xml
如果是application.properties文件,配置内容如下:
#驱动类名称
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#数据库连接的url
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/mybatis_test?
characterEncoding=utf8&useSSL=false
#连接数据库的⽤⼾名
spring.datasource.username=root
#连接数据库的密码
spring.datasource.password=root
# 配置 mybatis xml 的⽂件路径,在 resources/mapper 创建所有表的 xml ⽂件
mybatis.mapper-locations=classpath:mapper/**Mapper.xml
# 应用服务 WEB 访问端口
server.port: 8080
# 下面这些内容是为了让 MyBatis 映射
# 指定 MyBatis 的 Mapper 文件
mybatis.mapper-locations: classpath:mappers/*xml
# 指定 MyBatis 的实体目录
mybatis.type-aliases-package: com.example.mybatisstudy.mybatis.entity
# 数据库连接配置
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mybatis1?characterEncoding=utf8&useSSL=false
username: root
password: di12052427
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
configuration: # 配置打印 MyBatis⽇志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true #配置驼峰⾃动转换
mapper-locations: classpath:mapper/**Mapper.xml
这行配置的含义是告诉 MyBatis 框架去类路径下的 mapper 目录中寻找所有以 Mapper.xml 结尾的 XML 映射文件。这些 XML 文件包含了 SQL 映射语句,用于将 Java 接口中的方法与 SQL 查询、更新等操作进行映射关联。
写持久层代码
在使用 MyBatis 进行持久化操作时,持久层代码通常分为两部分:
-
方法定义 Interface: 这部分主要定义了数据访问接口(DAO接口),其中声明了各种数据操作方法的原型,包括查询、插入、更新和删除等。这些方法的参数和返回类型会被明确定义,通常是 Java 对象或基本数据类型。这些接口方法可以使用注解或者 XML 配置来映射具体的 SQL 语句。
-
方法实现: XXX.xml: 这部分实现了接口中定义的各种数据操作方法。每个方法都对应一个具体的 SQL 语句,这些 SQL 语句可以在 XML 文件中进行定义和管理。XML 文件中会配置与接口方法对应的 SQL 语句,包括查询、插入、更新和删除等操作。在 XML 文件中,还可以使用参数映射、结果映射等功能来实现更加灵活和复杂的数据库操作。
通过这种分层设计,持久层的接口和实现逻辑分离,提高了代码的可维护性和可扩展性。同时,通过定义接口,可以方便地在业务层中引用,并且可以灵活选择注解方式或 XML 配置方式来实现数据访问。
添加 mapper 接口
数据持久层的接口定义:
package com.example.mybatisstudy.mapper;
import com.example.mybatisstudy.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface UserInfoXmlMapper {
List<UserInfo>queryUserList();
}
添加 UserInfoXMLMapper.xml
数据持久成的实现。
MyBatis 的固定 xml 格式,复制即可:
<?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.mybatisstudy.mapper.UserInfoXmlMapper">
</mapper>
<mapper namespace="com.bite.mybatis.mapper.UserInfoXmlMapper">: 这是 <mapper> 元素,它是 MyBatis XML 映射文件的根元素。namespace 属性指定了该映射文件所对应的 Java 接口的全限定名。在这个示例中,该映射文件对应的是:"com.example.mybatisstudy.mapper.UserInfoXmlMapper"接口。
MyBatis 的每个映射文件通常都会与一个对应的 Java 接口相关联,该接口定义了数据库操作的方法。
通过 namespace 属性,MyBatis 能够将 XML 中的 SQL 映射到对应的 Java 接口方法上。
在这个示例中,XML 文件还没有定义任何具体的 SQL 映射关系,后续会在该文件中添加与 com.bite.mybatis.mapper.UserInfoXmlMapper 接口方法对应的 SQL 映射。
查询所有用户的具体实现:
package com.example.mybatisstudy.mapper;
import com.example.mybatisstudy.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface UserInfoXmlMapper {
List<UserInfo>queryUserList();
}
<select id="queryUserList" resultType="com.example.mybatisstudy.model.UserInfo">
select * from userinfo
</select>
这条语句的作用是执行一条查询语句,将查询结果映射为 com.example.mybatisstudy.model.UserInfo 类型的对象:
- id="queryUserList":指定了这个查询操作的唯一标识符,可以通过这个标识符在 Java 代码中进行调用。
- resultType="com.example.mybatisstudy.model.UserInfo":指定了查询结果的映射类型,即查询结果将会被映射成 com.example.mybatisstudy.model.UserInfo 类型的对象。
- <select> 标签内的 SQL 查询语句:定义了具体的查询操作,这里是查询 userinfo 表中的所有数据。
单元测试
增删改查操作
增(Insert)
如果使用@Param设置参数名称的话, 使用方法和注解类似。
实现接口:
<insert id="insert">
insert into
userinfo
(username, password,age,gender, phone)
values
(#{username},#{password},#{age},#{gender},#{phone})
</insert>
增加成功:
返回自增 id
接口定义不变,Mapper.xml 实现,设置useGeneratedKeys 和keyProperty属性:
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into
userinfo
(username, password,age,gender, phone)
values
(#{username},#{password},#{age},#{gender},#{phone})
</insert>
<insert id="insert2" useGeneratedKeys="true" keyProperty="id">
<selectKey keyProperty="id" resultType="java.lang.Integer" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
insert into
userinfo
(username, password, age, gender, phone)
values
(#{userinfo.username},#{userinfo.password},#{userinfo.age},
{userinfo.gender},#{userinfo.phone})
</insert>
删(Delete)
<delete id="delete">
delete from userinfo where id= #{id}
</delete>
删除成功。
改(Update)
<update id="update">
update userinfo
set password = #{param1}
where id = #{param2}
</update>
更新成功。
查(Select)
使用 XML 方式进行查询时,同样会面临数据封装的问题,因为数据库列名通常采用下划线分隔的命名方式,而 Java 对象的属性名可能采用驼峰命名法,导致查询结果字段与 Java 对象属性不匹配的情况。为了解决这个问题,可以采取以下方法:
- 起别名: 在 SQL 查询语句中,可以为查询的字段起别名,使其与 Java 对象的属性名称匹配。通过为查询结果字段设置别名,确保其与 Java 对象属性名一致,从而使得数据能够正确地封装到 Java 对象中。
- 结果映射: 可以在 XML 文件中定义结果映射,将数据库查询结果的字段与 Java 对象的属性进行映射。通过 <resultMap> 标签定义映射关系,将查询结果的字段与 Java 对象的属性进行一一映射,从而确保查询结果能够正确地映射到 Java 对象中。
- 开启驼峰命名: 可以通过 MyBatis 的配置来开启驼峰命名规则,使得数据库列名采用下划线分隔的命名方式,在映射到 Java 对象时自动转换为驼峰命名方式。这样就无需手动定义别名或者结果映射了,从而简化了配置,并且确保了查询结果能够正确地封装到 Java 对象的属性中。
最后一种方法毫无疑问是没有问题的,和注释那种方式一样,补充配置项即可。现在为了展示前两种方法,我们暂时注释掉配置项。
起别名:
我们先把 * 打开:
<select id="queryUserList" resultType="com.example.mybatisstudy.model.UserInfo">
select
id, username,password,gender,age,phone,
delete_flag,
create_time,
update_time
from userinfo
</select>
运行一下:
现在改别名:
<select id="queryUserList" resultType="com.example.mybatisstudy.model.UserInfo">
select
id, username,password,gender,age,phone,
delete_flag as deleteFlag,
create_time as createTime,
update_time as updateTime
from userinfo
</select>
结果映射:
<resultMap id="BaseMap" type="com.example.mybatisstudy.model.UserInfo">
<id property="id" column="id"></id>
<result property="deleteFlag" column="delete_flag"></result>
<result property="createTime" column="create_time"></result>
<result property="updateTime" column="update_time"></result>
</resultMap>
以上XML片段定义了一个名为 "BaseMap" 的结果映射(ResultMap),用于将数据库查询结果映射到 com.example.mybatisstudy.model.UserInfo 类型的Java对象。该 ResultMap 中包含了四个映射关系:
<id> 标签指定了对象的主键属性,其中 property 属性指定了Java对象中对应的属性名为 "id",column 属性指定了数据库表中对应的列名为 "id"。
<result> 标签用于指定普通字段的映射关系。在这里,有三个 <result> 标签,分别映射了 Java 对象中的 deleteFlag、createTime 和 updateTime 属性,以及数据库表中对应的列名。
每次映射并不一定都要带上主键属性。主键属性的映射是针对表中的主键字段,如果查询结果中包含了主键字段,那么就需要使用 <id> 标签来指定主键属性的映射关系。但并不是所有的查询操作都会涉及到主键字段,有些查询可能只需要普通字段的映射。
也就是说,在一个 <resultMap> 中,可以同时包含 <id> 标签和 <result> 标签,用于映射主键属性和普通字段属性。如果查询结果中包含了主键字段,则需要使用 <id> 标签来映射主键属性;如果查询结果中包含了其他普通字段,则可以使用 <result> 标签来映射这些普通字段属性。当然,你可以不写额外的 <result> 标签来映射这些普通字段属性。在这种情况下,MyBatis 将会自动进行基于名称的映射(因为我们上面写了type="com.example.mybatisstudy.model.UserInfo",所以大部分时候建议你还是写全每个映射,否则其他xml引用的时候,没有定义的那些列会映射失败),将查询结果中的普通字段与 Java 对象中的同名属性进行匹配,并自动设置值。
package com.example.mybatisstudy.mapper;
import com.example.mybatisstudy.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface UserInfoXmlMapper {
List<UserInfo>queryUserList();
List<UserInfo> queryUserList2();
Integer insert(UserInfo userInfo);
Integer delete(Integer id);
Integer update(String password,Integer id);
}
<?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.mybatisstudy.mapper.UserInfoXmlMapper">
<resultMap id="BaseMap" type="com.example.mybatisstudy.model.UserInfo">
<id property="id" column="id"></id>
<result property="deleteFlag" column="delete_flag"></result>
<result property="createTime" column="create_time"></result>
<result property="updateTime" column="update_time"></result>
</resultMap>
<select id="queryUserList" resultType="com.example.mybatisstudy.model.UserInfo">
select
id, username,password,gender,age,phone,
delete_flag as deleteFlag,
create_time as createTime,
update_time as updateTime
from userinfo
</select>
<select id="queryUserList2" resultMap="BaseMap">
select
id, username,password,gender,age,phone,
delete_flag,
create_time,
update_time
from userinfo
</select>
</mapper>
其他查询操作
多表查询
多表查询和单表查询类似,只是SQL不同而已。也就是说,多表查询和单表查询的区别在于涉及到的表的数量和查询条件的复杂程度。单表查询只涉及一个表,而多表查询涉及多个表,并且可能需要在查询条件中涉及到多个表之间的关联关系。
在实际业务中,相对于单表查询,多表查询的使用较少。这并不是说多表查询没有必要,而是出于对系统性能的考虑。多表查询通常会导致较慢的SQL查询,因为它需要在多个表之间进行联接操作,并且可能涉及大量的数据。
"慢 SQL" 是指执行时间较长的 SQL 查询或操作。这种情况通常发生在数据库查询需要处理大量数据或者查询语句本身效率较低的情况下。慢 SQL 可能导致系统性能下降,影响用户体验,并且可能会引起系统的各种性能问题,如响应延迟、资源竞争等。
如果业务对系统性能的要求不高,使用多表查询可能更加合适,因为它可以简化代码逻辑,并且可以通过SQL语句的优化来提高查询效率。
然而,如果业务对系统性能要求较高,通常会选择使用Java多线程来实现查询逻辑,尽可能避免多表查询。这样做的好处是可以更加灵活地控制查询逻辑,并且可以根据具体情况进行缓存、分页等性能优化操作,从而提高系统的响应速度和吞吐量。
准备工作
上面建了一张用户表,我们再来建一张文章表,进行多表关联查询:
-- 创建⽂章表
DROP TABLE IF EXISTS articleinfo;
CREATE TABLE articleinfo (
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 articleinfo ( title, content, uid ) VALUES ( 'Java', 'Java正⽂', 1
);
select * from articleinfo;
同时,我们回顾一下用户表长什么样子:
数据查询
接下来写联合查询的SQL语句:
该查询语句用于检索 articleinfo 表中 id 等于 1 的记录,并与 userinfo 表关联 id 进行左连接,获取关联用户的信息(username、age 和 gender)。
补充实体类:
package com.example.mybatisstudy.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;
}
接口定义:
先进行单表查询:
package com.example.mybatisstudy.mapper;
import com.example.mybatisstudy.model.ArticleInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface ArticleInfoMapper {
@Select("select * from articleinfo where id = #{id}")
ArticleInfo queryArticleInfo(Integer id);
}
接下来是多表查询:
你可以把多表看成是一张大的表,重新添加实体类:
package com.example.mybatisstudy.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;
private Integer gender;
}
package com.example.mybatisstudy.mapper;
import com.example.mybatisstudy.model.ArticleInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface ArticleInfoMapper {
@Select("select" +
" ta.*, tb.username, tb.age,tb.phone, tb.gender" +
" from articleinfo ta left join userinfo tb on ta.uid = tb.id" +
" where ta.id=#{id}")
ArticleInfo queryArticleInfo(Integer id);
}
同时,一定要注意,当SQL语句过长而换行的时候,语句拼接时非常容易遗漏或删去空格导致报错。
package com.example.mybatisstudy.mapper;
import com.example.mybatisstudy.model.ArticleInfo;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class ArticleInfoMapperTest {
@Autowired
private ArticleInfoMapper articleInfoMapper;
@Test
void queryArticleInfo() {
ArticleInfo articleInfo =articleInfoMapper.queryArticleInfo(1);
System.out.println(articleInfo);
}
}
#{ } 和 ${ }
MyBatis 参数赋值有两种方式,我们前面已经使用了 #{ } 进行赋值,接下来我们看下二者的区别。
#{} 和${} 使用
Interger类型的参数
@Select("select * from userinfo where id = #{id}")
UserInfo queryUserInfo(Integer userId);
观察我们打印的日志:
你会发现,我们输入的参数并没有在后面拼接,id的值是使用了 ? 作为参数占位符进行占位,实际的参数值则会在执行 SQL 查询时通过占位符传递给数据库。
这种SQL 我们称之为"预编译SQL"
"预编译 SQL" 是一种在执行 SQL 查询或更新操作时,使用参数占位符(通常是问号 ?)代替实际的参数值,并将参数值与 SQL 语句分开处理的技术。在预编译 SQL 中,SQL 语句中的具体值会被占位符取代,然后在执行时,会将实际的参数值传递给占位符,这样可以有效地提高 SQL 查询的效率和安全性。
你会不禁思考, ${} 呢? ${} 也时一样的吗?
现在我们就把 #{} 改成 ${} 再观察打印的日志:
@Select("select * from userinfo where id = ${id}")
UserInfo queryUserInfo2(Integer userId);
可以看到,这次的参数是直接拼接在SQL语句中了。
我们现在就来对比观察两个日志。
我们再看String类型的参数。
String类型的参数
@Select("select * from userinfo where username = #{name}")
UserInfo queryUserInfoByName(String name);
@Select("select * from userinfo where username = ${name}")
UserInfo queryUserInfoByName2(String username);
#{ }:
¥{ }:程序报错了。
这个错误是由于 SQL 语法错误引起的,具体错误信息是 Unknown column 'admin' in 'where clause',意思是在 WHERE 子句中找不到名为 'admin' 的列。
分析错误日志得出结论:
回想SQL语句,如果参数是字符串类型,直接拼接到 SQL 语句中会导致语法错误问题。因此,在 SQL 查询语句中我们通常都需要将字符串类型的参数用单引号或双引号括起来,以确保 SQL 语句的正确性和安全性:
SELECT * FROM userinfo WHERE username = 'admin';
在这个查询语句中,'admin' 是一个字符串参数,它被单引号括起来,表示它是一个字符串值而不是列名或其他 SQL 关键字。
当在 MyBatis 中使用参数时,#{} 和 ${} 是两种不同的方式来引用参数的值。
#{}:这种方式是预处理参数,会将参数值作为一个占位符传递给 SQL 查询语句,然后在执行 SQL 语句之前,MyBatis 会使用 PreparedStatement 的 setXXX() 方法将参数值设置到占位符中,以确保 SQL 的安全性和正确性。#{} 在 SQL 查询语句中使用时,参数值会被作为字符串、数字等类型处理,因此不需要手动添加引号。这种方式可以有效防止 SQL 注入等安全问题。
${}:这种方式是直接拼接参数值到 SQL 查询语句中,参数值会在 SQL 查询语句生成阶段被直接替换,不会经过预处理,因此存在安全风险。如果参数值是字符串类型,需要手动在 SQL 查询语句中添加引号,确保 SQL 语句的正确性。${} 可以用于动态拼接 SQL 语句的一部分,例如表名、列名等。但是由于直接拼接参数值到 SQL 语句中,容易受到 SQL 注入等安全问题的影响,因此在使用时需要格外小心。
我们把引号加上重新运行:
@Select("select * from userinfo where username = '${name}'")
UserInfo queryUserInfoByName2(String username);
深入理解 #{ } 和 ${ } 的区别(重要)
简单回顾:
当客户端发送一条 SQL 语句给服务器后,通常会经历以下大致流程:
-
解析语法和语义:服务器会首先对接收到的 SQL 语句进行语法和语义的解析,以确保 SQL 语句的正确性。在此阶段,服务器会检查 SQL 语句是否符合语法规范,并进行语义分析,确保 SQL 语句可以被正确理解和执行。
-
优化 SQL 语句:一旦 SQL 语句通过了语法和语义的校验,服务器会对 SQL 语句进行优化,制定执行计划。SQL 优化的目的是提高 SQL 查询的执行效率,减少查询所需的资源消耗。在这个阶段,服务器会根据数据库的索引、统计信息以及查询条件等因素,选择最优的执行方案。
-
执行并返回结果:经过语法和语义解析、SQL 优化后,服务器开始执行 SQL 语句,并返回执行结果。在执行过程中,服务器会根据 SQL 语句的类型(查询、插入、更新、删除等)以及所涉及的数据量等因素,调用相应的数据库引擎进行数据操作,并根据操作结果返回相应的响应给客户端。
一条 SQL 语句如果经历了以上流程的处理,即被正确解析、优化和执行,我们称之为 "Immediate Statements",即即时 SQL。这意味着 SQL 语句在执行过程中经过了完整的解析、优化和执行流程,确保了 SQL 查询的准确性和效率。
1. 预编译 SQL vs. 即时 SQL
预编译 SQL 和即时 SQL 是在处理 SQL 语句中参数传递方式上的两种不同策略,它们的主要区别在于参数值的处理方式和对 SQL 语句的编译和缓存方式:
-
预编译 SQL(#{}):
- 参数处理:预编译 SQL 使用 #{} 方式,即将参数值作为占位符传递给 SQL 查询语句,参数值会在执行 SQL 语句之前被预处理。
- 编译和缓存:在执行 SQL 语句之前,MyBatis 会对 SQL 语句进行编译并缓存起来,此时 SQL 语句中的参数值会被替换为占位符。下次再次执行相同的 SQL 语句时,只需要将参数值填入占位符,而不需要重新编译整个 SQL 语句,从而提高了查询效率。
-
即时 SQL(${}):
- 参数处理:即时 SQL 使用 ${} 方式,直接将参数值拼接到 SQL 查询语句中,而不经过预处理。
- 编译和缓存:每次执行 SQL 查询语句时,都会重新解析、编译 SQL 语句,然后执行查询,不会对 SQL 语句进行缓存。因此,即时 SQL 每次都需要重新编译整个 SQL 语句,效率相对较低。
2. 性能方面的考虑
- 预编译 SQL 的性能更高:预编译 SQL 对于反复调用或者参数值不同但 SQL 语句结构相同的场景具有明显的性能优势。因为预编译 SQL 缓存了编译后的 SQL 语句,而参数值的变化不会导致 SQL 语句的重新编译,只是简单地填充参数值,从而提高了查询效率。
所以,预编译 SQL 的性能更高的原因在于它能够避免重复的语法解析、SQL 优化和编译过程。
具体来说,预编译 SQL 具有以下性能优势:
-
减少重复的语法解析和优化过程:预编译 SQL 在第一次执行时会进行语法解析和 SQL 优化,并生成执行计划。然后,生成的执行计划会被缓存起来,以便后续执行相同结构的 SQL 语句时复用。这意味着即使是相同结构的 SQL 语句,只要参数值不同,也不需要重新进行语法解析和 SQL 优化,从而节省了大量的时间和系统资源。
-
减少数据库引擎的负担:SQL 数据库在执行 SQL 语句时需要将 SQL 语句解析为计算机能够理解的执行计划,并分配相应的执行资源。预编译 SQL 可以减少这一过程的重复执行,从而减轻了数据库引擎的负担,提高了数据库的整体性能和稳定性。
-
缓存执行计划:预编译 SQL 会将生成的执行计划缓存起来,以便后续相同结构的 SQL 语句能够直接使用缓存中的执行计划,而不需要重新生成。这样一来,查询相同结构的 SQL 语句时会更加高效,特别是在并发访问高、频繁执行相同结构 SQL 查询的场景下,能够明显提升数据库的响应速度和性能表现。
所以,预编译 SQL 能够通过缓存执行计划、减少重复的语法解析和 SQL 优化过程,从而提高数据库查询的效率和性能,特别是在需要反复执行相同结构 SQL 查询的场景下,能够明显降低系统资源的消耗,提升系统的稳定性和可靠性。
3. 安全性方面的考虑
- 防止 SQL 注入攻击:使用 #{} 的预处理方式可以有效防止 SQL 注入攻击。SQL 注入攻击是一种利用用户输入数据篡改 SQL 语句结构,以达到修改、删除、获取数据等恶意目的的攻击方式。由于预编译 SQL 的参数值被当作占位符处理,不会被解析为 SQL 语句的一部分,因此可以有效防止 SQL 注入攻击。预处理方式(#{})将参数值视为占位符,而不是直接将其拼接到 SQL 语句中。这意味着参数值不会被解析为 SQL 语句的一部分,从而避免了恶意用户利用参数输入来篡改 SQL 语句结构的可能性。即使用户输入了恶意的 SQL 代码,也不会对数据库的安全性造成威胁,因为参数值只会作为数据值而不会被解析为 SQL 语句的一部分。
- SQL 注入漏洞:相比之下,如果使用拼接方式(${})将参数值直接嵌入到 SQL 语句中,存在 SQL 注入漏洞的风险。恶意用户可以通过在参数中添加 SQL 关键字或语句来修改原始 SQL 语句的逻辑,执行恶意代码或获取敏感数据。这种情况下,应用程序很容易受到攻击,因为恶意用户可以通过构造特定的输入来绕过应用程序的验证和过滤,从而对数据库造成损害。这种情况下,预编译 SQL 的方式更加安全可靠。
所以,使用 #{} 的预处理方式更适合大多数情况,能够提高查询效率并且有效防止 SQL 注入攻击,而 ${} 的方式虽然灵活,但需要谨慎使用,尤其要注意防范 SQL 注入漏洞。
举个例子:刚刚的‘admin’,现在改为sql 注入代码: ' or 1 = '1'
@Test
void queryUserInfoByName2() {
log.info(userInfoMapper.queryUserInfoByName2("' or 1='1").toString());
}
这个异常通常是由于 MyBatis 的 selectOne 方法返回了多个结果,但实际期望只有一个结果或者为 null。具体来说,异常信息指示了期望返回一个结果或者 null,但实际返回了 4 个结果,这导致了异常的抛出。
可见SQL注入的可怕之处,稍作修改就得到所有的用户信息,更有甚者直接把程序搞崩溃……
综上所述,预编译 SQL 通过对 SQL 语句进行预处理和缓存,提高了查询效率和性能,特别适用于需要反复执行相同结构 SQL 查询的场景;而即时 SQL 则直接将参数值拼接到 SQL 查询语句中,每次执行都需要重新编译整个 SQL 语句,效率相对较低,适用于一次性的、动态变化较大的 SQL 查询场景。
模拟SQL注入完成用户登录
UserController:
/**
* 模拟SQL注入 完成用户登录
*/
@RequestMapping("/login")
public UserInfo login(String userName, String password){
//1. 根据用户名和密码去查询
return userService.queryUserByNameAndPassword(userName,password);
}
UserService:
public UserInfo queryUserByNameAndPassword(String name,String password) {
List<UserInfo> userInfos = userInfoMapper.queryUserByNameAndPassword(name, password);
if (userInfos.size()>0){
return userInfos.get(0);
}
return null;
}
UserInfoMapper:
@Select("select * from userinfo where username = '${name}' and password= '${password}'")
List<UserInfo> queryUserByNameAndPassword(String name, String password);
仍然拿到值:
排序功能
到这里大家会有一个疑问,既然#{}性能又高、又没有SQL注入问题,那么,是不是 ${} 就没有存在的必要性了呢?
当然不是!
#{} 在大多数情况下都是安全可靠的。然而,有一些情况下,使用 #{} 可能会有限制或者不能使用:
- 动态 SQL 拼接字段名或表名: 如果需要动态地拼接字段名或表名,#{} 是不能直接使用的,因为预处理参数只能用于值的替换,不能用于 SQL 语句的结构。在这种情况下,我们就需要使用 ${} 来实现动态拼接。
- 在 IN 子句中传递列表: 如果要在 SQL 的 IN 子句中传递一个列表,例如 select * from table where id in (#{idList}),这种情况下,#{} 是不能直接使用的,因为它会被解析为一个参数,而不是一个列表。在这种情况下,我们需要使用动态 SQL,并通过 ${} 来拼接列表的值。
- 排序方向动态传参: 如果需要动态地指定排序的方向,例如 order by column_name #{sortDirection},#{} 不能直接用于排序方向,因为它只能用于值的替换。在这种情况下,我们需要使用动态 SQL,并通过 ${} 来拼接排序方向。
在上面提到的情况下,使用 ${} 或者动态 SQL 来处理更加灵活。
先用#{}:
@Select("select * from userinfo order by id #{order}")
List<UserInfo> queryUserByOrder(String order);
这个错误是由于 SQL 语法错误引起的。根据错误信息,看起来是在执行 select * from userinfo order by id ? 这条 SQL 语句时出错了。
问题可能出在 order by id ? 这里。通常,在 order by 子句中,我们需要指定排序的字段名,然后跟着可选的 ASC 或 DESC 关键字表示升序或降序排序。然而,在这里就是因为 id ? 的问题。
假设 id ? 是想要动态地指定排序的方式,可以使用 ${} 或 #{} 来表示。如果是使用 #{} 会将参数作为预处理参数处理,面对String类型时,它会自动加上引号。但是这个地方加上引号显然是不对的,我们现在需要的是直接拼接。
@Select("select * from userinfo order by id ${order}")
List<UserInfo> queryUserByOrder(String order);
有些情况下 #{ } 不能使用,但是,使用 ${} 存在 SQL 注入的风险…… 我们这个时候应该怎么解决???
在一些情况下,既不能使用 #{},又存在使用 ${} 导致 SQL 注入的风险的情况下,可以考虑以下解决方案:
-
手动过滤和验证用户输入: 在使用 ${} 拼接 SQL 语句时,可以手动过滤和验证用户输入,确保输入的值不包含恶意的 SQL 代码。这可以通过使用正则表达式、内置的 SQL 注入检测函数或者自定义的输入验证函数来实现。
-
使用安全的 SQL 框架或 ORM 工具: 考虑使用具有防止 SQL 注入攻击功能的 SQL 框架或 ORM 工具,例如 Spring Data JPA、Hibernate 等。这些工具提供了更高级的参数化查询功能,可以帮助防止 SQL 注入攻击。
-
使用存储过程或者命名参数: 将 SQL 逻辑移到存储过程中,或者使用命名参数的方式执行 SQL 查询,这样可以在数据库层面进行参数化处理,减少 SQL 注入的风险。
-
严格限制用户输入的范围和格式: 在应用程序的前端或者后端,对用户输入的范围和格式进行严格的限制,只允许特定格式的输入,并在接收到用户输入时进行有效性验证和过滤。
-
使用安全的输入处理函数: 在拼接 SQL 语句时,使用数据库提供的安全输入处理函数,例如 MySQL 的 quote 函数或者 Java 中的 PreparedStatement 对象等,这些函数可以帮助过滤掉恶意的 SQL 代码。
综合考虑以上几种方案,我们可以根据具体情况选择合适的方法来防止 SQL 注入攻击,确保系统的安全性和稳定性。
like 查询(模糊查询)
like 使用 #{} 报错
我们先新增了几条数据。
@Select("select * from userinfo where username like %#{name}%")
List<UserInfo> queryUserByLike(String name);
@Select("select * from userinfo where username like '%${name}%'")
List<UserInfo> queryUserByLike(String name);
把 #{} 改成 ${} 可以正确查出来,但是${}存在SQL注入的问题,所以不能直接使用 ${}。
解决办法:使用 MySQL 的内置函数 concat() 来处理:
CONCAT 是 SQL 中的一个字符串函数,用于将多个字符串连接在一起。在 MySQL 中,CONCAT 函数接受一个或多个字符串作为参数,并返回这些字符串连接后的结果。如果参数中有 NULL 值,则 CONCAT 函数会将 NULL 视为一个空字符串。
@Select("select * from userinfo where username like CONCAT('%',#{name},'%')")
List<UserInfo> queryUserByLike(String name);
在这段代码中,使用了 CONCAT('%',#{name},'%') 这个 SQL 函数来构建一个模糊查询条件。CONCAT 是用于连接两个或多个字符串的函数,'%',#{name},'%') 就是将 % 符号与参数 name 前后拼接起来,形成一个包含 name 的模糊查询条件。
数据库连接池
在上⾯Mybatis的讲解中, 我们使用了数据库连接池技术,避免频繁的创建连接,销毁连接。
下面我们来了解下数据库连接池。
介绍
数据库连接池是应用程序和数据库之间的中间层,负责管理数据库连接的分配、使用和释放。其主要目的是允许应用程序重复利用现有的数据库连接,而不是每次都重新建立连接。
在数据库连接池中,应用程序可以从连接池中请求数据库连接,执行数据库操作,然后将连接返回给连接池以供重复利用。连接池会自动管理连接的分配和释放,并确保连接的可用性和有效性。
通过使用数据库连接池,可以降低连接数据库的成本和开销,提高系统的性能和并发能力,同时有效地管理数据库连接资源,避免资源泄漏和性能下降。
在没有使用数据库连接池的情况下,每次执行 SQL 语句都需要进行以下步骤:
- 创建一个新的数据库连接对象。
- 执行 SQL 语句。
- SQL 语句执行完毕后,关闭连接对象释放资源。
这种方式会导致频繁地创建和销毁连接对象,消耗大量资源,尤其在高并发的情况下,会增加系统的负担和性能开销。
而使用数据库连接池的情况下,程序启动时会预先在数据库连接池中创建一定数量的 Connection 对象。当客户端请求数据库连接时,连接池会从中获取一个空闲的 Connection 对象,然后执行 SQL 语句。SQL 语句执行完毕后,将 Connection 对象归还给连接池,而不是关闭它,以便下次重复利用。
数据库连接池的优点包括:
-
减少了网络开销:连接池中的连接对象可以被重复利用,避免了每次执行 SQL 都需要建立新的连接,从而减少了与数据库服务器之间的网络开销。因为建立连接通常需要经过网络传输,而连接池可以在应用程序启动时就创建好一定数量的连接,并在需要时分配给应用程序,因此可以减少连接建立和释放所带来的网络开销。
-
资源重用:连接池允许连接对象被重复利用,避免了频繁地创建和销毁连接,从而节省了系统资源。频繁地创建和销毁连接会消耗大量的系统资源,而连接池可以通过重复利用连接对象,减少资源的浪费,提高系统的效率和性能。
-
提升了系统的性能:通过减少连接的创建和销毁过程,以及有效地管理连接的分配和释放,数据库连接池可以提高系统的性能和吞吐量,同时降低了系统的资源消耗。连接池可以根据系统的负载动态调整连接的数量,确保系统能够快速响应请求并保持稳定运行。
-
提高了并发能力:连接池可以有效地管理连接的分配和释放,避免了因连接过多而导致系统崩溃的情况发生。通过合理配置连接池的参数,可以提高系统的并发能力,确保系统能够同时处理大量的请求,并且保持高效稳定的运行状态。
使用
常见的数据库连接池有:
-
C3P0:C3P0 是一个开源的 JDBC 连接池,提供了连接池管理、连接检查等功能,使用方便。但是相比其他连接池,C3P0 在高并发情况下的性能可能稍显逊色。
-
DBCP:DBCP 是 Apache 的 Jakarta 项目的一部分,也是一个开源的 JDBC 连接池。它支持连接池的基本功能,但相对来说比较老旧,性能可能不如其他连接池。
-
Druid:Druid 是阿里巴巴开源的数据库连接池项目,功能强大、性能优秀,在性能和功能上都有很好的表现。它提供了监控、防火墙、SQL 注入检测等高级功能,被广泛应用于各种 Java 项目中。
-
Hikari:Hikari 是一个轻量级、高性能的 JDBC 连接池,由其快速的初始化和低延迟的连接获取而闻名。Spring Boot 默认使用 Hikari 作为其数据源,因为它在性能和稳定性方面表现出色。
目前比较流行的是 Hikari 和 Druid:
-
Hikari:Spring Boot 默认使用的数据库连接池,以追求性能极致为目标,因其高性能而受到青睐。
-
Druid:阿里巴巴开源的数据库连接池项目,功能强大、性能优秀,提供了丰富的监控和安全功能,是许多企业和开发者的首选。如果想要切换默认的数据库连接池为 Druid,只需要引入相关依赖即可。总的来说,Druid 连接池在功能和性能方面都表现出色,是 Java 语言中最好的数据库连接池之一。
参考官方地址:druid/druid-spring-boot-starter at master · alibaba/druid · GitHub
学习文档:首页 · alibaba/druid Wiki · GitHub
两者对比可以参考:Hikaricp和Druid对比_数据库_晚风暖-华为云开发者联盟 (csdn.net)<dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.12</version> </dependency>
日志中显示了连接池的启动过程,Hikari 连接池的名称为 "HikariPool-1",并且连接池的启动已经完成。