引言
使⽤Mybatis的注解⽅式,主要是来完成⼀些简单的增删改查功能. 如果需要实现复杂的SQL功能,建议使⽤XML来配置映射语句,也就是将SQL语句写在XML配置⽂件中.
之前,我们学习了,用注解的方式来实现MyBatis
接下来我们学习XML的⽅式
MyBatis XML的⽅式需要以下两步
- 配置数据库连接字符串和MyBatis
- 写持久层代码
MyBatis XML配置⽂件
配置连接字符串和MyBatis
此步骤需要进⾏两项设置,数据库连接字符串设置和 MyBatis 的 XML ⽂件配置。
# 数据库连接配置
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 ⽂件
//mapper是目录,自己设置
//文件名的后缀部分必须是Mapper.xml不能修改,前面可以添加内容
//这里的*代表通配符
mybatis.mapper-locations=classpath:/myBatis/*Mapper.xml
mybatis:
mapper-locations: classpath:mapper/**Mapper.xml
写持久层代码
持久层代码分两部分
- ⽅法定义 Interface
- ⽅法实现: XXX.xml
添加 mapper 接⼝
数据持久层的接⼝定义:
import com.example.demo.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface UserInfoXMlMapper {
List<UserInfo> queryAllUser();
}
添加 UserInfoXMLMapper.xml
在resources目录下添加前面在配置文件里面写的xml路径
在xml文件里面添加如下内容:
其中,mapper标签里面的namespace属性里面填的是 mapper接口的路径
<?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.demo.mapper.UserMapper">
</mapper>
查询所有⽤⼾的具体实现 :
<?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.demo.mapper.UserInfoXMlMapper">
<select id="queryAllUser" resultType="com.example.demo.model.UserInfo">
select username,`password`, age, gender, phone from userinfo
</select>
</mapper>
单元测试
@SpringBootTest
class UserInfoMapperTest {
@Autowired
private UserInfoMapper userInfoMapper;
@Test
void queryAllUser() {
List<UserInfo> userInfoList = userInfoMapper.queryAllUser();
System.out.println(userInfoList);
}
}
增删改查操作
增(Insert)
UserInfoMapper接⼝
Integer insertUser(UserInfo userInfo);
UserInfoMapper.xml实现
<insert id="insertUser">
insert into userinfo (username, `password`, age, gender, phone) values (#
{username}, #{password}, #{age},#{gender},#{phone})
</insert>
如果使⽤@Param设置参数名称的话, 使⽤⽅法和注解类似UserInfoMapper接⼝:
Integer insertUser(@Param("userinfo") UserInfo userInfo);
UserInfoMapper.xml实现:
<insert id="insertUser">
insert into userinfo (username, `password`, age, gender, phone) values
(#{userinfo.username},#{userinfo.password},#{userinfo.age},#
{userinfo.gender},#{userinfo.phone})
</insert>
返回⾃增 id
接⼝定义不变, Mapper.xml 实现 设置useGeneratedKeys 和keyProperty属性
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
insert into userinfo (username, `password`, age, gender, phone) values
(#{userinfo.username},#{userinfo.password},#{userinfo.age},#
{userinfo.gender},#{userinfo.phone})
</insert
删(Delete)
UserInfoMapper接⼝:
Integer deleteUser(Integer id);
UserInfoMapper.xml实现:
<delete id="deleteUser">
delete from userinfo where id = #{id}
</delete>
改(Update)
UserInfoMapper接⼝:
Integer updateUser(UserInfo userInfo);
UserInfoMapper.xml实现:
<update id="updateUser">
update userinfo set username=#{username} where id=#{id}
</update>
查(Select)
同样的, 使⽤XML 的⽅式进⾏查询, 也存在数据封装的问题我们把SQL语句进⾏简单修改, 查询更多的字段内容.
<select id="queryAllUser" resultType="com.example.demo.model.UserInfo">
select id, username,`password`, age, gender, phone, delete_flag,
create_time, update_time from userinfo
</select>
运⾏结果:
![](https://i-blog.csdnimg.cn/direct/5a6231f6ebc24a41a24b70cebd9a0328.png)
解决办法和注解类似:
- 起别名
- 结果映射
- 开启驼峰命名
其中1,3的解决办法和注解⼀样,不再多说, 接下来看下xml如果来写结果映射
Mapper.xml
<resultMap id="BaseMap" type="com.example.demo.model.UserInfo">
<id column="id" property="id"></id>
<result column="delete_flag" property="deleteFlag"></result>
<result column="create_time" property="createTime"></result>
<result column="update_time" property="updateTime"></result>
</resultMap>
<select id="queryAllUser" resultMap="BaseMap">
select id, username,`password`, age, gender, phone, delete_flag,
create_time, update_time from userinfo
</select>
#{} 和 ${}
MyBatis 参数赋值有两种⽅式, 咱们前⾯使⽤了
#{}
进⾏赋值, 接下来我们看下⼆者的区别
#{} 和${} 使⽤
先看Interger类型的参数
@Select("select username, `password`, age, gender, phone from userinfo where
id= #{id} ")
UserInfo queryById(Integer id);
观察我们打印的⽇志
![](https://i-blog.csdnimg.cn/direct/3494b629e03b40f3b80fd642e5006599.png)
发现我们输出的SQL语句:
select username, `password`, age, gender, phone from userinfo where id= ?
我们输⼊的参数并没有在后⾯拼接,id的值是使⽤ ? 进⾏占位. 这种SQL 我们称之为"预编译SQL"
我们把 #{} 改成 ${} 再观察打印的⽇志:
@Select("select username, `password`, age, gender, phone from userinfo where
id= ${id} ")
UserInfo queryById(Integer id);
可以看到, 这次的参数是直接拼接在SQL语句中了.
接下来我们再看String类型的参数
@Select("select username, `pasword`, age, gender, phone from userinfo where
username= #{name} ")
UserInfo queryByName(String name);
观察我们打印的⽇志, 结果正常返回
![](https://i-blog.csdnimg.cn/direct/6c8d6bfd49b3468dbd9c656efa0339d5.png)
我们把 #{} 改成 ${} 再观察打印的⽇志
@Select("select username, `password`, age, gender, phone from userinfo where
username= ${name} ")
UserInfo queryByName(String name);
可以看到, 这次的参数依然是直接拼接在SQL语句中了, 但是字符串作为参数时, 需要添加引号
''
, 使
⽤
${}
不会拼接引号
''
, 导致程序报错.
修改代码如下:
@Select("select username, `password`, age, gender, phone from userinfo where
username= '${name}' ")
UserInfo queryByName(String name);
再次运⾏, 结果正常返回
![](https://i-blog.csdnimg.cn/direct/5076a84c6b6f4b46a3f942a4354cd2cb.png)
从上⾯两个例⼦可以看出:#{} 使⽤的是预编译SQL, 通过 ? 占位的⽅式, 提前对SQL进⾏编译, 然后把参数填充到SQL语句中.#{} 会根据参数类型, ⾃动拼接引号 '' .${} 会直接进⾏字符替换, ⼀起对SQL进⾏编译. 如果参数为字符串, 需要加上引号 '' .参数为数字类型时, 也可以加上, 查询结果不变, 但是可能会导致索引失效, 性能下降.
#{} 和 ${}区别
#{} 和 ${} 的区别就是预编译SQL和即时SQL 的区别.
简单回顾:当客⼾发送⼀条SQL语句给服务器后, ⼤致流程如下:
- 解析语法和语义, 校验SQL语句是否正确
- 优化SQL语句, 制定执⾏计划
- 执⾏并返回结果
⼀条 SQL如果⾛上述流程处理, 我们称之为 Immediate Statements(即时 SQL)
性能更⾼
绝⼤多数情况下, 某⼀条 SQL 语句可能会被反复调⽤执⾏, 或者每次执⾏的时候只有个别的值不同(⽐ 如 select 的 where ⼦句值不同, update 的 set ⼦句值不同, insert 的 values 值不同). 如果每次都需要经过上⾯的语法解析, SQL优化、SQL编译等,则效率就明显不⾏了.
![](https://i-blog.csdnimg.cn/direct/9b9fbc95ee624d82901c2829118bd7aa.png)
预编译SQL,编译⼀次之后会将编译后的SQL语句缓存起来,后⾯再次执⾏这条语句时,不会再次编译(只是输⼊的参数不同), 省去了解析优化等过程, 以此来提⾼效率
更安全(防⽌SQL注⼊)
SQL注⼊:是通过操作输⼊的数据来修改事先定义好的SQL语句,以达到执⾏代码对服务器进⾏攻击的⽅法。
由于没有对⽤⼾输⼊进⾏充分检查,⽽SQL⼜是拼接⽽成,在⽤⼾输⼊参数时,在参数中添加⼀些
SQL关键字,达到改变SQL运⾏结果的⽬的,也可以完成恶意攻击。
sql 注⼊代码:
' or 1='1
@Select("select username, `password`, age, gender, phone from userinfo where
username= '${name}' ")
List<UserInfo> queryByName(String name);
测试代码:
正常访问情况:
@Test
void queryByName() {
List<UserInfo> userInfos = userInfoMapper.queryByName("admin");
System.out.println(userInfos);
}
结果运⾏正常
![](https://i-blog.csdnimg.cn/direct/61cf8f5f1ca74c9384755f637356452b.png)
SQL注⼊场景:
@Test
void queryByName() {
List<UserInfo> userInfos = userInfoMapper.queryByName("' or 1='1");
System.out.println(userInfos);
}
结果依然被正确查询出来了, 其中参数 or被当做了SQL语句的⼀部分
![](https://i-blog.csdnimg.cn/direct/0ff5b82128e042ac99774f96e3180e52.png)
可以看出来, 查询的数据并不是⾃⼰想要的数据. 所以⽤于查询的字段,尽量使⽤
#{}
预查询的⽅式
SQL注⼊是⼀种⾮常常⻅的数据库攻击⼿段, SQL注⼊漏洞也是⽹络世界中最普遍的漏洞之⼀. 如果发⽣在⽤⼾登录的场景中, 密码输⼊为 ' or 1='1 , 就可能完成登录(不是⼀定会发⽣的场景, 需要看登录代码如何写)
排序功能
从上⾯的例⼦中, 可以得出结论: ${} 会有SQL注⼊的⻛险, 所以我们尽量使⽤#{}完成查询
既然如此, 是不是 ${} 就没有存在的必要性了呢?
当然不是.
Mapper实现
@Select("select id, username, age, gender, phone, delete_flag, create_time,
update_time " +
"from userinfo order by id ${sort} ")
List<UserInfo> queryAllUserBySort(String sort);
使⽤
${sort}
可以实现排序查询, ⽽使⽤
#{sort}
就不能实现排序查询了.
注意: 此处 sort 参数为String类型, 但是SQL语句中, 排序规则是不需要加引号 '' 的, 所以此时的${sort} 也不加引号
我们把
${}
改成
#{}
@Select("select id, username, age, gender, phone, delete_flag, create_time,
update_time " +
"from userinfo order by id #{sort} ")
List<UserInfo> queryAllUserBySort(String sort);
可以发现, 当使⽤ #{sort} 查询时, asc 前后⾃动给加了引号, 导致 sql 错误
#{} 会根据参数类型判断是否拼接引号 ''如果参数类型为String, 就会加上 引号.
除此之外, 还有表名作为参数时, 也只能使⽤
${}
like 查询
like 使⽤ #{} 报错
@Select("select id, username, age, gender, phone, delete_flag, create_time,
update_time " +
"from userinfo where username like '%#{key}%' ")
List<UserInfo> queryAllUserByLike(String key);
把 #{} 改成 ${} 可以正确查出来, 但是${}存在SQL注⼊的问题, 所以不能直接使⽤ ${}.
解决办法: 使⽤ mysql 的内置函数 concat() 来处理,实现代码如下:
@Select("select id, username, age, gender, phone, delete_flag, create_time,
update_time " +
"from userinfo where username like concat('%',#{key},'%')")
List<UserInfo> queryAllUserByLike(String key);
数据库连接池
数据库连接池负责分配、管理和释放数据库连接,它允许应⽤程序重复使⽤⼀个现有的数据库连接,⽽不是再重新建⽴⼀个.
![](https://i-blog.csdnimg.cn/direct/aac8050387a1429b9fe678efe138f357.png)
- 没有使⽤数据库连接池的情况: 每次执⾏SQL语句, 要先创建⼀个新的连接对象, 然后执⾏SQL语句, SQ语句执⾏完, 再关闭连接对象释放资源. 这种重复的创建连接, 销毁连接⽐较消耗资源
- 使⽤数据库连接池的情况: 程序启动时, 会在数据库连接池中创建⼀定数量的Connection对象, 当客⼾ 请求数据库连接池, 会从数据库连接池中获取Connection对象, 然后执⾏SQL, SQL语句执⾏完, 再把 Connection归还给连接池
优点:
- 减少了⽹络开销
- 资源重⽤
- 提升了系统的性能
常⻅的数据库连接池:
- C3P0
- DBCP
- Druid
- Hikari
⽬前⽐较流⾏的是 Hikari, DruidHikari : SpringBoot默认使⽤的数据库连接池