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;
}
企业开发规范
- 表名:字段名使用小写字母或数字,单词之间以下划线分割,尽量避免出现数字开头或者两个下划线,中间只出现数字。
- 表必备三字段:id(主键)、create_time(创建时间)、 update_time(更新时间),有同等含义的字段即可,字段名不做强制要求。
- 在表查询中,避免使用 * 作为查询的字段列表。
数据库配置
这里展示 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实现
简单插入示例–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>
注意:
- < where>只会在子元素有内容的情况下才插入where子句
- 会自动去除子句的开头的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 >
对集合进行遍历时可以使用该标签。
- collection:绑定方法参数中的集合,如 List,Set,Map或数组对象
- item:遍历时的每一个对象
- open:语句块开头的字符串
- close:语句块结束的字符串
- 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>