【后端开发】MyBatis

发布于:2025-04-23 ⋅ 阅读:(11) ⋅ 点赞:(0)

MyBatis入门

定义

MyBatis是⼀款优秀的持久层框架,⽤于简化JDBC的开发。

依赖

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>3.0.4</version>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

数据准备

数据库

-- 创建数据库
DROP DATABASE IF EXISTS mybatis_test;
CREATE DATABASE mybatis_test DEFAULT CHARACTER SET utf8mb4;

-- 使用数据数据
USE mybatis_test;

-- 创建表[用户表]
DROP TABLE IF EXISTS userinfo;
CREATE TABLE `userinfo` (
        `id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
        `username` VARCHAR ( 127 ) NOT NULL,
        `password` VARCHAR ( 127 ) NOT NULL,
        `age` TINYINT ( 4 ) NOT NULL,
        `gender` TINYINT ( 4 ) DEFAULT '0' COMMENT '1-男 2`在这里插入代码片`-女 0-默认',
        `phone` VARCHAR ( 15 ) DEFAULT NULL,
        `delete_flag` TINYINT ( 4 ) DEFAULT 0 COMMENT '0-正常, 1-删除',
        `create_time` DATETIME DEFAULT now(),
        `update_time` DATETIME DEFAULT now(),
        PRIMARY KEY ( `id` )
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4;

-- 添加用户信息
INSERT INTO mybatis_test.userinfo ( username, `password`, age, gender, phone )
VALUES ( 'admin', 'admin', 18, 1, '18612340001' );
INSERT INTO mybatis_test.userinfo ( username, `password`, age, gender, phone )
VALUES ( 'zhangsan', 'zhangsan', 18, 1, '18612340002' );
INSERT INTO mybatis_test.userinfo ( username, `password`, age, gender, phone )
VALUES ( 'lisi', 'lisi', 18, 1, '18612340003' );
INSERT INTO mybatis_test.userinfo ( username, `password`, age, gender, phone )
VALUES ( 'wangwu', 'wangwu', 18, 1, '18612340004' );

在这里插入图片描述

实体类

对应上述实现出来的数据库表,实现对应的实体类 UserInfo 。

@Data
public class UserInfo {
     private Integer id;
     private String username;
     private String password;
     private Integer age;
     private Integer gender;
     private String phone;
     private Integer deleteFlag;
     private Date createTime;
     private Date updateTime;
}

企业开发规范

  1. 表名:字段名使用小写字母或数字,单词之间以下划线分割,尽量避免出现数字开头或者两个下划线,中间只出现数字。
  2. 表必备三字段:id(主键)、create_time(创建时间)、 update_time(更新时间),有同等含义的字段即可,字段名不做强制要求。
  3. 在表查询中,避免使用 * 作为查询的字段列表。

数据库配置

这里展示 application.yml 配置文件。

spring:
  application:
    name: mybatis-demo
  datasource:
    url: jdbc:mysql://localhost:3306/mybatis_test?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

基础实现与测试

Mapper层

Mybatis的持久层接口规范一般叫 XxxMapper。
@Mapper注解:表示是MyBatis中的Mapper接口,程序运行时,框架会自动生成接口的实现类对象(代理对象),并给交Spring的IoC容器管理。
@Select注解:代表的就是select查询,也就是注解对应⽅法的具体实现内容。

@Mapper
public interface UserInfoMapper {
    @Select("select * from userinfo")
    List<UserInfo> getUserInfoAll();
}

测试

IDEA可以自动生成测试类,在需要测试的Mapper接口中,右键->Generate ->Test,setUp/@Before方法会出现在执行该测试的前面,tearDown/@After会出现在执行该测试的后面,一定要将@SpringBootTest注解到该类,以及通过@Autowired注入Mapper对象。
在这里插入图片描述

@SpringBootTest
class UserInfoMapperTest {
    @Autowired
    private UserInfoMapper userInfoMapper;
    @BeforeEach
    void setUp() {
        System.out.println("before each");
    }

    @Test
    void getUserInfoAll() {
        System.out.println(userInfoMapper.getUserInfoAll());
    }
    
    @AfterEach
    void tearDown() {
        System.out.println("after each");
    }

MyBatis日志

在Mybatis中可以借助日志查看到sql语句的执行、执行传递的参数以及执行结果,在配置文件中进行配置即可,配置内容如下:

mybatis:
  configuration: # 配置打印 MyBatis 执行的 SQL
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

增删改查

增–Insert

//Mapper类
    @Insert("insert into userinfo (username, password, age, gender) " +
            "values (#{username},#{password},#{age},#{gender})")
    Integer insert(UserInfo userinfo);

//测试类
    @Test
    void insert() {
        UserInfo userinfo = new UserInfo();
        userinfo.setUsername("laojun");
        userinfo.setPassword("123456");
        userinfo.setAge(18);
        userinfo.setGender(1);
        System.out.println(userInfoMapper.insert(userinfo));
    }

在这里插入图片描述
在这里插入图片描述

增–insert

//Mapper类
    @Insert("insert into userinfo (username, password, age, gender) " +
            "values (#{username},#{password},#{age},#{gender})")
    Integer insert(UserInfo userinfo);

//测试类
    @Test
    void insert() {
        UserInfo userinfo = new UserInfo();
        userinfo.setUsername("laojun");
        userinfo.setPassword("123456");
        userinfo.setAge(18);
        userinfo.setGender(1);
        System.out.println(userInfoMapper.insert(userinfo));
    }

在这里插入图片描述
在这里插入图片描述

@Param

如果设置了 @Param 属性,#{…}对象就只能是一个整体需要使用 参数.属性 来获取。

    @Insert("insert into userinfo (username, password, age, gender) " +
            "values (#{userinfo.username},#{userinfo.password},#{userinfo.age},#{userinfo.gender})")
    Integer insert1(@Param("userinfo") UserInfo userinfo);

插入成功返回ID–@Options

返回主键
如果想要拿到 ID ,需要在Mapper接口的方法上添加一个Options的注解。
keyProperty:指定能够唯一识别对象的属性。

//Mapper类
    @Options(useGeneratedKeys = true, keyProperty = "id")
    @Insert("insert into userinfo (username, password, age, gender) " +
            "values (#{userinfo.username},#{userinfo.password},#{userinfo.age},#{userinfo.gender})")
    Integer insert1(@Param("userinfo") UserInfo userinfo);

//测试类
    @Test
    void insert1() {
        UserInfo userinfo = new UserInfo();
        userinfo.setUsername("laojun2");
        userinfo.setPassword("1234562");
        userinfo.setAge(12);
        userinfo.setGender(2);
        System.out.println(userInfoMapper.insert1(userinfo));
        System.out.println("id: "+userinfo.getId());
    }

在这里插入图片描述

删–delete

//Mapper类
    @Delete("delete from userinfo where id = #{id}")
    Integer delete(Integer id);

//测试类
    @Test
    void delete() {
        System.out.println(userInfoMapper.delete(6));
    }

在这里插入图片描述在这里插入图片描述

改–update

//Mapper类
    @Update("update userinfo set password = #{password} where id = #{id}")
    Integer update(UserInfo userInfo);

//测试类
    @Test
    void update() {
        UserInfo userinfo = new UserInfo();
        userinfo.setId(1);
        userinfo.setPassword("123456");
        System.out.println(userInfoMapper.update(userinfo));
    }

在这里插入图片描述
在这里插入图片描述

查–select

//Mapper类
    @Select("select * from userinfo")
    List<UserInfo> queryAllUser();

//测试类
    @Test
    void queryAllUser() {
        System.out.println(userInfoMapper.queryAllUser());
    }

在这里插入图片描述
从结果中可以看出,实体类与数据库字段名不同的结果为null,无法接收到相应的数据。
原因:

当自动映射查询结果时,MvBatis 会获取结果中返回的列名并在 Java 类中查找相同名字的属性。
实体类的属性名与数据库字段名不同。(deleteFlag–delete_flag、createTime–create_time、updateTime–update_time)
在这里插入图片描述

解决办法:

起别名
结果映射
开启驼峰命名

起别名

//Mapper类
    @Select("select id, username, password, age, gender, phone, " +
            "delete_flag as deleteFlag, create_time as createTime, update_time as updateTime " +
            "from `userinfo`")
    List<UserInfo> selectUserInfos();

//测试类
    @Test
    void selectUserInfos() {
        System.out.println(userInfoMapper.selectUserInfos());
    }

在这里插入图片描述

结果映射

//Mapper类
    @Results(id ="resultMap" , value = {
            @Result(column = "delete_flag", property = "deleteFlag"),
            @Result(column = "create_time", property = "createTime"),
            @Result(column = "update_time", property = "updateTime")
    })
    @Select("select * from userinfo")
    List<UserInfo> selectUserInfo2();

//测试类
    @Test
    void selectUserInfo2() {
        System.out.println(userInfoMapper.selectUserInfo2());
    }

在这里插入图片描述

开启驼峰命名

通常数据库列使用蛇形命名法进行命名(下划线分割各个单词),而 java 属性一般遵循驼峰命名法约定,为了在这两种命名方式之间启用自动映射,需要将 mapUnderscoreToCamelCase 设置为 true,进行如下配置即可。

//Mapper类
    @Select("select * from userinfo")
    List<UserInfo> queryAllUser();

//测试类
    @Test
    void queryAllUser() {
        System.out.println(userInfoMapper.queryAllUser());
    }
mybatis:
  configuration:
    map-underscore-to-camel-case: true

参数传递

将方法中的参数,传给SQL语句,使用 #{}或${}的方式获取方法中的参数。

#{} 和 ${}区别

案例

#{}:

    @Select("select * from userinfo where username = #{username}")
    List<UserInfo> getUserInfoByUsername(String username);
    
    @Test
    void getUserInfoByUsername() {
        System.out.println(userInfoMapper.getUserInfoByUsername("admin"));
    }

在这里插入图片描述
${}:

    @Select("select * from userinfo where username = ${username}")
    List<UserInfo> getUserInfoByUsername1(String username);
    
    @Test
    void testGetUserInfoByUsername1() {
        System.out.println(userInfoMapper.getUserInfoByUsername1("admin"));
    }

报错
在这里插入图片描述
修改:

    @Select("select * from userinfo where username = '${username}'")
    List<UserInfo> getUserInfoByUsername1(String username);
    
    @Test
    void testGetUserInfoByUsername1() {
        System.out.println(userInfoMapper.getUserInfoByUsername1("admin"));
    }

在这里插入图片描述

区别

预编译SQL和即时SQL

#{}:使用的是预编译SQL,通过?占位的方式,提前对SQL进行编译,然后把参数填充到SQL语句中,# 会根据参数类型,自动拼接引号" “。
${}:直接进行字符替换,一起对SQL进行编译,如果参数为字符串,需要加上引号” "。

#{}的效率更高

绝大多数情况下,某一条 SQL 语句可能会被反复调用执行,或者每次执行的时候只有个别的值不同,如果每次都需要经过 语法解析-SQL优化-SQL编译等,则效率就明显不行了。
预编译SQL,编译一次之后会将编译后的SQL语句缓存起来,后面再次执行这条语句时,不会再次编译(只是输入的参数不同),省去了解析优化等过程,以此来提高效率。

#{}更安全(防止sql注入)

因为${}会直接进行字符替换,一起对SQL进行编译,很有可能造成sql注入。

SQL注入:通过操作输入的数据来修改事先定义好的SQL语句,以达到执行代码对服务器进行攻击的方法。
' or 1='1,这就是一个经典的sql注入语句。

#会根据参数类型,如果参数类型为Sting,自动拼接引号" “,当不想要自动拼接引号” "时,选择使用${},就一定要考虑SQL注入。

示例(排序):

    @Select("select * from userinfo order by id ${sort}")
    public List<Userinfo> queryAllUser2(String sort);

此时${sort}拼接的是desc或者asc,但是这种其实也就两种情况,为了安全起见,最好还是将其写死,不需要参数引用,写为两个方法最好不过。

like查询

在这里插入图片描述

	@Select("select * from userinfo where username like '#${key}%'")
    List<UserInfo> queryAllUser3(String key);

    @Select("select * from userinfo where username like '%${key}%'")
    List<UserInfo> queryAllUser3(String key);

在这里插入图片描述
在这里插入图片描述

#{}会SQL错误,但是${}存在SQL注入的问题,所以不能直接使用 ${}解决办法,使用 mysql 的内置函数 concat()来处理。

    @Select("select * from userinfo where username like concat('%',#{username},'%')")
    List<UserInfo> queryAllUser4(String key);

在这里插入图片描述

传递多个参数

1.如果SQL语句中只有一个变量,变量名不需要与参数名对应。
2.如果SQL语句中有多个变量,变量名必需与参数名对应。
3.会自动生成对应参数比如param1自动对应函数的第一个参数,也可以通过 @Param,设置参数的别名,如果使用 @Param 设置别名, #{…}里面的属性名必须和@Param 设置的一样。

xml实现

配置对应的xml文件存放的位置,calsspath对应

简单插入示例–insert

首先,配置yml文件,将xml文件的路径配置在yml文件中。

mybatis:
  mapper-locations: classpath:mybatis/**Mapper.xml

其次,创建UserInfoXmlMapper接口,作为Mapper层,此时注意该Mapper接口所在的文件的路径。

package com.example.mybatis.mapper;

@Mapper
public interface UserInfoXmlMapper {
    Integer insert(UserInfo userInfo);
}

随后,xml的路径下创建xxMapper.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.mybatis.mapper.UserInfoXmlMapper">
    <insert id="insert">
        insert into userinfo (username, password, age, gender)
        values (#{username}, #{password}, #{age}, #{gender})
    </insert>
</mapper>

最后,进行测试。

    @Test
    void insert() {
        UserInfo userinfo = new UserInfo();
        userinfo.setUsername("laojun2");
        userinfo.setPassword("1234562");
        userinfo.setAge(12);
        userinfo.setGender(2);
        System.out.println(userInfoXmlMapper.insert(userinfo));

    }

在这里插入图片描述

简单删除示例–delete

    Integer deleteById(Integer id);

    @Test
    void deleteById() {
        System.out.println(userInfoXmlMapper.deleteById(14));
    }
    <delete id="deleteById">
        delete from userinfo where id = #{id}
    </delete>

在这里插入图片描述

简单修改示例–delete

    Integer updateById(UserInfo userInfo);

    @Test
    void updateById() {
        UserInfo userinfo = new UserInfo();
        userinfo.setId(1);
        userinfo.setPassword("admin");
        System.out.println(userInfoXmlMapper.updateById(userinfo));
    }
    <update id="updateById">
        update userinfo set password = #{password} where id = #{id}
    </update>

在这里插入图片描述

简单查询示例–delete

    List<UserInfo> selectAllUserInfo();

    @Test
    void selectAllUserInfo() {
        System.out.println(userInfoXmlMapper.selectAllUserInfo());
    }

这里需要注意的是resultType指定返回的类型。

    <select id="selectAllUserInfo" resultType="com.example.mybatis.model.UserInfo">
        select * from userinfo
    </select>

同时,还需要注意的是,此时我的配置文件中配置了自动驼峰转换。
在这里插入图片描述

多表查询

多表查询和单表查询类似,只是SOL不同而已。

准备工作

上面建了一张用户表,这里再建一张文章表,进行多表关联查询。

-- 创建⽂章表
DROP TABLE IF EXISTS articleinfo;
 
CREATE TABLE articleinfo (
	id INT PRIMARY KEY auto_increment,
	title VARCHAR ( 100 ) NOT NULL,
	content TEXT NOT NULL,
	uid INT NOT NULL,
	delete_flag TINYINT ( 4 ) DEFAULT 0 COMMENT '0-正常, 1-删除',
	create_time DATETIME DEFAULT now(),
	update_time DATETIME DEFAULT now() 
) DEFAULT charset 'utf8mb4';

INSERT INTO articleinfo ( title, content, uid ) VALUES ( 'Java', 'Java正⽂', 1);

对应实体类:

@Data
public class ArticleInfo {
	private Integer id;
	private String title;
	private String content;
	private Integer uid;
	private Integer deleteFlag;
	private Date createTime;
	private Date updateTime;
}

原始sql语句:根据uid查询作者的名称等相关信息

SELECT
	ta.id,
	ta.title,
	ta.content,
	ta.uid,
	tb.username,
	tb.age,
	tb.gender 
FROM
	articleinfo ta
	LEFT JOIN userinfo tb ON ta.uid = tb.id 
WHERE
	ta.id =1

补充实体类:

@Data
public class ArticleInfo {
	private Integer id;
	private String title;
	private String content;
	private Integer uid;
	private Integer deleteFlag;
	private Date createTime;
	private Date updateTime;
	//⽤⼾相关信息
	private String username;
	private Integer age;
	private Integer gender;
}

Mapper接口:

@Mapper
public interface ArticleInfoMapper {
	@Select("SELECT ta.id,ta.title,ta.content,ta.uid,tb.username,tb.age,tb.gender " +
	"FROM articleinfo ta LEFT JOIN userinfo tb ON ta.uid = tb.id " +
	"WHERE ta.id = #{id}")
	ArticleInfo queryUserByUid(Integer id);
}

其对应的xml未改一下就行。

动态SQL

< if >和< trim >

是存在非必填字段问题的解决方法。简单来说,< if >标签就是一个判断语句,test 里是要进行的判断,如果为true,则会填充标签里的内容。< trim >标签可以修饰< if >标签填充的不合法SQL语句。

< trim >标签属性:
prefix:表示整个语句块,以prefix的值作为前缀
suffix:表示整个语句块,以suffix的值作为后缀
prefixOverrides:表示整个语句块要去除掉的前缀
suffixOverrides:表示整个语句块要去除掉的后缀

  • 使用< trim >标签动态生成SQL语句:
    第一个< trim>处理列名部分
    第二个< trim>处理值部分
    自动去除末尾多余的逗号

  • 使用< if>条件判断:
    只有当字段不为null时(如username!=null)才会包含该字段
    gender字段比较特殊,只检查是否存在(不为空)而不判断null

  • 最终生成的SQL会根据传入参数动态变化,只包含非空的字段,避免插入null值。

<insert id="insert">
        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">
                gender
            </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">
                #{gender}
            </if>
        </trim>
    </insert>

如果只传了username和password,生成的SQL会是:

insert into userinfo(username, password) values(#{username}, #{password})

< where >

如果我们想根据 id,password 来对表进行删除操作,仅使用< if>标签和< trim>标签:

<delete id="delete">
        delete from userinfo
        <trim prefix="where" prefixOverrides="and">
            <if test="id!=null">
                id = #{id}
            </if>
            <if test="password!=null">
                and password = #{password}
            </if>
        </trim>
    </delete>

面对条件判断使用< where>标签可以化简,使用< where>标签对上述代码化简:

<delete id="delete1">
        delete from userinfo
        <where>
            <if test="id!=null">
                id = #{id}
            </if>
            <if test="password!=null">
                and password = #{password}
            </if>
        </where>
    </delete>

注意:

  1. < where>只会在子元素有内容的情况下才插入where子句
  2. 会自动去除子句的开头的AND或 OR

< set >

根据传入的用户对象属性来更新用户数据,可以使用标签来指定动态内容,与< where>标签的使用类似
根据传入的用户 id 属性,修改其他不为 null 的属性:

 <update id="update">
        update userinfo
        <set>
        	<if test="username!=null">
            	username=#{username}
        	</if>
        </set>
        where id=#{id}
    </update>

< set> :动态的在SQL语句中插⼊set关键字,并会删掉额外的逗号

< foreach >

对集合进行遍历时可以使用该标签。

  1. collection:绑定方法参数中的集合,如 List,Set,Map或数组对象
  2. item:遍历时的每一个对象
  3. open:语句块开头的字符串
  4. close:语句块结束的字符串
  5. separator:每次遍历之间间隔的字符串

根据多个id,删除用户数据

    <delete id="delete4">
        delete from userinfo
        where id in
        <foreach collection="ids" open="(" close=")" separator="," item="id" >
            #{id}
        </foreach>
    </delete>

< include >

在xml映射文件中配置的SQL,有时可能会存在很多重复的片段,此时就会存在很多冗余的代码。可以对重复的代码片段进行抽取,将其通过< sql >标签封装到一个SQL片段,然后再通过< include >标签进行引用。

< sql >:定义可重用的SQL片段
< include >:通过属性refid,指定包含的SQL片段

<sql id="baseColumns">
    id, 
    username, 
    age, 
    gender
</sql>

<select id="selectAllUser" resultType="com.example.myBatis.model.UserInfo">
    SELECT
        <include refid="baseColumns"/>
    FROM 
        userinfo
</select>

网站公告

今日签到

点亮在社区的每一天
去签到