目录
[1.#{} 和 ${}](#{} 和 ${})
[1.1#{} 和{} 使用](#{} 和{} 使用)
[1.2#{} 和 ${}区别](#{} 和 ${}区别)
[3.1 <if>标签](#3.1 <if>标签)
[3.2 <trim>标签](#3.2 <trim>标签)
承接上文详解MyBatis(二)
1.#{} 和 ${}
MyBatis的参数赋值有两种方式#{}和${},接下来我们通过使用观察两者区别
1.1#{} 和${} 使用
Integer类型参数
java
@Select("select username,`password`,age,gender,phone from userinfo where id = #{id}")
UserInfo queryById(Integer id);
观察我们打印的日志:
发现我们输出的SQL语句:
sql
select username, `password`, age, gender, phone from userinfo where id= ?
我们输出的参数并没有在后面拼接,id的值是使用 ? 进行占位,这种SQL我们称为**"预编译SQL"**
预编译 SQL 是一种数据库操作技术。简单来说,就是在执行 SQL 语句之前,先将包含参数占位符的 SQL 模板进行编译和准备 。之后在实际运行时,只需将具体的参数值传递进去进行绑定,而无需再次对整个 SQL 语句进行重新编译。
我们将#{}改成${}再观察打印的日志:
可以看到, 这次的参数是直接拼接在SQL语句中了.
String类型参数
java
@Select("select username,`password`,age,gender,phone from userinfo where username = #{username}")
UserInfo queryByName(String username);
观察我们打印的日志, 结果是正常返回
我们将#{}改成${}再观察打印的日志:
可以看到,这次的参数依然是直接拼接在SQL语句中了,但是字符串作为参数时 ,需要添加引号' '
使用${}未拼接引号导致程序报错
java
@Select("select username,`password`,age,gender,phone from userinfo where username = '${username}'")
UserInfo queryByName(String username);
再次运行程序:
结果正常返回
从上面两个例子可以看出:
#{} 使⽤的是预编译SQL, 通过 ?占位的⽅式, 提前对SQL进行编译, 然后把参数填充到SQL语句中,#{}会根据参数类型, 自动拼接引号' '
${}会直接进行字符替换, ⼀起对SQL进⾏编译. 如果参数为字符串, 需要加上引号' '
参数为数字类型时, 也可以加上, 查询结果不变, 但是可能会导致索引失效, 性能下降.
1.2#{} 和 ${}区别
#{} 和${} 的区别就是预编译SQL和即时SQL 的区别.
当发送⼀条SQL语句给服务器后, ⼤致流程如下:
解析语法和语义, 校验SQL语句是否正确
优化SQL语句, 制定执⾏计划
执⾏并返回结果
⼀条 SQL如果⾛上述流程处理, 我们称之为 Immediate Statements(即时SQL)
1.2.1#{}性能更高(预编译)
绝大多数情况下, 某⼀条 SQL 语句可能会被反复调用执行 , 或者每次执行的时候只有个别的值不同(比如 select 的 where子句值不同, update 的 set子句值不同, insert 的 values 值不同). 如果每次都需要经过上面的语法解析, SQL优化、SQL编译等,则效率就明显不行了.
预编译SQL ,编译⼀次之后会将编译后的SQL语句缓存起来 ,后面再次执行这条语句时,不会再次编译 (只是输入的参数不同), 省去了解析优化等过程, 以此来提高效率
1.2.2#{}更安全(防止SQL注入)
SQL 注入是一种常见的数据库安全漏洞。
它是指攻击者通过在应用程序与数据库交互时提交恶意构造的特定字符串或数据,来改变原本 SQL 语句的逻辑和意图,从而执行非预期的操作,比如获取未经授权的数据、修改数据、执行任意系统命令等。
由于没有对用户输⼊进行充分检查 ,而SQL⼜是拼接⽽成 ,在用户输⼊参数时 ,在参数中添加⼀些 SQL关键字 ,达到改变SQL运行结果的目的 ,也可以完成恶意攻击。
SQL注入实例:
java
@Select("select username,`password`,age,gender,phone from userinfo where username = '${username}'")
UserInfo queryByName(String username);
测试代码:
正常访问情况:
java
@Test
void queryByName() {
List<UserInfo> userInfos = userInfoMapper.queryByName("Romised");
System.out.println(userInfos.toString());
}
结果运行正常:
**SQL注入场景:**先用一个单引号与前面的单引号闭合,然后再重新构建一个判断条件
java
@Test
void queryByName() {
List<UserInfo> userInfos = userInfoMapper.queryByName("' or 1='1");
System.out.println(userInfos.toString());
}
结果依然被正确查询出来了, 其中参数 or被当做了SQL语句的⼀部分
将${}改成#{},再次运行程序
可以发现,编译器将一长串数据都当做了String,所以查询结果为0,防止了SQL注入
如果发生在登录场景,则有可能判断为正确(具体看代码实现)
1.3排序功能
从上面的例⼦中, 可以得出结论: ${} 会有SQL注入的风险, 所以我们尽量使用#{}完成查询
既然如此, 是不是 ${} 就没有存在的必要性了呢? 当然不是.
接下来我们看下${}的使用场景
Mapper实现
java
@Select("select id,username,`password`,age,gender,phone from userinfo order by id ${sort}")
List<UserInfo> queryAllUserBySort(String sort);
注意:此时sort参数为String类型,但是SQL语句中,排序规则不需要加引号 ' ' ,所以此时的sort也不加引号
运行结果正常:
将${}改成#{}再次运行:
可以发现,当使用#{sort}查询时,desc前后自动加了引号导致SQL错误
#{}会根据参数类型判断是否拼接引号,如果是String则会自动添加引号 ' '
除此之外, 还有表名作为参数时, 也只能使用 ${}
1.4like查询
使用 #{}
java
@Select("select id,username,`password`,age,gender,phone from userinfo where username like #{%like%}")
List<UserInfo> queryAllUserByLike(String like);
运行结果:无法正确进行查询
使用${}进行like查询:
java
@Select("select id,username,`password`,age,gender,phone from userinfo where username like '%${like}%'")
List<UserInfo> queryAllUserByLike(String like);
运行结果:查询正确
把 #{} 改成 {} 可以正确查出来, 但是{}存在SQL注⼊的问题, 所以不能直接使⽤ ${}.
解决办法: 使⽤ mysql 的内置函数 concat() 来处理 ,实现代码如下:
java
@Select("select id,username,`password`,age,gender,phone from userinfo where username like concat('%',#{key},'%')")
List<UserInfo> queryAllUserByLike(String like);
2.数据库连接池
在上⾯Mybatis的讲解中, 我们使⽤了数据库连接池技术, 避免频繁的创建连接, 销毁连接
2.1连接池介绍
数据库连接池负责分配、管理和释放数据库连接 ,它允许应用程序重复使用⼀个现有的数据库连接, 而不是再重新建⽴⼀个.
没有使⽤数据库连接池的情况: 每次执行SQL语句, 要先创建⼀个新的连接对象 , 然后执行SQL语句, SQL 语句执行完, 再关闭连接对象释放资源. 这种重复的创建连接****, 销毁连接比较消耗资源
使用数据库连接池的情况: 程序启动时, 会在数据库连接池中创建⼀定数量的Connection对象, 当客户请求数据库连接池, 会从数据库连接池中获取****Connection对象, 然后执行SQL, SQL语句执行完, 再把Connection归还给连接池.
数据库连接池具有以下优点:
- 提高性能:避免频繁创建和销毁数据库连接的开销,显著提升系统对数据库操作的响应速度
- 资源有效利用:合理管理连接资源,防止过多无效的连接占用系统资源。
- 稳定性增强:通过对连接的有效管理和监控,减少因连接问题导致的系统不稳定情况。
- 可配置性强:可以根据实际需求灵活调整连接池的参数,如连接数、超时时间等,以适应不同的应用场景和负载情况。
2.2更换连接池
常见的数据库连接池:
1.DBCP(DataBase Connection Pool):是 Apache 软件基金组织下的开源连接池实现
2.C3P0:一个被广泛使用的连接池,配置相对灵活。
3.Hikari:以高性能著称。
4.Druid:功能较为强大,提供了丰富的监控和统计功能
Hikari : SpringBoot默认使⽤的数据库连接池:
如果我们想把默认的数据库连接池切换为Druid数据库连接池, 只需要引⼊相关依赖即可
引入Druid:
XML
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>
• Druid连接池是阿⾥巴巴开源的数据库连接池项⽬
• 功能强大,性能优秀,是Java语⾔最好的数据库连接池之⼀
3.动态SQL
动态 SQL 是Mybatis的强⼤特性之⼀ ,能够完成不同条件下不同的 sql 拼接。
3.1 <if>标签
在注册用户的时候 ,可能会有这样⼀个问题 ,如下图所示:
注册分为两种字段:必填字段和非必填字段 ,那如果在添加用户的时候有不确定的字段传入
这时就需要使用动态标签来判断了,如添加的时候性别gender 为⾮必填字段,具体实现如下:
接口定义:
java
Integer insertUserByCondition(UserInfo userInfo);
Mapper.xml实现:
XML
<insert id="insertUserByCondition">
INSERT INTO userinfo (
username,
`password`,
age,
<if test="gender != null">
gender,
</if>
phone)
VALUES (
#{username},
#{age},
<if test="gender != null">
#{gender},
</if>
#{phone})
</insert>
或者使用注解方式(不推荐)
把上面的SQL(包括标签),使用<script></script>标签括起来就可以
java
@Insert("<script>" +
"INSERT INTO userinfo (username,`password`,age," +
"<if test='gender!=null'>gender,</if>" +
"phone)" +
"VALUES(#{username},#{age}," +
"<if test='gender!=null'>#{gender},</if>" +
"#{phone})"+
"</script>")
Integer insertUserByCondition(UserInfo userInfo);
注意 test 中的gender ,是传⼊对象中的属性 ,不是数据库字段
Q: 可不可以不进⾏判断, 直接把字段设置为null呢?
A: 不可以, 这种情况下, 如果gender字段有默认值, 就会设置为默认值
上述案例通过<if>标签判定个别传入数据是否为空,如果不为空的语句是:
java
insert into userinfo (username,`password`,age,gender,`phone`)
values (#{username},#{password},#{age},#{gender},#{phone});
如果gender和phone传入对象为空,则语句变成:
java
insert into userinfo (username,`password`,age)
values (#{username},#{password},#{age});
**注意:**编写代码时要注意逗号等位置,避免某个元素为空时逗号多余
3.2 <trim>标签
在 MyBatis 中,<trim>标签主要用于对 SQL 语句进行灵活的拼接和处理。
标签中有如下属性:
• prefix:表示整个语句块 ,以prefix的值作为前缀
• suffix:表示整个语句块 ,以suffix的值作为后缀
• prefixOverrides:表示整个语句块要去除掉的前缀
• suffixOverrides:表示整个语句块要去除掉的后缀
调整 Mapper.xml 的插⼊语句为:
XML
<insert id="insertUserByCondition">
INSERT INTO userinfo
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="username !=null">
username,
</if>
<if test="password !=null">
`password`,
</if>
<if test="age != null">
age,
</if>
<if test="gender != null">
gender,
</if>
<if test="phone != null">
phone,
</if>
</trim>
VALUES
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="username !=null">
#{username},
</if>
<if test="password !=null">
#{password},
</if>
<if test="age != null">
#{age},
</if>
<if test="gender != null">
#{gender},
</if>
<if test="phone != null">
#{phone}
</if>
</trim>
</insert>
或者使用注解方式(不推荐)
java
@Insert("<script>" +
"INSERT INTO userinfo " +
"<trim prefix='(' suffix=')' suffixOverrides=','>" +
"<if test='username!=null'>username,</if>" +
"<if test='password!=null'>password,</if>" +
"<if test='age!=null'>age,</if>" +
"<if test='gender!=null'>gender,</if>" +
"<if test='phone!=null'>phone,</if>" +
"</trim>" +
"VALUES " +
"<trim prefix='(' suffix=')' suffixOverrides=','>" +
"<if test='username!=null'>#{username},</if>" +
"<if test='password!=null'>#{password},</if>" +
"<if test='age!=null'>#{age},</if>" +
"<if test='gender!=null'>#{gender},</if>" +
"<if test='phone!=null'>#{phone}</if>" +
"</trim>"+
"</script>")
Integer insertUserByCondition(UserInfo userInfo);
在以上sql动态解析时,会做如下处理:
基于prefix配置,开始部分加上左括号( 基于suffix配置,结束部分加上有括号) 基于suffixOverrides配置,去掉最后一个逗号,
3.3<where>标签
看下面这个场景, 系统会根据我们的筛选条件, 动态组装where 条件
需求: 传入的用户对象 ,根据属性做where条件查询
原有SQL:
sql
SELECT
*
FROM
userinfo
WHERE
age = 18
AND gender = 1
AND delete_flag =0;
接口定义:
java
List<UserInfo> queryByCondition();
Mapper.xml实现:
XML
<select id="queryByCondition" resultType="com.example.demo.model.UserInfo">
select id, username, age, gender, phone, delete_flag, create_time, update_ti
from userinfo
<where>
<if test="age != null">
and age = #{age}
</if>
<if test="gender != null">
and gender = #{gender}
</if>
<if test="deleteFlag != null">
and delete_flag = #{deleteFlag}
</if>
</where>
</select>
注解方式实现:
java
@Select("<script>select id, username, age, gender, phone, delete_flag, create_ti
" from userinfo" +
" <where>" +
" <if test='age != null'> and age = #{age} </if>" +
" <if test='gender != null'> and gender = #{gender} </if>" +
" <if test='deleteFlag != null'> and delete_flag = #{deleteFlag}
" </where>" +
"</script>")
List<UserInfo> queryByCondition(UserInfo userInfo);
<where>标签只会在⼦元素有内容的情况下才插入where⼦句 ,而且会⾃动去除⼦句的开头的AND或OR
3.4<set>标签
需求: 根据传入的用户对象属性来更新⽤户数据 ,可以使用标签来指定动态内容.
接⼝定义: 根据传入的用户id 属性 ,修改其他不为 null 的属性
java
Integer updateUserByCondition(UserInfo userInfo);
Mapper.xml实现:
XML
<update id="updateUserByCondition">
update userinfo
<set>
<if test="username != null">
username = #{username},
</if>
<if test="age != null">
age = #{age},
</if>
<if test="deleteFlag != null">
delete_flag = #{deleteFag},
</if>
</set>
where id = #{id}
</update>
注解方式实现:
java
@Update("<script>" +
"update userinfo " +
"<set>" +
"<if test='username!=null'>username=#{username},</if>" +
"<if test='age!=null'>age=#{age},</if>" +
"<if test='deleteFlag!=null'>delete_flag=#{deleteFlag},</if>" +
"</set>" +
"where id=#{id}" +
"</script>")
Integer updateUserByCondition(UserInfo userInfo);
<set>标签主要用于动态生成 UPDATE 语句中的 SET 子句。它可以根据条件动态地设置需要更新的字段,并且能够自动处理字段值为空或不合法的情况,避免错误地更新到数据库中。
3.5<foreach>标签
对集合进行遍历时可以使⽤该标签。标签有如下属性:
• collection :绑定⽅法参数中的集合 ,如 List ,Set ,Map或数组对象
• item :遍历时的每⼀个对象
• open :语句块开头的字符串
• close:语句块结束的字符串
• separator:每次遍历之间间隔的字符串
需求: 根据多个userid, 删除用户数据
接口方法:
java
void deleteByIds(List<Integer> ids);
ArticleMapper.xml 中新增删除 sql:
XML
<delete id="deleteByIds">
delete from userinfo
where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</delete>
或者使用注解方式:
java
@Delete("<script>" +
"delete from userinfo where id in" +
"<foreach collection='ids' item='id' separator=',' open='(' close=')
"#{id}" +
"</foreach>" +
"</script>")
Integer deleteUser(Integer id);
3.6<include>标签
问题分析:xml映射文件中配置的SQL ,可能存在很多重复的片段 ,此时就会存在很多冗余的代码
我们可以对重复的代码片段进行抽取,将其通过<sql>标签封装到一个SQL片段,然后再通过<include>标签进行引用
<sql>: 定义可重用的SQL片段
<include>:通过属性refid,制定包含的SQL片段
通过<include>标签在原来抽取的地方进行引用,操作如下:
XML
<select id="queryAllUser" resultMap="BaseMap">
select
<include refid="allColumn"></include>
from userinfo
</select>
<select id="queryById" resultType="com.example.demo.model.UserInfo">
select
<include refid="allColumn"></include>
from userinfo where id= #{id}
</select>