1.#{} 和 ${}的使用
1.1数据准备
1.1.1.MySQL数据准备
(1)创建数据库:
CREATE DATABASE mybatis_study DEFAULT CHARACTER SET utf8mb4;
(2)使用数据库
-- 使⽤数据数据
USE mybatis_study;
(3)创建用户表
-- 创建表[⽤⼾表]
CREATE TABLE `user_info` (
`id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
`username` VARCHAR ( 127 ) NOT NULL,
`password` VARCHAR ( 127 ) NOT NULL,
`age` TINYINT ( 4 ) NOT NULL,
`gender` TINYINT ( 4 ) DEFAULT '0' COMMENT '1-男 2-⼥ 0-默认',
`phone` VARCHAR ( 15 ) DEFAULT NULL,
`delete_flag` TINYINT ( 4 ) DEFAULT 0 COMMENT '0-正常, 1-删除',
`create_time` DATETIME DEFAULT now(),
`update_time` DATETIME DEFAULT now() ON UPDATE now(),
PRIMARY KEY ( `id` )
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4;
(4)添加用户信息
-- 添加⽤⼾信息
INSERT INTO mybatis_study.user_info( username, `password`, age, gender, phone )
VALUES ( 'admin', 'admin', 18, 1, '18612340001' );
INSERT INTO mybatis_study.user_info( username, `password`, age, gender, phone )
VALUES ( 'zhangsan', 'zhangsan', 18, 1, '18612340002' );
INSERT INTO mybatis_study.user_info( username, `password`, age, gender, phone )
VALUES ( 'lisi', 'lisi', 18, 1, '18612340003' );
INSERT INTO mybatis_study.user_info( username, `password`, age, gender, phone )
VALUES ( 'wangwu', 'wangwu', 18, 1, '18612340004' );
1.1.2.创建对应的实体类
实体类的属性名与表中的字段名⼀⼀对应
@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;
}
注意:在实际开发中不管什么实体类都要设置删除标志、创建时间、修改时间
1.2 获取Integer类型
1.2.1 #{}
Mapper接口:
@Mapper
public interface UserInfoMapper {
// 获取参数中的 UserId
@Select("select * from user_info where id = #{userId} ")
UserInfo queryById(@Param("userId") Integer id);
测试代码:
@Slf4j
@SpringBootTest //启动Sring 容器
class UserInfoMapperTest {
@Test
void queryById() {
UserInfo result = userInfoMapper.queryById(8);
log.info(result.toString());
}
}
运行结果:
通过日志可以发现,?
进行占位,传的参数进行绑定到占位符。
1.2.2 ${}
Mapper接口:
@Mapper
public interface UserInfoMapper {
// 获取参数中的 UserId
@Select("select * from user_info where id = ${userId} ")
UserInfo queryById(@Param("userId") Integer id);
测试代码:
@Slf4j
@SpringBootTest //启动Sring 容器
class UserInfoMapperTest {
@Test
void queryById() {
UserInfo result = userInfoMapper.queryById(8);
log.info(result.toString());
}
}
运行结果:
通过日志可以发现,SQL命令是完整的,因为,该方法是把字符串拼接在一起执行的。
1.3 获取String类型
1.3.1 #{}
Mapper接口:
@Mapper
public interface UserInfoMapper {
// 获取参数中的 username
@Select("select * from user_info where username = #{username} ")
List<UserInfo> queryByUsername( String username);
测试代码:
@Slf4j
@SpringBootTest //启动Sring 容器
class UserInfoMapperTest {
@Autowired
private UserInfoMapper userMapper;
@Test
void queryByUsername() {
userMapper.queryByUsername("lisi");
}
}
运行结果:
通过日志可以发现,?
进行占位,传的参数进行绑定到占位符。
1.3.2 ${}
Mapper接口:
@Mapper
public interface UserInfoMapper {
// 获取参数中的 username
@Select("select * from user_info where username = ${username} ")
List<UserInfo> queryByUsername( String username);
测试代码:
@Slf4j
@SpringBootTest //启动Sring 容器
class UserInfoMapperTest {
@Autowired
private UserInfoMapper userMapper;
@Test
void queryByUsername() {
userMapper.queryByUsername("lisi");
}
}
运行结果:报错
从SQL语句中明显的看到WHERE username 后面的字符串没有引号,导致报错。
因为,${}直接把字符内容直接放进SQL语句中而没有加单引号。
修改后的mapper接口:
@Mapper
public interface UserInfoMapper {
// 获取参数中的 username
@Select("select * from user_info where username = '${username}' ")
UserInfo queryByUsername( String username);
2.#{} 和 ${}的区别
2.1 预编译SQL和即时SQL的执行过程
2.1.1 预编译SQL执行过程
#{}是预编译SQL。
第一步:数据库客户端(如 JDBC 驱动)将 SQL 模板发送到数据库服务器。
// SQL模版
PreparedStatement pstmt = connection.prepareStatement("SELECT * FROM user WHERE id = ? AND name = ?");
第二步:SQL 预编译
(1)数据库解析 SQL 模板,生成执行计划(包括语法检查、语义分析、优化等),并缓存该计划。
(2)此时,占位符 ? 的具体值尚未填充,数据库只处理 SQL 的结构。
第三步:客户端通过 PreparedStatement 的方法设置参数值
//参数值以二进制形式单独发送到数据库,不会直接拼接到 SQL 中,避免了 SQL 注入
pstmt.setInt(1, 123); // 绑定 id
pstmt.setString(2, "Alice"); // 绑定 name
第四步:SQL 执行
(1)数据库使用缓存的执行计划,将绑定参数代入执行计划,直接运行查询或更新操作。
(2)如果相同的 SQL 模板再次执行(仅参数不同),数据库可复用缓存的执行计划,减少编译开销。
2.1.2 即时QL执行过程
${}是即时SQL。
第一步:SQL 语句拼接
SELECT * FROM user ORDER BY ${columnName}
//如果 columnName = "age"
//生成
SELECT * FROM user ORDER BY age; DROP TABLE user;
第二步:SQL 发送到数据库
客户端将拼接好的完整 SQL 字符串通过 Statement 或类似接口发送到数据库
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql);
第三步:SQL解析与编译
语法解析:检查 SQL 语句的语法是否正确。
语义分析:验证表名、列名等是否存在,权限是否足够。
优化:生成执行计划,选择最优的查询路径。
第四步:SQL执行
数据库根据生成的执行计划执行 SQL,完成查询或更新操作。
2.2性能比较
预编译SQL(#{})性能更高:
绝⼤多数情况下, 某⼀条 SQL 语句可能会被反复调⽤执⾏, 或者每次执⾏的时候只有个别的值不同(⽐如 select 的 where ⼦句值不同, update 的 set ⼦句值不同, insert 的 values 值不同). 如果每次都需要经过上⾯的语法解析, SQL优化、SQL编译等,则效率就明显不⾏了
预编译SQL,编译⼀次之后会将编译后的SQL语句缓存起来,后⾯再次执⾏这条语句时,不会再次编译 (只是输⼊的参数不同), 省去了解析优化等过程, 以此来提⾼效率
预编译SQL(#{})更安全(防⽌SQL注⼊):
由于没有对⽤⼾输⼊进⾏充分检查,⽽SQL⼜是拼接⽽成,在⽤⼾输⼊参数时,在参数中添加⼀些 SQL关键字,达到改变SQL运⾏结果的⽬的,也可以完成恶意攻击。
2.3 排序举例
排序需要用到SQL的关键字asc
和desc
,把该两个关键字设置为参数时需要用到${}
,因为#{}
会把asc
和desc
认为是字符串
2.3.1 #{}
Mapper接口:
@Mapper
public interface UserInfoMapper {
@Select("select * from userInfo order by username #{flag}")
List<UserInfo> findAll(String flag);
}
测试代码
@Slf4j
@SpringBootTest //启动Sring 容器
class UserInfoMapperTest {
@Autowired
private UserInfoMapper userMapper;
@Test
void findAll() {
userMapper.findAll("asc");
}
}
运行结果:
2.3.2 #{}
Mapper接口:
@Mapper
public interface UserInfoMapper {
@Select("select * from userInfo order by username ${flag}")
List<UserInfo> findAll(String flag);
}
测试代码
@Slf4j
@SpringBootTest //启动Sring 容器
class UserInfoMapperTest {
@Autowired
private UserInfoMapper userMapper;
@Test
void findAll() {
userMapper.findAll("asc");
}
}
运行结果:
2.4 like 查询
2.4.1 #{}
Mapper接口:
@Mapper
public interface UserInfoMapper {
@Select("select * from user_info where username like '%#{s}%'")
List<UserInfo> queryLike(String s);
}
测试代码:
@Slf4j
@SpringBootTest //启动Sring 容器
class UserInfoMapperTest {
@Autowired
private UserInfoMapper userMapper;
@Test
void queryLike() {
String s = "6";
userMapper.queryLike(s);
}
}
运行结果:
把 #{} 改成 可以正确查出来 , 但是 {} 可以正确查出来, 但是 可以正确查出来,但是{}存在SQL注⼊的问题, 所以不能直接使⽤ ${}.解决办法: 使⽤ mysql 的内置函数 concat() 来处理,实现代码如下:
修改后的Mapper接口:
@Mapper
public interface UserInfoMapper {
@Select("select * from user_info where username like concat('%',#{s},'%') ")
List<UserInfo> queryLike(String s);
运行结果:
2.4.2 ${}
Mapper接口:
@Mapper
public interface UserInfoMapper {
@Select("select * from user_info where username like '%${s}%' ")
List<UserInfo> queryLike(String s);
}
测试代码:
@Slf4j
@SpringBootTest //启动Sring 容器
class UserInfoMapperTest {
@Autowired
private UserInfoMapper userMapper;
@Test
void queryLike() {
String s = "6";
userMapper.queryLike(s);
}
}
运行结果:
3.什么是SQL注入?
SQL注⼊:是通过操作输⼊的数据来修改事先定义好的SQL语句,以达到执⾏代码对服务器进⾏攻击的⽅法。
举例:
下面定义的接口是由username得到该username的信息
Mapper接口:
@Mapper
public interface UserInfoMapper {
// 获取参数中的 username
@Select("select * from user_info where username = ${username} ")
List<UserInfo> queryByUsername( String username);
可以通过输入' or username='
来获取该表中所有人的信息
测试代码:
@Slf4j
@SpringBootTest //启动Sring 容器
class UserInfoMapperTest {
@Autowired
private UserInfoMapper userMapper;
@Test
void queryByUsername() {
userMapper.queryByUsername("lisi");
}
}
运行结果:
可以看出来, 查询的数据越界了接口的定义。所以⽤于查询的字段,尽量使⽤#{}
预查询的⽅式
SQL注⼊是⼀种⾮常常⻅的数据库攻击⼿段, SQL注⼊漏洞也是⽹络世界中最普遍的漏洞之⼀。
4.数据库连接池
4.1介绍
数据库连接池负责分配、管理和释放数据库连接,它允许应⽤程序重复使⽤⼀个现有的数据库连接,⽽不是再重新建⽴⼀个.
没有使⽤数据库连接池的情况: 每次执⾏SQL语句, 要先创建⼀个新的连接对象, 然后执⾏SQL语句, SQL语句执⾏完, 再关闭连接对象释放资源. 这种重复的创建连接, 销毁连接⽐较消耗资源
使⽤数据库连接池的情况: 程序启动时, 会在数据库连接池中创建⼀定数量的Connection对象, 当客⼾请求数据库连接池, 会从数据库连接池中获取Connection对象, 然后执⾏SQL, SQL语句执⾏完, 再把 Connection归还给连接池.
优点:
1.减少了⽹络开销
2.资源重⽤
3.提升了系统的性能
4.26.2使⽤
常⻅的数据库连接池:
•C3P0
•DBCP
•Druid
•Hikari
⽬前⽐较流⾏的是 Hikari, Druid
Hikari : SpringBoot默认使⽤的数据库连接池
Hikari 是⽇语"光"的意思(ひかり), Hikari也是以追求性能极致为⽬标
2.Druid
如果我们想把默认的数据库连接池切换为Druid数据库连接池, 只需要引⼊相关依赖即可
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>1.2.21</version>
</dependency>
如果SpringBoot版本为2.X, 使⽤druid-spring-boot-starter 依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>
Druid连接池是阿⾥巴巴开源的数据库连接池项⽬
功能强⼤,性能优秀,是Java语⾔最好的数据库连接池之⼀