什么是MyBatis?
MyBaits是一个更简单的完成程序与数据库交互的工具。MyBatis将复杂的JDBC进行了封装,让使用者可以通过简单的xml和注解对数据库进行记录。
MyBatis的执行流程
创建MyBatis项目
添加依赖
还是SpringBoot的创建流程:SpringBoot项目创建和使用_追梦不止~的博客-CSDN博客
在添加依赖处增加如下两个依赖:
项目创建好后,是如下图(此处我删除了三个文件,不删也可以):
配置连接
在配置文件中(application.properties)中添加如下设置:
//这两行是连起来的,其中mycnblog是我的数据库名,修改成你自己的
spring.datasource.url=jdbc:mysql://localhost:3306/mycnblog?characterEncoding=utf8&useSSL=false
//用户名和密码改成你自己的,一般用户名没有改就都是root
spring.datasource.username=root
spring.datasource.password=123456
//固定的
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
如果是yml后缀的配置文件,也是相同的配置,将上述改成yml格式即可。
配置xml路径
同样是在配置文件中添加:
//myBatis是目录,自己设置
//文件名的后缀部分必须是Mapper.xml不能修改,前面可以添加内容
//这里的*代表通配符
mybatis.mapper-locations=classpath:/myBatis/*Mapper.xml
添加业务代码
添加实体类
这个实体类是与我表中的属性一一对应的(包括属性名和类型)
@Data
public class UserEntity {
private int id;
private String username;
private String password;
private String phone;
private LocalDateTime createtime;
private LocalDateTime updatetime;
private int state;
}
添加mapper接口
//注解不能少
@Mapper
public interface UserMapper {
//每个方法都对应一个sql操作
}
添加Mapper.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>
单元测试
什么是单元测试?
单元测试是开发中经常用到的测试方法,在数据库操作中的作用尤为明显。
因为我们在对数据库操作时难免会出现错误,这就可能导致数据库中的数据丢失或出错,通过单元测试就可以很好的解决这个问题。
因为单元测试中有一个标签@Transactional,它会将我们的数据库操作进行回滚,以至于我们的测试不会对数据库的内容造成改变。
MyBatis中实现与数据库交互
注意交互前要先有数据库和表,我是已经提前创建好了
单元测试
什么是单元测试?
单元测试是开发中经常用到的测试方法,在数据库操作中的作用尤为明显。
因为我们在对数据库操作时难免会出现错误,这就可能导致数据库中的数据丢失或出错,通过单元测试就可以很好的解决这个问题。
因为单元测试中有一个标签@Transactional,它会将我们的数据库操作进行回滚,以至于我们的测试不会对数据库的内容造成改变。
如何生成单元测试?
我们对userinfo这个表整体查询,首先在mapper接口类中添加一个方法:
List<UserEntity> getAll();
然后再xml文件中的mapper标签中添加如下内容:
其中id属性是方法名getAll,resultType属性是返回类型,因为返回集合中的类型是UserEntity所以这里就填UserEntity的路径
<select id="getAll" resultType="com.example.demo.entity.UserEntity">
select * from userinfo
</select>
然后我们通过单元测试的方式来进行测验。
我们鼠标右键这个getAll方法后,点击生成,点击生成测试
然后回出现如下界面,勾中要生成测试的方法,然后点击确定即可。
在test目录下就会生成测试类,如下:
我们对类进行如下操作:
首先要添加@SpringBootTest注解,然后再将mapper类注入进去,然后在生成出来的方法上添加@Transactional注解,让其进行回滚操作,这里是查询操作,不加也可以,如果需要让数据库跟随修改,也可以选择不加。
@SpringBootTest
class UserMapperTest {
@Autowired
private UserMapper mapper;
@Transactional
@Test
void getAll() {
}
}
然后我们在方法中写入我们的代码即可,如下:
@Transactional
@Test
void getAll() {
List<UserEntity> list = mapper.getAll();
list.stream().forEach(System.out::println);
}
然后我们运行这个方法,结果如下:
此时我的数据库中结果如下:
查询操作
使用select标签。
在单元测试里面已经演示了一个简单的查询的sql操作,下面的查询操作也是基于前面的基础上的。
${}的使用
xml文件中,${xxx}通常用在sql语句中,充当类似占位符的作用,但是在解析是会将${xxx}里面对应内容看做sql语句的一部分,对于数字类型,如:Integer、double等,它可以直接代指,对于字符串类型需要在${xxx}外面加上引号或单引号:'${xxx}'
此时我们对userinfo这个表中 id 为 1 并且username为 'admin' 的数据进行查询(在我的数据库中共有一条)
此时在mapper接口类中添加方法,返回为UserEntity对象,如下:
@Param注解是给其后面的属性其别名,因为要将参数内容传入xml文件里的sql中,${}里面填加的内容就是注解名字(不加就填原来的名字)
UserEntity getUser(@Param("id")Integer id, @Param("username")String username);
在xml文件中添加如下:
id是数字类型,所以不需要加引号或单引号,username是字符串类型,${}并不会为其自动加引号,所以需要手动添加。
<select id="getUser" resultType="com.example.demo.entity.UserEntity">
select * from userinfo where id = ${id} and username = '${username}'
</select>
生成单元测试,这里不写步骤了,直接给出最终代码,后面同样:
@Transactional
@Test
void getUser() {
UserEntity user = mapper.getUser(1, "admin");
System.out.println(user);
}
执行结果如下:
同时也可以看到,${}的位置填充成了数据。
数据库执行相同操命令,结果如下:
${}的缺点:sql注入
前面提到了${}会将里面的内容转化为sql的一部分,但是这样也会产生一系列问题,比如下面的这个查询操作:通过username 和 password来查询某个用户的信息
xml文件:
因为文章规范原因,这里的sql语句进行了修改,将password和=和'${password}'前面的换行删了即可,见谅一下。
<select id="login" resultType="com.example.demo.entity.UserEntity">
select * from userinfo where username='${username}' and
password
=
'${password}'
</select>
测试代码:
@Transactional
@Test
void login() {
UserEntity user = mapper.login("admin","'or 1 = '1");
System.out.println(user);
}
我们在password的传入中给了一串奇怪的字符串,但是数据库中是没有password为上述所见的数据的,并且此时数据库中只有一条数据。
执行结果如下:
可以看到:虽然数据库中没有这样的数据,但是仍然拿到了用户信息,这样的情况就是sql注入。
并且我们对这条sql进行解读过后,它的解析如下:
查询 userinfo 表中 username = 'admin' 并且 password 为空串 的数据 或者 1 = '1'
因为and的优先级比or高,所以会将 username = 'admin' and password 为空串 看为一体,or 1 = '1'另看做一体,此时只要 1 = '1' 这个条件满足,就会查询出userinfo表中的所有数据,所以表中的数据会被查询出来,但是因为接收的类型是UserEntity,所以如果表有多条数据的情况下就无法用这个来获取,需要注入更复杂的sql来获取了。
#{}的使用
#{}与${}不同的是,${}会将内容看做为sql语句的一部分,而#{}则是将其看做一个占位符。因此#{}无法被sql注入,并且对于字符串类型不需要另外加引号。
我们使用和上面sql注入相同的数据来测试:
xml文件:
此处将${}换做了#{},所以要将外部的单引号去掉,不然会报错。
<select id="login" resultType="com.example.demo.entity.UserEntity">
select * from userinfo where username=#{username} and password=#{password}
</select>
测试代码:
@Transactional
@Test
void login() {
UserEntity user = mapper.login("admin","'or 1 = '1");
System.out.println(user);
}
执行结果如下:
得到的结果是null,并且在sql中 #{}的位置替换成了?,和JDBC中的?占位符是相同的作用。
#{}和${}的区别
${}是字符串替换,对字符串类型替换时需要再外围加上引号或单引号,执行时会将${}里面的内容转化为sql语句的一部分,容易出现sql注入。
#{}是预编译处理,字符串类型不需要加引号,执行时会将#{}替换为?,它的作用和JDBC中的?作用相同。
like查询
like查询与其他查询比较特殊的地方在于:like的模糊匹配中无法使用#{}来注入数据,只能使用${}来注入,但是这个注入方式前面也说了,不如不对输入的内容进行判断,很容易被sql注入,所以此处我们使用concat(MySQL里面的字符串拼接函数)配合#{},来完成like的模糊匹配。
举例:在userinfo表中模糊查询username = '%zhang%'的信息。
mapper接口类:
List<UserEntity> getListByLike(@Param("username") String username);
xml文件:
<select id="getListByLike" resultType="com.example.demo.entity.UserEntity">
select * from userinfo where username like concat('%', #{username}, '%')
</select>
测试代码:
@Test
void getListByLike() {
List<UserEntity> list = mapper.getListByLike("zhang");
list.stream().forEach(System.out::println);
}
结果如下:
数据库数据如下:
返回字典映射resultMap
在前面的查询操作中我们使用的是resultType,绝大部分的查询返回类型都可以通过resultType来设置,但是有一种情况比较特殊:当实体类中的属性名和与其对应的表中的列名不相同时,虽然不会报错,但是该属性不会获取到数据。
这个时候就可以通过resultMap来解决。
首先在xml文件里面创建一个resultMap标签,并将要获取的属性插入标签中。
其中id是该reslutMap的名字,type是要接受数据的类的路径
id标签:主键列
result标签:普通列
在标签中:
property属性 表示 类的属性名
column属性 表示 数据库中表的列名
<resultMap id="BaseMap" type="com.example.demo.entity.UserEntity">
<id property="id" column="id"></id>
<result property="username" column="username"></result>
<result property="pwd" column="password"></result>
<result property="createtime" column="createtime"></result>
<result property="updatetime" column="updatetime"></result>
</resultMap>
使用:将原本的resultType代替为resultMap。
举例:在userinfo表中模糊查询username = '%zhang%'的数据(测试代码和mapper接口类中的内容和前面的like查询中的例子相同)
xml文件:
<resultMap id="BaseMap" type="com.example.demo.entity.UserEntity">
<id property="id" column="id"></id>
<result property="username" column="username"></result>
<result property="pwd" column="password"></result>
<result property="createtime" column="createtime"></result>
<result property="updatetime" column="updatetime"></result>
</resultMap>
<select id="getListByLike" resultMap="BaseMap">
select * from userinfo where username like concat('%', #{username}, '%')
</select>
执行结果:
数据库结果:
修改操作
xml中使用update标签
修改和查询不同,它的返回的是执行成功的行数,所以,它的返回类型就是清一色的int/Integer。
举例:查询id 为 1 并且 password = admin的数据,将password修改为123456
mapper接口类:
int updatePassword(@Param("id")Integer id,
@Param("password")String password,
@Param("newPassword")String newPassword);
xml文件:
<update id="updatePassword">
update userinfo set password=#{newPassword} where id=#{id} and password=#{password}
</update>
测试代码:
此处我没有添加@Transactional,为的是可以更好的观察数据是否被修改。
@Test
void updatePassword() {
int result = mapper.updatePassword(1,"admin","123456");
System.out.println(result);
}
执行结果如下:
数据库中查询结果如下:
如图是update操作前后两次的查询结果
password的已被修改。
删除操作
使用delete标签。
和update操作相同,返回值都int/Integer类型,返回的是删除的行数。
举例:删除userinfo表中,username 为 zhangsan 的数据(该数据已经提前被插入进去了)
mapper接口类:
int delByName(@Param("username") String username);
xml文件:
<delete id="delByName">
delete from userinfo where username = #{username}
</delete>
测试代码:
@Test
void delByName() {
int result = mapper.delByName("zhangsan");
System.out.println(result);
}
执行结果:
查询结果:
增添操作
insert标签,在insert标签中可以选择:是否返回该条操作主键列的数据和返回主键的位置。
它的返回类型为int/Integer,返回的是新增成功的行数。
举例:在userinfo表中 添加username 为 zhangsan,password 为 123456的数据,并且将该条sql的主键列返回到id中。
mapper接口类:
int addUserGetId(UserEntity user);
xml文件:
其中useGeneratedKeys代表的是是否返回该条操作的主键列数据,默认为false。
KeyProperty填的是返回到哪个属性里面,因为传入的时候会传入一个UserEntity对象过来,返回的主键数据会存放到这个对象里。
<insert id="addUserGetId" useGeneratedKeys="true" keyProperty="id">
insert into userinfo(username, password) values(#{username}, #{password})
</insert>
测试代码:
@Test
void addUserGetId() {
UserEntity user = new UserEntity();
user.setUsername("zhangsan");
user.setPassword("123456");
int result = mapper.addUserGetId(user);
System.out.println(result);
System.out.println(user.getId());
}
执行结果如下:
数据库中数据如下:
多表查询
多表操作,也需要实体类来接受,首先是一个文章类
@Data
public class ArticleInfo {
private int id;
private String title;
private String content;
private LocalDateTime createtime;
private LocalDateTime updatetime;
private int uid;
private int rcount;
private int state;
}
然后将用户类userinfo的username属性和文章类相结合,形成一个多表查询的实体类(也可以直接在文章类里面加个username属性)
因为有了文章类,所以直接让多表查询的实体类继承文章类即可。并且要重写里面的toString方法,因为lombok的toString方法不会将父类的属性一并打印,所以要自己进行重写。
@Data
public class ArticleInfoVO extends ArticleInfo {
private String username;
@Override
public String toString() {
return "ArticleInfoVO{" +
"username='" + username + '\'' +
"} " + super.toString();
}
}
接下来是mapper接口类:
@Mapper
public interface ArticleMapper {
}
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.demo.mapper.ArticleMapper">
</mapper>
一对一情况
举例:查找文章表中文章编号为1的文章,在查找信息里面要包含该文章对应的作者名。
解析:因为查找信息需要文章信息和作者名,所以需要联合文章表和作者表两个表进行多表查询。
mapper接口类:
List<ArticleInfoVO> getDetail(@Param("id") Integer id);
xml文件:
这里的sql里使用了起别名,articleinfo是文章表别名为a,userinfo是作者表别名为u(后面不再声明)
<select id="getDetail" resultType="com.example.demo.entity.vo.ArticleInfoVO">
select a.*,u.username from articleinfo a left join userinfo u on u.id = a.uid where a.id = #{id}
</select>
测试代码:
@Test
void getDetail() {
List<ArticleInfoVO> list = mapper.getDetail(1);
list.stream().forEach(System.out::println);
}
结果:
一对多情况
举例:在文章表中查找uid(作者id)为1的文章,在查找信息里面要包含该文章对应的作者名。
mapper接口类:
List<ArticleInfoVO> getListByUid(@Param("uid") Integer uid);
xml文件:
<select id="getListByUid" resultType="com.example.demo.entity.vo.ArticleInfoVO">
select a.*,u.username from articleinfo a left join userinfo u on u.id = a.uid where a.uid = #{uid}
</select>
测试代码:
@Test
void getListByUid() {
List<ArticleInfoVO> list = mapper.getListByUid(1);
list.stream().forEach(System.out::println);
}
结果:
动态sql使用
动态sql本质上就是在sql语句中加上了逻辑判断,在一些场景下需要在sql阶段进行逻辑判断。
比如:在注册信息时,有必填项和选填项,在选填项中可能一些项在创建表时是有默认值的,如果不传递该项就会使用默认值,此时就可以使用动态sql进行逻辑判断,来决定该项是否要传递。
<if>标签
和Java里面的if判断相同。
语法:在sql中使用,if标签里面的test属性为必填项,相当于if中的逻辑判断语句。
举例:添加一个用户,使用if标签判断是否传递了photo属性,并查看sql区别
mapper接口类:
int addUser2(UserEntity user);
xml文件:
test中的photo代表的是传入的photo属性,但是不需要加#{}。
<insert id="addUser2">
insert into userinfo(username,password
<if test="photo != null">
,photo
</if>
) values(#{username},#{password}
<if test="photo != null">
,#{photo}
</if>
)
</insert>
测试代码1,传递photo属性:
@Transactional
@Test
void addUser2() {
UserEntity user = new UserEntity();
user.setUsername("lisi");
user.setPassword("123456");
//user.setPhoto("photo.png");
int result = mapper.addUser2(user);
System.out.println(result);
}
结果:
测试代码2,传递photo属性:
@Transactional
@Test
void addUser2() {
UserEntity user = new UserEntity();
user.setUsername("lisi");
user.setPassword("123456");
user.setPhoto("photo.png");
int result = mapper.addUser2(user);
System.out.println(result);
}
结果:
和测试1相比测试2的结果多了一个参数photo,而测试1因为没有传递,所以在if判断时被排查到,所以没有传递photo。
<trim>标签
<trim>标签它一般和<if>标签搭配使用,在<trim>标签包含的语句,可以通过<trim>里面的属性进行前缀、后缀的添加和删除。
prefix:添加前缀
suffix:添加后缀
prefixOverrides:清除前缀
suffixOverrides:清除后缀
举例:假设所有的属性都是非必选,此时需要所有属性都需要<if>标签判断,但是会出现逗号如何分配的问题,此时将所有的<if>标签的最后都加上逗号,并使用<trim>标签删除最后一个逗号,完成sql语句。
mapper接口类:
int addUser3(UserEntity user);
xml文件:
<insert id="addUser3">
insert into userinfo
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="username != null">
username,
</if>
<if test="password != null">
password,
</if>
<if test="photo != null">
photo,
</if>
</trim>
values
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="username != null">
#{username},
</if>
<if test="password != null">
#{password},
</if>
<if test="photo != null">
#{photo},
</if>
</trim>
</insert>
测试代码:
@Transactional
@Test
void addUser3() {
UserEntity user = new UserEntity();
user.setUsername("lisi");
user.setPassword("123456");
user.setPhoto("photo.png");
int result = mapper.addUser3(user);
System.out.println(result);
}
结果:
<where>标签
<where>标签也是搭配<if>标签使用的。
<where>标签里面如果没有内容则不会添加where语句,如果里面有内容,则会在内容前加上where并且会去除标签内容的前缀and。
使用场景:where里面的条件为非必传
举例:根据id和title查询articleinfo表中的数据,id和title都可能为null
mapper接口类:
List<ArticleInfoVO> getUserByIdOrTitle(@Param("id")Integer id, @Param("title")String title);
xml文件:
第一种方法:where 1=1
因为id和title都可能为null会被<if>标签排除,那么where的后面可能会出现没有条件的情况,此时添加1=1,可以解决这个问题。
<select id="getUserByIdOrTitle" resultType="com.example.demo.entity.vo.ArticleInfoVO">
select * from articleinfo
where 1=1
<trim prefixOverrides="and">
and
<if test="id != null and id > 0">
and id = #{id}
</if>
<if test="title != null and title != ''">
and title like concat('%',#{title},'%')
</if>
</trim>
</select>
第二种方法:<trim>标签
因为trim标签中如果没有内容就不会添加前后缀,所以在trim标签中加上前缀where,也可以解决问题。
<select id="getUserByIdOrTitle" resultType="com.example.demo.entity.vo.ArticleInfoVO">
select * from articleinfo
<trim prefix="where" prefixOverrides="and">
<if test="id != null and id > 0">
and id = #{id}
</if>
<if test="title != null and title != ''">
and title like concat('%',#{title},'%')
</if>
</trim>
</select>
第三种方法:<where>标签
<where>标签相当于简化的第二种方法,因为<where>标签的特性:若内容不为空自动添加where和去除前缀and,所以也可以解决问题,并且代码更加简洁。
<select id="getUserByIdOrTitle" resultType="com.example.demo.entity.vo.ArticleInfoVO">
select * from articleinfo
<where>
<if test="id != null and id > 0">
and id = #{id}
</if>
<if test="title != null and title != ''">
and title like concat('%',#{title},'%')
</if>
</where>
</select>
测试代码:
这里只写了一种情况,还可以 全为null 或者 都不为null 或者 title属性为null,id不为null
@Test
void getUserByIdOrTitle() {
List<ArticleInfoVO> list = mapper.getUserByIdOrTitle(null,"s");
list.stream().forEach(System.out::println);
}
结果:
<set>标签
set标签使用于修改sql(update)
<set>标签里面的内容不能为空,并且会自动添加set前缀。
举例:根据传入的id,修改articleinfo表中的title和content属性
mapper接口类:
int updateTitleAndContent(@Param("title")String title,
@Param("content")String content,
@Param("id")Integer id);
xml文件:
传入数据时要注意,set标签里面一定不能没有内容
<update id="updateTitleAndContent">
update articleinfo
<set>
<if test="title != null and title != ''">
title = #{title},
</if>
<if test="content != null and content != ''">
content = #{content},
</if>
</set>
where id = #{id}
</update>
测试代码:
@Transactional
@Test
void updateTitleAndContent() {
int result = mapper.updateTitleAndContent("java","java正文",1);
}
结果:
<foreach>标签
<foreach>标签 会将传入的集合类或数组等容器里面的数据循环取出并注入到sql中。
<foreach>标签 中有五个属性:
collection:传入的集合类的变量名。比如:我传入一个List对象,名字为list,此时里面填list
item:集合类中每一个成员的别名。比如:别名为id,在<foreach>标签里面使用时就可以使用#{id}等。
open:添加前缀。
close:添加后缀。
separator:循环内容的间隔符
举例:删除articleinfo表中id为1、2、3的数据,1、2、3使用List传输。
mapper接口类:
int delByIdList(List<Integer> idList);
xml文件:
collection里填写集合名idList,item填写别名为id,separator填写间隔符号为英文逗号,open填写前缀为( ,close填写后缀为) 。
<delete id="delByIdList">
delete from articleinfo
where id in
<foreach collection="idList" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</delete>
测试代码:
@Transactional
@Test
void delByIdList() {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
int result = mapper.delByIdList(list);
System.out.println(result);
}
结果: