MyBatis项目的创建和使用

什么是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);
}

结果:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

追梦不止~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值