🚗MyBatis学习·第三站~
🚩起始站:MyBatis概述&环境搭建(一)
🚩本文已收录至专栏:数据库学习之旅
👍希望您能有所收获
上一篇我们学习了如何使用Mapper代理开发,核心配置文件,但却始终没讲SQL映射文件,接下来便让我们一起来认识一下映射文件,再学习一下如何利用此完成常用CRUD操作,并介绍一下使用过程中涉及的一些知识点。
一.XML映射器
(1) 概述
MyBatis官方文档中有提到MyBatis 的真正强大在于它的语句映射,这是它的魔力所在。由于它的异常强大,映射器的 XML 文件就显得相对简单。如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% 的代码。MyBatis 致力于减少使用成本,让用户能更专注于 SQL 代码。
我们只需要在对应增删改查标签下编写SQL语句即可,对于参数设置等操作,它提供了丰富的属性选项来帮助我们配置。例如:
(2) resultMap结果映射
resultMap结果映射是 MyBatis 最强大的特性,如果对其理解透彻,许多复杂的映射问题都能迎刃而解。
接下来我们通过一个小案例学习它的用处:
当我们在进行查询操作时,如果实体类属性名 和 数据库表列名 不一致时,MyBatis不能自动封装数据。有些人可能会想,我将名称设置成一样不就行了,问题时,表不一定是你创建的,而且很多情况下,数据库中比较倾向下划线分割而Java中却是驼峰命名。
名称不一致:
名称一致:
- 解决此类问题我们有两种方法:
- 起别名:在SQL语句中,对不一样的列名起别名,令别名和实体类属性名一样,即可
这样可以很快解决问题,但是我们想,如果我们要写数十个SQL岂不是每个SQL语句都要重复的as as?这繁琐的工作量…还有一种利用片段的方法优化,比较鸡肋就不赘述。
- resultMap映射:定义 对不一致的属性名和列名进行映射
<!--id属性:映射唯一标识 ,type:映射的类型-->
<resultMap id="brandResultMap" type="brand">
<!--id 用于完成主键字段的映射,如果相同可以不用写-->
<id property="id" column="brand_id" />
<!-- result 用于一般字段的映射,-->
<!-- property:实体类属性名称-->
<!-- column:表字段名称-->
<result property="brandName" column="brand_name"/>
<result property="companyName" column="company_name"/>
</resultMap>
然后需要在引用它的语句中将 resultType
属性改为 resultMap
属性就行了
<select id="selectAll" resultMap="brandResultMap">
select * from tb_brand
</select>
至此就不用再理会 实体类属性名 和 数据库表列名 不一致的问题啦。是不是比之第一种简洁很多?
如上便是resultMap的一种简单用法,更多复杂使用可查阅官方文档,上面给出许多种用法~
二.CRUD引入
-
使用MyBatis完成各项操作基本只需要如下三步即可:
- 在Mapper接口中编写xxx方法
- 在SQL映射文件(xml)中编写实现操作的SQL语句
- 调用执行方法
-
关键在于我们需要根据业务的不同分析:
- SQL语句如何写
- 是否需要传递参数
- 返回值应该是什么类型
接下来,让我们一起基于上一节搭建的Mapper代理开发环境,先通过配置文件实现MyBatis的增删改查操作,再一起看看通过注解开发如何简化一些流程。
三.通过配置文件开发
(1) 查询
我们可以在标签中完成查询语句的编写~
(1.1) 查询所有数据
- 根据业务分析,我们可以在Mapper接口中编写如下方法:
List<Brand> selectAll();
不需要传递参数,返回值应该为一个User集合。
- 在映射文件(xml)中编写实现业务SQL语句:
<select id="selectAll" resultType="User">
select * from tb_user;
</select>
可以通过上一篇所讲MyBatisX插件快速生成外层标签,自己写业务SQL即可。
- 调用方法进行测试
public class MyBatisDemo {
public static void main(String[] args) {
try {
// 1. 加载mybatis的核心配置文件,获取SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 2. 获取SqlSession对象,用于执行sql
SqlSession sqlSession = sqlSessionFactory.openSession();
// 3.通过SqlSession的getMapper方法获取 Mapper接口的代理对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 4.使用接口中编写的方法
List<User> userList = userMapper.selectAll();
// 5.打印查看结果
System.out.println(userList);
// 4.释放资源
sqlSession.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
由于后续使用步骤基本一致,除了4,5步代码都将省略书写。
(1.2) 查询单个(参数占位符介绍)
- 根据业务分析,我们可以在Mapper接口中编写如下方法:
Brand selectById(int id);
我们可以通过id获取结果,因此返回值应该为单个User对象。
- 在映射文件(xml)中编写实现业务SQL语句:
<select id="selectById" parameterType="int" resultType="User">
select * from tb_user where id = #{id};
</select>
parameterType
:用于设置参数类型,该参数可以省略,MyBatis 可以自动推断出。
#{}
表示参数占位符,我们使用时传递过来的数值会将其替换掉,分为如下两种:
-
#{}
:执行SQL时,会将#{}占位符替换为?,将来自动设置参数值,防止SQL注入问题, 底层使用的是PreparedStatement
,推荐使用√
-
${}
:拼接SQL。会存在SQL注入问题,底层使用的是Statement
,不推荐使用
- 调用方法进行测试
// ...省略
// 4.使用接口中编写的方法
User user = userMapper.selectById(2);
// 5.打印查看结果
System.out.println(user);
(1.3) 多条件查询(三种方式)
根据业务分析,我们可以知道查询条件往往会是多个参数,返回结果也应该为对应实体类集合。
例如,我们可以在映射文件中编写如下多条件查询代码:
<select id="selectByCondition" resultType="User">
select *
from tb_user
where username = #{username}
and gender = #{gender}
and addr = #{addr}
</select>
接下来让我们一起来看看三种不同的接口使用形式~
(1.3.1) 散装参数
当我们直接写参数名称时,MyBatis无法得知我们的参数与查询条件对应情况,因此需要使用@Param("SQL中的参数名称")
将参数与SQL条件字段#{xxx}
进行映射。
List<User> selectByCondition(@Param("username") String username, @Param("gender") String gender, @Param("addr") String addr);
调用方法进行测试
// ...省略
// 4.直接使用接口中编写的方法即可
List<User> userList = userMapper.selectByCondition("zhangsan", "男", "北京");
// 5.打印查看结果
System.out.println(userList);
(1.3.2) 实体类封装参数
我们也可以将参数都设置在实体类中,并保证SQL中的参数名#{xxx}
和 实体类属性名对应上,将实体类作为参数传递即可。
List<User> selectByCondition(User user);
调用方法进行测试
// ...省略
//4.直接使用接口中编写的方法即可
User user = new User();
user.setUsername("zhangsan");
user.setGender("男");
user.setAddr("北京");
List<User> userList = userMapper.selectByCondition(user);
// 5. 打印结果
System.out.println(userList);
(1.3.3) map集合
我们还可以通过map集合的方式对参数进行传递,必须确保key值与SQL中的参数名#{xxx}
保持一致
List<User> selectByCondition(Map map);
调用方法测试
//4.直接使用接口中编写的方法即可
HashMap<String, String> hashMap = new HashMap<>();
hashMap.put("username", "zhangsan");
hashMap.put("gender", "男");
hashMap.put("addr", "北京");
List<User> userList = userMapper.selectByCondition(hashMap);
// 5. 打印结果
System.out.println(userList);
(1.4) 动态SQL查询优化
测试上述多条件查询可以发现,我们只有同时写上三个条件时才能查询到结果,少设置部分则无法查询到结果,这意味着我们需要重复写查询新的SQL,例如条件1,3,条件2,3,条件1,2。我的天,排列组合,当参数过多时…
我们可以使用动态SQL来优化这一过程,使得我们有更好的体验。
定义:SQL语句会随着用户的输入或外部条件的变化而变化
官网文档中这样说道:动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。
详细使用可以查询官方文档~
我们可以对上述sql使用动态SQL语法进行如下改造,这样便可自由决定传递的参数个数:
<select id="selectByCondition" resultType="User">
select *
from tb_user
<where>
<if test="username!=null">
username = #{username}
</if>
and gender like #{gender}
<if test="gender!=null">
and gender like #{gender}
</if>
<if test="addr!=null">
and addr like #{addr}
</if>
</where>
</select>
-
:用于判断参数是否有值,使用test属性进行条件判断
-
标签替换 where 关键字,解决第一个条件不需要逻辑运算符问题
动态SQL除了可以用于优化多条件查询,还可以简化单条件查询,例如碰到下述场景,我们可以写多个单条件查询SQL,但是使用动态SQL却可以简化这一过程。
编写接口
List<User> selectByConditionSingle(User user)
编写配置文件:
choose (when, otherwise)
:选择,类似于Java 中的 switch 语句,when->case,otherwise->default
<select id="selectByCondition" resultType="User">
select *
from tb_user
where
<choose>
<when test="username!=null">
username like #{username}
</when>
<when test="gender!=null">
gender like #{gender}
</when>
<when test="addr!=null">
addr like #{addr}
</when>
<otherwise>
1=1
</otherwise>
</choose>
</select>
如果不需要otherwise,为了防止报错,也可以写为如下形式:
<select id="selectByCondition" resultType="User">
select *
from tb_user
<where>
<choose>
<when test="username!=null">
username like #{username}
</when>
<when test="gender!=null">
gender like #{gender}
</when>
<when test="addr!=null">
addr like #{addr}
</when>
</choose>
</where>
</select>
测试:
// 省略...
//4.直接使用接口中编写的方法即可
User user = new User();
user.setUsername("zhangsan");
List<User> userList = userMapper.selectByConditionSingle(user);
// 5. 打印结果
System.out.println(userList);
至此我们已经简单了解了动态SQL的使用,更多用法可查看官方文档~
(2) 添加
我们可以在标签中完成插入语句的编写~
(2.1) 添加
- 根据业务分析,我们可以在Mapper接口中编写如下方法:
void add(User user);
参数应该为除了id之外的所有数据
- 在映射文件(xml)中编写实现业务SQL语句:
<insert id="add">
insert into tb_user (username, password, gender, addr)
values (#{username}, #{password}, #{gender}, #{addr});
</insert>
- 运行测试
//4.直接使用接口中编写的方法即可
User user = new User();
user.setPassword("666");
user.setGender("男");
user.setAddr("长安");
user.setUsername("李白");
userMapper.add(user);
-
测试上述代码,我们打开数据库可以发现数据并没有插入成功,难道是我们哪里写错了?
-
并没有,这是因为MyBatis默认开启事务:
- 进行增删改操作后需要使用 sqlSession.commit(); 手动提交事务
//4.直接使用接口中编写的方法即可 User user = new User(); user.setPassword("666"); user.setGender("男"); user.setAddr("长安"); user.setUsername("李白"); userMapper.add(user); // 提交事物 sqlSession.commit();
- 也可以设置为自动提交事务(关闭事务)openSession(true):
// 2. 获取SqlSession对象,关闭事务 SqlSession sqlSession = sqlSessionFactory.openSession(true); //... // 后续操作不用再手动提交事物
(2.2) 添加(返回主键)
很多时候,在数据添加成功后,我们需要获取插入数据库数据的主键,比如添加用户后,我们可能需要获取其id信息.
- 根据业务分析,我们可以在Mapper接口中编写如下方法:
void add(User user);
参数应该为除了id之外的所有数据
- 在映射文件(xml)中编写实现业务SQL语句:
<insert id="add" useGeneratedKeys="true" keyProperty="id">
insert into tb_user (username, password, gender, addr)
values (#{username}, #{password}, #{gender}, #{addr});
</insert>
注意:keyProperty
指定的是实体类的属性名,表示将获取到的主键值封装到哪儿个属性里
- 运行测试
v//4.直接使用接口中编写的方法即可
User user = new User();
user.setPassword("666");
user.setGender("男");
user.setAddr("长安");
user.setUsername("李白");
userMapper.add(user);
// 打印返回id值
System.out.println(user.getId());
需要注意的是,返回的主键重新封装在了传递的User实体类中。
(3) 修改
我们可以在标签中完成修改语句的编写~
(3.1) 修改全部字段
- 根据业务分析,我们可以在Mapper接口中编写如下方法:
void update(User user);
参数应该为除了id之外的所有数据
- 在映射文件(xml)中编写实现业务SQL语句:
<update id="update">
update tb_user
set username = #{username},
password = #{password},
ordered = #{ordered},
gender = #{gender},
addr = #{addr}
where id = #{id};
</update>
- 运行测试
//4.直接使用接口中编写的方法即可
User user = new User();
user.setId(10);
user.setPassword("66");
user.setGender("男");
user.setAddr("安");
user.setUsername("白");
userMapper.update(user);
注意:如果没有关闭事务,此处仍需要手动提交事务且必须填充上每个字段,否则会将为设置的字段置为null!
(3.2) 修改部分字段
在上述修改全部代码中,与之前多条件查询类似,为了简化类似SQL的编写,我们可以使用动态SQL的语法来简化开发.
- 编写一个与上述一致的接口
void update(User user);
- 在映射文件(xml)中优化上述修改SQL语句:
<update id="update">
update tb_user
<set>
<if test="username != null">
username = #{username},
</if>
<if test="password != null">
password = #{password},
</if>
<if test="gender != null">
gender = #{gender},
</if>
<if test="addr != null">
addr = #{addr},
</if>
</set>
where id = #{id};
</update>
标签替换 set关键字,解决末尾,号多于引起的问题
- 运行测试
//4.直接使用接口中编写的方法即可
User user = new User();
user.setId(10);
user.setPassword("66");
user.setGender("男");
user.setUsername("白");
userMapper.update(user);
注意:如果没有关闭事务,此处仍需要手动提交事务,可以只修改部分想修改的字段,不会影响其他字段!
(4) 删除
我们可以在标签中完成删除语句的编写~
(4.1) 删除单个
- 根据业务分析,我们可以在Mapper接口中编写如下方法:
void deleteById(@Param("id") int id);
参数为待删除字段的标识
- 在映射文件(xml)中编写实现业务SQL语句:
<delete id="deleteById">
delete
from tb_user
where id = #{id}
</delete>
- 测试
//省略...
//4.直接使用接口中编写的方法即可
userMapper.deleteById(8);
注意:如果没有关闭事务,此处仍需要手动提交事务!
(4.2) 批量删除
- 根据业务分析,我们可以在Mapper接口中编写如下方法:
void deleteByIds(@Param("id") int id);
参数为多个需要被删除的数据标识
- 在映射文件(xml)中编写实现业务SQL语句:
<delete id="deleteByIds">
delete from tb_user
where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">#{id}</foreach>
</delete>
由于我们需要删除的个数是不确定的,因此我们仍然可以使用动态SQL的一些语句编写形成正确的SQL语句。
foreach
标签用来迭代任何可迭代的对象(如数组,集合)。
- collection 属性:
- mybatis会将数组参数,封装为一个Map集合。
- 默认:array = 数组
- 使用@Param注解改变map集合的默认key的名称
- mybatis会将数组参数,封装为一个Map集合。
- item 属性:本次迭代获取到的元素。
- separator 属性:集合项迭代之间的分隔符。
foreach
标签不会错误地添加多余的分隔符。也就是最后一次迭代不会加分隔符。 - open 属性:该属性值是在拼接SQL语句之前拼接的语句,只会拼接一次
- close 属性:该属性值是在拼接SQL语句拼接后拼接的语句,只会拼接一次
- 测试
//4.直接使用接口中编写的方法即可
int[] ids = {1, 3, 4};
userMapper.deleteByIds(ids);
四.通过注解开发
通过上面的学习我们已经学会了如何在配置文件中编写代码,其实注解开发与配置文件开发的区别就是编写SQL的位置不同。
官方文档表明:使用注解开发简单的SQL语句可以使我们的代码更加简洁,但是对应复杂的SQL配置文件依旧是它的最好选择,我们可以在注解和配置文件中自由切换~
与<Select>
、<Insert>
、<Update>
、<Delete>
所对应,MyBatis同样提供了我们几个让我们用于注解开发@Select
、@Insert
、@Update
、@Delete
(1) 查询
使用注解开发将无需在xml映射文件中重复编写代码,对于简单SQL还是一件比较舒服的事~
@Select("select from tb_user")
List<User> selectAll();
@Select("select from tb_user where id = #{id}")
User selectById(int id);
(2) 插入
@Insert("insert into tb_user (username, password, gender, addr) values (#{username}, #{password}, #{gender}, #{addr})")
int add(User user);
(3) 修改
@Update("update tb_user set username = #{username},password = #{password}, gender = #{gender},addr = #{addr} where id = #{id}")
void update(User user);
(4) 删除
@Delete("delete from tb_user where id = #{id}")
void deleteById(@Param("id") int id);
可以看到删除和查询这种短小的语句用注解写起来还是挺舒服的,但是换成插入和修改语句便不是特别友好了,因此我们可以按官方文档所述,不必拘泥于一种方式,而是根据自己的情况选择合适的方式~
五.参数传递流程(补充)
Mybatis 接口方法中可以接收各种各样的参数,如下:
- 多个参数
- 单个参数:单个参数又可以是如下类型
- POJO 类型
- Map 集合类型
- Collection 集合类型
- List 集合类型
- Array 类型
- 其他类型
(1) 多个参数
如下面的代码,就是接收两个参数,而接收多个参数需要使用 @Param
注解,那么为什么要加该注解呢?这个问题要弄明白就必须来研究Mybatis 底层对于这些参数是如何处理的。
User select(@Param("username") String username,@Param("password") String password);
<select id="select" resultType="user">
select *
from tb_user
where
username=#{username}
and password=#{password}
</select>
我们在接口方法中定义多个参数,Mybatis 会将这些参数封装成 Map 集合对象,值就是参数值,而键在没有使用 @Param
注解时有以下命名规则:
-
以 arg 开头 :第一个参数就叫 arg0,第二个参数就叫 arg1,以此类推。如:
map.put(“arg0”,参数值1);
map.put(“arg1”,参数值2);
-
以 param 开头 : 第一个参数就叫 param1,第二个参数就叫 param2,依次类推。如:
map.put(“param1”,参数值1);
map.put(“param2”,参数值2);
代码验证:
-
在
UserMapper
接口中定义如下方法User select(String username,String password);
-
在
UserMapper.xml
映射配置文件中定义SQL<select id="select" resultType="user"> select * from tb_user where username=#{arg0} and password=#{arg1} </select>
或者
<select id="select" resultType="user"> select * from tb_user where username=#{param1} and password=#{param2} </select>
-
运行代码结果如下
在映射配合文件的SQL语句中使用用
arg
开头的和param
书写,代码的可读性会变的特别差,此时可以使用@Param
注解。
在接口方法参数上使用 @Param
注解,Mybatis 会将 arg
开头的键名替换为对应注解的属性值。
代码验证:
-
在
UserMapper
接口中定义如下方法,在username
参数前加上@Param
注解User select(@Param("username") String username, String password);
Mybatis 在封装 Map 集合时,键名就会变成如下:
map.put(“username”,参数值1);
map.put(“arg1”,参数值2);
map.put(“param1”,参数值1);
map.put(“param2”,参数值2);
-
在
UserMapper.xml
映射配置文件中定义SQL<select id="select" resultType="user"> select * from tb_user where username=#{username} and password=#{param2} </select>
-
运行程序结果没有报错。而如果将
#{}
中的username
还是写成arg0
<select id="select" resultType="user"> select * from tb_user where username=#{arg0} and password=#{param2} </select>
-
运行程序则可以看到错误
结论:以后接口参数是多个时,在每个参数上都使用 @Param
注解。这样代码的可读性更高。
(2) 单个参数
-
POJO 类型
可以直接使用。要求
属性名
和参数占位符名称
一致 -
Map 集合类型
可以直接使用。要求
map集合的键名
和参数占位符名称
一致 -
Collection 集合类型
Mybatis 会将集合封装到 map 集合中,如下:
map.put(“arg0”,collection集合);
map.put(“collection”,collection集合;
可以使用
@Param
注解替换map集合中默认的 arg 键名。 -
List 集合类型
Mybatis 会将集合封装到 map 集合中,如下:
map.put(“arg0”,list集合);
map.put(“collection”,list集合);
map.put(“list”,list集合);
可以使用
@Param
注解替换map集合中默认的 arg 键名。 -
Array 类型
Mybatis 会将集合封装到 map 集合中,如下:
map.put(“arg0”,数组);
map.put(“array”,数组);
可以使用
@Param
注解替换map集合中默认的 arg 键名。 -
其他类型
比如int类型,
参数占位符名称
叫什么都可以。尽量做到见名知意
六.特殊字符处理(补充)
我们肯定会在SQL语句中写特殊字符,比如某一个字段大于某个值,如下图
可以看出报错了,因为映射配置文件是xml类型的问题,而 > < 等这些字符在xml中有特殊含义,不能直接使用。所以此时我们需要将这些符号进行转义,可以使用以下两种方式进行转义:
-
转义字符
下图的
<
就是<
的转义字符。
- <![CDATA[内容]]>可将内容直接写在这其中