Mybatis的基础操作就是通过Mybatis完成对数据的增删改查。我们通过例子来引入这些操作,之前的项目较久远,因此我们从零开始进行准备工作:
搭建项目
一、创建数据库user_list并插入数据:
-- 创建数据库
create table user_list
(id int unsigned primary key auto_increment comment '主键ID',username varchar(100) comment '用户名',password varchar(32) default '123456' comment '密码',gender tinyint unsigned comment '性别,1男2女',job tinyint unsigned comment '部门ID',creat_time datetime not null comment '创建时间',update_time datetime not null comment '修改时间'
) comment '基础信息表';
INSERT INTO user_list (username, password, gender, job, creat_time, update_time)
VALUES ('张三', 'zhangsan_pwd123', 1, 1, NOW() - INTERVAL 14 DAY, NOW()),('李四', 'lisi_secure_pwd', 2, 2, NOW() - INTERVAL 13 DAY, NOW()),('王五', 'wangwu_123456', 1, 3, NOW() - INTERVAL 12 DAY, NOW()),('赵六', 'zhaoliu_password', 2, 4, NOW() - INTERVAL 11 DAY, NOW()),('孙七', 'sunqi_pwd_2023', 1, 5, NOW() - INTERVAL 10 DAY, NOW()),('周八', 'zhouba_secure', 2, 1, NOW() - INTERVAL 9 DAY, NOW()),('吴九', 'wujiu_123', 1, 2, NOW() - INTERVAL 8 DAY, NOW()),('郑十', 'zhengshi_pwd', 2, 3, NOW() - INTERVAL 7 DAY, NOW()),('王十一', 'wangshiyi_securepwd', 1, 4, NOW() - INTERVAL 6 DAY, NOW()),('李十二', 'lishier_password1', 2, 5, NOW() - INTERVAL 5 DAY, NOW()),('张十三', 'zhangshisan_pwd2', 1, 1, NOW() - INTERVAL 4 DAY, NOW()),('刘十四', 'liushisi_secure3', 2, 2, NOW() - INTERVAL 3 DAY, NOW()),('陈十五', 'chenshiwu_1234', 1, 3, NOW() - INTERVAL 2 DAY, NOW()),('高十六', 'gaoshiliu_pwd5', 2, 4, NOW() - INTERVAL 1 DAY, NOW()),('陆十七', 'lushiqi_securepwd6', 1, null, NOW(), NOW());
二、创建一个新的Springboot项目,并引入对应的起步依赖(Mybatis、mysql驱动、lombok):
三、在默认配置文件application.properties中引入数据库连接信息:
#application.properties文件
spring.application.name=MybatisTest
#驱动类名称
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#数据库连接的url
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_database
#连接数据库的用户名
spring.datasource.username=root
#连接数据库密码
spring.datasource.password=1234
四、创建对应的实体类UserList(定义java类采用驼峰命名法):
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;/*author CHN*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserList {private int id; // 主键IDprivate String username; // 用户名private String password; // 密码private int gender; // 性别,1表示男,2表示女private int job; // 部门IDprivate LocalDateTime creatTime; // 创建时间private LocalDateTime updateTime; // 修改时间
}
五、创建对应的Mapper接口UserMapper:
import org.apache.ibatis.annotations.Mapper;/*author CHN*/
@Mapper
public interface UserMapper {
}
最终效果:
删除
前端发送删除请求以删除数据,为了明确要删除哪一条基础,我们通常选择的唯一的主键来查询并删除。
明确目标后在UserMapper接口中定义方法:先添加注解删除所需注解@Delete(),括号内补充对应的sql语句并以""包裹,并补充方法(方法体暂时略,下文介绍):
@Mapper
public interface UserMapper {@Delete("delete from user_list where id=17")public void deleteUserById();
}
但此时我们只能删除id为17的元素,不能满足需求,我们需要使该参数能动态的变化,即根据方法所接收的参数而变化,这里就要用到Mybatis提供的可变参数形式#{},{}内填入对应的参数。为方便测试,我们仍在测试文件中编写代码:
//UserMapper接口————————————————————————————
@Mapper//将其作为bean注入ioc
public interface UserMapper {
// 根据ID删除数据
@Delete("delete from user_list where id=#{targetId}")//可变参数
public void deleteUserById(Integer targetId);
}
//测试类————————————————————————————————————
@SpringBootTest
class MybatisTestApplicationTests {@Autowired//使用ioc中的bean创建实例private UserMapper userMapper;@Testvoid test() {userMapper.deleteUserById(15);}
}
执行test方法,返回数据库并刷新即可看到id为15的那条数据被删除
注意deleteUserById方法因为我们无需返回值所以设置为void方法,但实际上它是有返回值的,其返回值代表此次操作操作影响了几条数据。
如果该接口方法只传入一个普通参数,那#{}中的属性名可以随便写,但不建议知道就好
此时数据已被成功删除,但Mybatis底层到底执行了什么sql语句,执行结果是什么我们并不知道。在Mybatis框架中,我们可以借助日志来查看这一信息,日志默认是关闭的,想要开启则需在配置文件application.properties添加相关语句:
#配置Mybatis的日志,并将其输出到控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
之后再执行,可以看到控制台明显多了些语句,其中最重要的语句为:
==> Preparing: delete from user_list where id=?
==> Parameters: 16(Integer)
<== Updates: 1
- 第一行中的"?"为参数占位符,代表可变参数
- 第二行代表将Inreger类的参数16传入
- 第三行代表影响了一条数据
预编译SQL
这种使用了可变参数的方式为预编译SQL,即在数据库服务器上预编译一条SQL语句,并在之后的操作中多次绑定不同的参数来执行这条语句。这种方法不仅可以提高SQL执行的效率,还能增强安全性,特别是防止SQL注入攻击。
我们先来介绍如何提高执行效率:
提高效率:
我们java项目中编写sql语句后,需先连接上数据库并发送语句,数据库接收语句后还要进行语法解析、优化SQL、编译SQL才能执行。为了提高效率,其会将前三步结束后的数据缓存下来,如果下次执行相同语句则可跳过前三步,直接执行。
之前写的SQL语句preparing部分(Parameters部分为空):
delete from user_list where id=16;
delete from user_list where id=17;
delete from user_list where id=18;
每次执行语句都需执行一遍前三步,效率较低,使用可变参数后SQL语句preparing部分(Parameters部分依次为16(Integer)、17(Integer)、18(Integer)):
delete from user_list where id=?;-- 16、17、18依次传入替代?
因为语句相同,所以前三步只需执行一次即可,16、17、18依次传入并执行,与之前的执行结果相同。再来看防止SQL注入攻击:
SQL注入攻击
SQL注入攻击的原理是利用输入字段或URL参数等,将恶意SQL语句插入到应用程序的查询中。由于应用程序在执行SQL查询时,直接将用户输入的数据拼接到查询字符串中,未经充分验证或转义,攻击者可以通过构造特定的输入,使数据库执行意外的SQL语句。
我们来看一个例子:登录页面输入账号和密码,正确通过,错误则不通过,对应sql语句preparing部分为(Parameters部分为空):
select count(*) from user_list where username='用户名'and password='密码';
如果该数据正确则返回1,不正确则返回0,但如果在输入密码时为'or '1'='1呢,此时日志中SQL语句就会变为:
==> Preparing: select count(*) from user_list where username='用户名'and password=''or '1'='1';
==> Parameters:
<== Columns: count(*)
<== Row: 20
<== Total: 1
密码的判断部分永远为真,程序判断失效,这就是一个简单的SQL注入。
再来看预编译SQL(即使用了可变参数),日志中其sql语句就会变为:
==> Preparing: select count(*) from user_list where username=? and password=? ;
==> Parameters: 用户名(String),'or '1'='1(String)
<== Columns: count(*)
<== Row: 0
<== Total: 1
可以看到有两个'?',代表两个可变参数,Parameters部分的参数作为String传入,此时传入的参数就不会影响到程序执行,这就是防止SQL注入攻击。
参数占位符不止#{...}还有${...}
#{...}执行时,会将其替换成?并生成预编译SQL语句,后续参数自动替换。
${...}将该参数直接拼接在SQL语句中,存在一定的SQL注入风险,一般对表名或字段名进行设置时使用。
增加
先根据表结构编写执行插入操作的sql语句(因为id为自增,所以无需添加):
INSERT INTO user_list (username, password, gender, job, creat_time, update_time)
VALUES ('刘涛', 'liutao_password1', 1, 1, NOW() - INTERVAL 12 DAY, NOW() - INTERVAL 12 DAY),('王芳', 'wangfang_securepwd2', 2, 2, NOW() - INTERVAL 11 DAY, NOW() - INTERVAL 11 DAY);
再在UserMapper接口中添加对应方法和注解@Insert():
@Insert("INSERT INTO user_list (username, password, gender, job, creat_time, update_time)" +"VALUES ('刘涛', 'liutao_password1', 1, 1, NOW() - INTERVAL 12 DAY, NOW() - INTERVAL 12 DAY)," +" ('王芳', 'wangfang_securepwd2', 2, 2, NOW() - INTERVAL 11 DAY, NOW() - INTERVAL 11 DAY);")
public void insertUsers();
与上文相同,这样的参数不可变,需变为可变参数,也可以和上文一样声明参数并传递,但因为参数较多,会导致代码可读性较弱难以维护,因此我们选择将这些参数封装到一个对象中,即封装到之前创建的UserList类对象中:
@Insert("INSERT INTO user_list (username, password, gender, job, creat_time, update_time)" +"VALUES (#{username},#{password},#{gender},#{job},#{creatTime},#{updateTime});" )
public void insertUsers(UserList userList);
这样方法就已编写好了,我们继续在测试类中模拟前端发起请求:
@Testvoid test() {//自行添加无id参数的构造函数UserList target = new UserList("王芳", "wangfang_securepwd2", 2, 2, LocalDateTime.now(), LocalDateTime.now());userMapper.insertUsers(target);}
//控制台输出
==> Preparing: INSERT INTO user_list (username, password, gender, job, creat_time, update_time)VALUES (?,?,?,?,?,?);
==> Parameters: 王芳(String), wangfang_securepwd2(String), 2(Integer), 2(Integer), 2024-11-27T22:08:06.845000400(LocalDateTime), 2024-11-27T22:08:06.845000400(LocalDateTime)
<== Updates: 1
可以看到可变参数和参数一一对应并传入,实现了增加数据。
主键返回
主键返回指的是在数据库表中插入一条新记录后,将该记录的主键值返回。
我们以表多对多的关系为例,中间表关联着A和B表的id,当我们在A表新建一条数据时,需要得到该数据id的值并返回给中间表,以便建立多对多的关系。
实现方法:在添加数据的接口方法上添加注解@Options:
//UserMapper
@Mapper
public interface UserMapper {@Options(useGeneratedKeys = true,keyProperty = "id")@Insert("INSERT INTO user_list (username, password, gender, job, creat_time, update_time)" +"VALUES (#{username},#{password},#{gender},#{job},#{creatTime},#{updateTime});" )public void insertUsers(UserList userList);
}
//test类中添加getid()
@Test
void test() {UserList target = new UserList("测试用例", "wangfang_securepwd2", 2, 2, LocalDateTime.now(), LocalDateTime.now());userMapper.insertUsers(target);System.out.println(target.getId());
}
//控制台输出
<== Updates: 1
Closing non transactional SqlSession
50
执行结果显示控制台输出了新插入的数据的id为50。
修改
与删除类似:前端发送修改请求以修改数据,为了明确要修改哪一条基础,我们通常选择的唯一的主键来查询并修改。
先根据表结构编写执行修改操作的sql语句:
update user_list
set username='',password='',gender='',job='',update_time='',username=''
where id = 33;
再在UserMapper接口中添加对应方法和注解@Update(),与添加字段相同,我们将需要的参数封装到userList对象中:
//UserMapper接口————————————————————————————
@Update("update user_list set username=#{username},password=#{password},gender=#{gender},job=#{job},update_time=#{updateTime} where id = #{id};")
public void updateUsers(UserList userList);
//测试类————————————————————————————————————@Testvoid testUpdate() {UserList target = new UserList(33,"测试用例", "test2", 2, 2, LocalDate.now(), LocalDateTime.now());userMapper.updateUsers(target);}
//控制台输出
==> Preparing: update user_list set username=?,password=?,gender=?,job=?,update_time=? where id = ?;
==> Parameters: 测试用例(String), test2(String), 2(Integer), 2(Integer), 2024-11-28T01:39:21.615929600(LocalDateTime), 33(Integer)
<== Updates: 1
执行testUpdate方法,返回数据库并刷新即可看到id为33的那条数据被修改。
查询
同样根据唯一的主键来查询,执行查询操作的sql语句:
select *
from user_list where id=38;
再在UserMapper接口中添加对应方法和注解@Select(),注意之前的方法返回值都是void,查询方法则应返回对应的类:
//UserMapper接口————————————————————————————@Select("select * from user_list where id=#{id};")public UserList getUserById(Integer id);
//测试类————————————————————————————————————@Testvoid testSelect() {UserList target = userMapper.getUserById(38);System.out.println(target);}
//控制台输出
==> Preparing: select * from user_list where id=?;
==> Parameters: 38(Integer)
<== Columns: id, username, password, gender, job, creat_time, update_time
<== Row: 38, 郑十, zhengshi_pwd, 2, 3, 2024-11-19 22:53:11, 2024-11-26 22:53:11
<== Total: 1
UserList(id=38, username=郑十, password=zhengshi_pwd, gender=2, job=3, creatTime=null, updateTime=null)
此时数据虽已被查询出来,但最后两个字段却为空,这是因为有关该字段java类中和数据库中的名字不一样,一个为creatTime,一个为creat_time。
因为如果实体类属性名和数据库查询返回的字段名一样,Mybayis会自动封装,不一样则不会封装。
有两种较笨重的解决方法:
- 在sql语句中给名称不一样的字段起别名。
- 添加注解@Results和@Result。
//方法一:起别名使字段名相同
@Select("select id, username, password, gender, job," +"creat_time creatTime, update_time updateTime from user_list where id=#{id};")
public UserList getUserById(Integer id);
//方法二:添加注解Results
@Results({@Result(column="creat_time",property="creatTime"),@Result(column="update_time",property="updateTime")
})
@Select("select * from user_list where id=#{id};")
public UserList getUserById(Integer id);
//控制台输出
UserList(id=38, username=郑十, password=zhengshi_pwd, gender=2, job=3, creatTime=2024-11-19, updateTime=2024-11-26T22:53:11)
@Results()注解内部需要一个数组,数组内部则为@Result注解,@Result注解中column为原先的字段名,property为java类中的属性名。
这两种方法都比较臃肿,我们知道就好。
接下来介绍第三种方法:开启Mybatis的驼峰命名自动映射开关。
想要开启也需在配置文件application.properties添加相关语句:
#开启Mybatis的驼峰命名自动映射开关
mybatis.configuration.map-underscore-to-camel-case=true
这样类似于creat_time的字段就会自动被修改为creatTime。
条件查询
先来看sql语句:查询userList中名称中包含”十“,gender为1,job为3或5的人,查询结果根据创建时间倒序排序:
select *
from user_list
where username like '%十%'and gender = 1and job in (3, 5)
order by creat_time desc;
再在UserMapper接口中添加对应方法和注解@Select(),因为可能返回多条数据,所以应定义一集合接收返回数据,但要注意username为%十%,按照之前的方法应为username like '%#{name}%',但#{}不允许出现在''之中1,所以我们应使用${}替换:
//UserMapper接口————————————————————————————@Select("select * from user_list where username like '%${name}%' and gender = #{gender} and job in (#{job1}, #{job2}) order by creat_time desc;")public List<UserList> targetList(String name,int gender,int job1,int job2);
//测试类————————————————————————————————————@Testvoid testSelect() {List<UserList> targetList = userMapper.targetList("十", 1, 3, 5);System.out.println(targetList);}
//控制台输出
==> Preparing: select * from user_list where username like '%十%' and gender = ? and job in (?, ?) order by creat_time desc;
==> Parameters: 1(Integer), 3(Integer), 5(Integer)
<== Columns: id, username, password, gender, job, creat_time, update_time
<== Row: 45, 陆十七, lushiqi_securepwd6, 1, 5, 2024-11-26 22:53:11, 2024-11-26 22:53:11
<== Row: 43, 陈十五, chenshiwu_1234, 1, 3, 2024-11-24 22:53:11, 2024-11-26 22:53:11
<== Total: 2
[UserList(id=45, username=陆十七, password=lushiqi_securepwd6, gender=1, job=5, creatTime=2024-11-26, updateTime=2024-11-26T22:53:11),
UserList(id=43, username=陈十五, password=chenshiwu_1234, gender=1, job=3, creatTime=2024-11-24, updateTime=2024-11-26T22:53:11)]
但使用了${}又会导致性能低、sql注入等问题, 我们可以通过concat字符拼接函数来解决。
CONCAT
CONCAT是一个字符串连接函数,它主要用于将两个或多个字符串连接成一个字符串,语法为:CONCAT(string1, string2, ..., stringN)。
使用该函数就可以将%十%拆分为三部分,这样就可以使用#{}了:
//修改前:@Select("select * from user_list where username like '%${name}%' and gender = #{gender} and job in (#{job1}, #{job2}) order by creat_time desc;")public List<UserList> targetList(String name,int gender,int job1,int job2);
//修改后:@Select("select * from user_list where username like concat('%',#{name},'%') and gender = #{gender} and job in (#{job1}, #{job2}) order by creat_time desc;")public List<UserList> targetList(String name,int gender,int job1,int job2);
参数名说明
在SpringBoot的2.X版本,只需注意参数名和#{}中的参数名相同即可,但在早期版本,系统会将参数名编译为var1、var2,这时就需要使用注解@Param来标明该字段对应的#{}中的字段:
//老版本:@Delete("delete from user_list where id=#{targetId}")public void deleteUserById(@Param("targetId") Integer Id);
//现版本:@Delete("delete from user_list where id=#{targetId}")public void deleteUserById(Integer Id);
在上篇文章的引入的第三部分编写sql语句我们提到过:编写语句实现Mybatis有两种方式1、通过注解实现Mybatis,2、通过xml实现Mybatis,第一种已经在上文中讲过了,接下来看第二种方法。
XML映射文件
xml文件放在对应的resourse文件下,在Mybatis中定义xml映射文件需要遵守规范:
一、XML映射文件的名称与Mapper接口名称一致,且XML映射文件和Mapper接口应放在相同的对应包下(同包同名)。
右键Resources-New-Directory-输入org/example/mybatistest/Mapper-右键新建文件-New-FIle-UserMapperxml:
在定义java类时我们通常采用"."来标示不同级的目录,例如package1.package2.package3,但在resources中我们需要以"/"分隔,例如package1/package2/package3,其次就是需要保证两文件同包同名:
二、XML文件的namespace属性需与Mapper接口的全类名保持一致。
从Mybatis官网复制xml文件,并修改namesapce属性为自己的全类名(在UserMapper接口中右键接口名-Copy Reference即可获取全类名):复制"探究已映射的 SQL 语句"部分代码https://mybatis.net.cn/getting-started.html
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.example.mybatistest.Mapper.UserMapper"><select id="targetList"></select>
</mapper>
三、XML文件中的sql语句中的id需与Mapper接口中的方法名保持一致,并保持返回类型一致。
<!--UserMapper.xml文件:-->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.example.mybatistest.Mapper.UserMapper"><select id="targetList" resultType="org.example.mybatistest.pojo.UserList">select *from user_listwhere username like concat('%', #{name}, '%')and gender = #{gender}and job in (#{job1}, #{job2})order by creat_time desc;</select>
</mapper>
//UserMapper.java文件
@Mapper
public interface UserMapper {public List<UserList> targetList(String name,int gender,int job1,int job2);
}
//测试文件:
@SpringBootTest
class MybatisTestApplicationTests {@Autowiredprivate UserMapper userMapper;@Testvoid testSelect() {List<UserList> targetList = userMapper.targetList("十", 1, 3, 5);System.out.println(targetList);}
}
因为在UserMapper.java文件下方法名为targetList,因此select id="targetList" ,又因为返回类型需要保持一致,resultType即结果类型,应写全类名,所以resultType="org.example.mybatistest.pojo.UserList">,此时再执行testSelect即可看到返回值相同。
为什么要遵守这三点规范:我们通过Mybatis操作数据库是通过调用接口中的方法,再执行与该方法对应的sql语句即可完成操作,难点在于怎么找到与方法对应的sql语句。
之前通过注解操作时执行方法就是执行在方法上方注解中的sql语句,但使用xml文件时方法和sql语句是分开的,因此我们需要根据接口中的方法名找到对应的sql语句。
执行了以上三点规范执行方法后,Mybatis会自动查找“namespsce”与“该方法所在接口文件的全类名”相同的xml映射文件,并查找id与方法名相同的sql语句并执行,返回值为“resultType”与“该类所在文件的全类名”相同的类。
但因为方法和sql语句不在同一个地方,一旦有多个方法对应的语句会使我们难以判断相应的方法和sql语句在哪,MyBatisX插件可以帮助我们解决这一问题:
MyBatisX插件
MyBatisX为IntelliJ IDEA中的一款插件,它旨在简化MyBatis框架的使用,提升开发效率。MyBatisX能够根据数据库表结构自动生成Mapper接口、Mapper XML文件和Java实体类。这减少了手动编写重复代码的工作量,提高了开发效率。
并且该插件还会在接口类和xml文件左侧加上一一对应的快捷跳转按钮,以便我们查找对应语句在哪:
对于是使用注解还是xml映射,官方已对其做出说明:
使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java 注解不仅力不从心,还会让你本就复杂的 SQL 语句更加混乱不堪。 因此,如果你需要做一些很复杂的操作,最好用 XML 来映射语句。
选择何种方式来配置映射,以及认为是否应该要统一映射语句定义的形式,完全取决于你和你的团队。 换句话说,永远不要拘泥于一种方式,你可以很轻松的在基于注解和 XML 的语句映射方式间自由移植和切换。
有关namespace
在之前版本的 MyBatis 中,命名空间(Namespaces)的作用并不大,是可选的。 但现在,随着命名空间越发重要,你必须指定命名空间。
命名空间的作用有两个,一个是利用更长的全限定名来将不同的语句隔离开来,同时也实现了你上面见到的接口绑定。就算你觉得暂时用不到接口绑定,你也应该遵循这里的规定,以防哪天你改变了主意。 长远来看,只要将命名空间置于合适的 Java 包命名空间之中,你的代码会变得更加整洁,也有利于你更方便地使用 MyBatis。
命名解析:为了减少输入量,MyBatis 对所有具有名称的配置元素(包括语句,结果映射,缓存等)使用了如下的命名解析规则。
- 全限定名(比如 “com.mypackage.MyMapper.selectAllThings)将被直接用于查找及使用。
- 短名称(比如 “selectAllThings”)如果全局唯一也可以作为一个单独的引用。 如果不唯一,有两个或两个以上的相同名称(比如 “com.foo.selectAllThings” 和 “com.bar.selectAllThings”),那么使用时就会产生“短名称不唯一”的错误,这种情况下就必须使用全限定名。