前言
在MySQL的学习阶段,我们知道了如何使用JDBC去操作,也正是因为学习了JDBC也知道其操作的繁琐,每次的CRUD操作都需要从数据库连接池中去获取数据库连接,然后再编写SQL语句,并绑定对应的参数,接着通过连接执行SQL语句,如果返回值的话还得处理返回值,最后还得释放连接等资源。明明在增删改查阶段只需要修改SQL语句以及对应的参数和处理返回结果即可,但还是存在很多重复性的操作的。因此,Mybatis就由此而诞生了,Mybatis实现了对JDBC的进一步简化与封装,让Java开发者可以更加简单的去操作数据库。
Mybatis快速入门
Mybatis操作数据库的步骤
1、准备工作(创建SpringBoot项目、数据库表的准备、Java实体类的准备)
2、引入Mybatis的相关依赖,配置Mybatis(数据库连接信息)
3、编写SQL语句(使用注解或者xml)
4、测试代码
File -> New -> Project:
接下来创建对应的数据表以及填充数据:
-- 创建表[用户表]
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' );
创建Java中对应的实体类:
@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;
}
配置数据库连接:
spring:datasource:url: jdbc:mysql://127.0.0.1:3306/mybatis_test?characterEncoding=utf8&useSSL=falseusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
接下来就是编写查询SQL了(先使用看看效果,后面再学习)
@Mapper // 只能使用@Mapper注解
public interface UserInfoMapper { // 在mapper包下(属于数据层)@Select("select * from userinfo")List<UserInfo> selectAll();
}
接着在UserInfoMapper这个类所在的文件中,右键找到Generate,点击 Test
按照上述步骤,就会生成一个测试类:
@SpringBootTest // 加载spring的运行环境(为了DI)
class UserInfoMapperTest {@Autowiredprivate UserInfoMapper userInfoMapper;@Test// 标记该方法为测试方法,可以直接运行void selectAll() {List<UserInfo> userInfos = userInfoMapper.selectAll();System.out.println(userInfos);}
}
我们只需要在@Test注解的左侧点击run即可运行该方法(运行结果如下)
从上述步骤来看,从编写SQL语句到测试的步骤非常简单。 所以的一切都是spring帮我们做好了,我们只需要使用即可。
注意:
1、只有查询的SQL中对应的字段才会在对象对应的属性中有值。但细心的小伙伴可能会发现,删除标志、创建时间、更新时间 也同样没有值,这个我们后面再学习。
2、Mybatis也是一个框架,是独立于Spring框架的,Mybatis是对JDBC的封装,即JDBC在Maven项目中可以使用,同样Mybatis也能在Maven项目中使用。但要注意的是上面的依赖是只能在SpringBoot项目中使用,在Maven项目使用的依赖并不是长那样。
既然已经知道了Mybatis框架的优点,接下来就具体学习Mybatis是如何操作数据库的。
Mybatis的基础操作(注解实现接口类)
使用Mybatis操作数据库的话,我们都是创建一个mapper包用来存放具体的操作接口,然后在对应的接口中去编写方法。接着就只需要Mybatis来实现该接口方法,然后我们再将该接口类注册到Spring的容器中,使用容器来实现依赖注入,接着就是调用对应的操作方法即可。 Mybatis 实现 该接口方法的方式有两种:1、使用注解;2、使用xml。我们先来学习简单的注解。
打印日志
在Mybatis中,我们可以使用日志,来查看SQL语句的执行、参数的传递,以及执行的结果。因此我们需要先配置:
# 配置Mybatis日志
mybatis:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
查询
如果我们想要查询数据库中的数据,只需要创建一个接口方法,并在上方加上@Select注解,里面加上查询的语句即可。
需求:查询id=1的用户
@Mapper // 只能使用@Mapper注解
public interface UserInfoMapper { // 在mapper包下(属于数据层)@Select("select * from userinfo")List<UserInfo> selectAll();@Select("select * from userInfo where id=1")UserInfo selectById();}
测试类
@SpringBootTest // 加载spring的运行环境(为了DI)
class UserInfoMapperTest {@Autowiredprivate UserInfoMapper userInfoMapper;@Test// 标记该方法为测试方法,可以直接运行void selectAll() {List<UserInfo> userInfos = userInfoMapper.selectAll();System.out.println(userInfos);}@Testvoid selectById() {UserInfo Uer1=userInfoMapper.selectById();}
}
结果
虽然上述方式能够查询id = 1的数据,但是这种方法是写死的,即通过手动指定来查询的数据,但在实际开发中,往往是需要动态的数值。这里就需要用到 "#{参数名}" 的方式了。
@Select("select * from userInfo where id=#{id}")UserInfo selectById2(Integer id);
测试类
void selectById2() {UserInfo Uer2=userInfoMapper.selectById2(2);System.out.println(Uer2);}
结果
注意:
1、如果接口方法里面的形参只有一个时,#{参数名},这里的参数名可以是任意值。
2、如果想改名的话,需要通过@Param注解将修改后的参数名写入其中。

解决方法:
@Select("select id, username, `password`, age, gender, phone, delete_flag as
deleteFlag, " +"create_time as createTime, update_time as updateTime from user_info")
public List<UserInfo> queryAllUser();
@Select("select id, username, `password`, age, gender, phone, delete_flag,
create_time, update_time from user_info")
@Results({@Result(column = "delete_flag",property = "deleteFlag"),@Result(column = "create_time",property = "createTime"),@Result(column = "update_time",property = "updateTime")
})
List<UserInfo> queryAllUser();
@Select("select id, username, `password`, age, gender, phone, delete_flag,
create_time, update_time from user_info")
@Results(id = "resultMap",value = {@Result(column = "delete_flag",property = "deleteFlag"),@Result(column = "create_time",property = "createTime"),@Result(column = "update_time",property = "updateTime")
})
List<UserInfo> queryAllUser();
@Select("select id, username, `password`, age, gender, phone, delete_flag,
create_time, update_time " +"from user_info where id= #{userid} ")
@ResultMap(value = "resultMap")
UserInfo queryById(@Param("userid") Integer id);
开启驼峰命名(推荐)
mybatis:configuration:map-underscore-to-camel-case: true #配置驼峰⾃动转换
驼峰命名规则: abc_xyz => abcXyz• 表中字段名:abc_xyz• 类中属性名:abcXyz
增加
增加数据,是通过@Insert注解实现的,只需要在里面加上对应的SQL语句即可。
上述这种方式是比较繁琐的,我们可以使用传输对象的方式:
Insert语句默认返回的是受影响的行数,但在有些情景下,数据插入之后,需要获取的新插入的数据的id,提供给后续的操作。例如,订单系统的中的下完订单之后,后续的物流系统、库存系统等都需要拿到订单的id来进行操作。如果想要拿到自增的id,需要在接口方法上添加Options注解。
获取自动生成的主键值:通过Options注解来获取,其属性useGeneratedKeys=true表示使用数据库自增主键,keyColumn用于指定数据库table中的主键,keyProperty用于指定传入对象的属性(绑定到对象的哪个属性)。如果我们已经在数据库表中指定了主键,并且数据库中主键的名称和Java属性的名称是相同的,那么keyColumn属性可以省略。
@Options(useGeneratedKeys = true, keyProperty = "id")@Insert("insert into userinfo (username, password, age, gender) values (#{username},#{password},#{age},#{gender})")Integer insert(UserInfo userInfo);
测试代码
@Testvoid insert() {UserInfo userInfo = new UserInfo();userInfo.setUsername("zhaoliu");userInfo.setPassword("zhaoliu");userInfo.setAge(18);userInfo.setGender(1);Integer result = userInfoMapper.insert(userInfo);System.out.println("result: "+result + ",id:"+userInfo.getId());}
删除
@Delete("delete from userinfo where id = #{id}")Integer delete(Integer id);
更新
更新数据,是通过@Update注解实现的,只需要在里面加上对应的SQL语句即可。
@Update("update userInfo set username = #{username} where id = #{id}")Integer update(UserInfo userInfo);
void update() {UserInfo userInfo=new UserInfo();userInfo.setUsername("lisi");userInfoMapper.selectById2(2);Integer result=userInfoMapper.update(userInfo);System.out.println(result);}
XML实现接口类
快速上手
配置数据源和导入依赖与前面是一样的。下面是不一样的配置:
1、配置 Mybatis 的 xml文件路径
mybatis: # classpath 表示resources目录下,整体表示的是 resources/mapper/所有xml文件# * 代表通配符,表示只要是 .xml 文件即可mapper-locations: classpath:mapper/*.xml
第二步同样还是创建接口类,第三步不再是通过注解来实现SQL语句了,而是通过在配置的XML文件目录下创建XML文件,在文件中编写对应的实现SQL语句。
在新创建的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">
<!-- namespace 表示的该xml文件实现的接口类的全限定名称 -->
<mapper namespace=""></mapper>
这里的namespace的值就是UserInfoMapperXML文件的全限定类名。
例如:com.example.demo.UserInfoMapperXML
这里再介绍一个插件: mybatisx,
安装该插件之后,会有两个好处:
1、知道XML文件中的mapper标签的namespace值是否绑定正确。如果绑定正确的话,对应的文件位置就会出现小鸟图标,点击该图标可以在绑定的文件之中任意跳转。
2、插件会自动校验对应的接口类的接口方法是否生成了对应的实现标签。如果生成对应的实现标签的话,就会在对应的标签和方法之间生成小鸟图标,同样可以实现自动跳转,反之,如果没有生成的话,就会在对应的接口类所在的方法上爆红。
接下来,就可以在标签中编写对应的SQL语句了。
<select id="select" resultType="com.example.demo.UserInfo">select * from userinfo;</select>
最后就是测试对应的SQL语句了,同样在接口类中,鼠标右键 -> Generate -> Test:
查询
根据id查询:
System.out.println(userInfoMapperXML.select());
<select id="selectbyid" resultType="com.example.demo.UserInfo">select * from userinfo where id = #{id};</select>
@Testvoid selectbyid() {System.out.println(userInfoMapperXML.selectbyid(3));}
根据 username 和 password 查询:
// 根据username和password查询
UserInfo selectByUsernameAndPassword(String username, String password);@Test
void selectByUsernameAndPassword() {System.out.println(userInfoMapperXML.selectByUsernameAndPassword("admin", "admin_123456"));
}
根据 对象 来查询:
// 根据对象来查询
UserInfo selectByUserInfo(UserInfo userInfo);<select id="selectByUserInfo" resultType="com.springboot.mybatisdemo2.pojo.UserInfo"><!-- 注意,这里#{}内部的是实体类的属性名,其余的都是数据库的字段 -->select * from user_info where username = #{username} and password = #{password}
</select>@Test
void selectByUserInfo() {UserInfo userInfo = new UserInfo();userInfo.setUsername("admin");userInfo.setPassword("admin_123456");System.out.println(userInfoMapperXML.selectByUserInfo(userInfo));
}
由于之前在配置文件中,设置过了驼峰自动转换,因此这里并不会出现对象属性为null的情况。现在我们来看在XML文件中,使用起别名和result标签如何解决。
<!-- 起别名 -->
<select id="selectAll2" resultType="com.springboot.mybatisdemo2.pojo.UserInfo">select id, username, password, age, gender, phone,delete_flag as deleteFlag,create_time as createTime,update_time as updateTime from user_info
</select><!-- id是给别的标签使用所引用的 type表示映射到哪个实体类上 -->
<resultMap id="resultMap" type="com.springboot.mybatisdemo2.pojo.UserInfo"><!-- 规范的写法是要将数据表中所有字段和实体类的属性相对应的,即使有些字段和属性是对应上的,也最好要加上,因为是规范id 表示主键,result表示普通字段--><id column="id" property="id"/><result column="username" property="username"/><result column="password" property="password"/><result column="age" property="age"/><result column="gender" property="gender"/><result column="phone" property="phone"/><result column="delete_flag" property="deleteFlag"/><result column="create_time" property="createTime"/><result column="update_time" property="updateTime"/>
</resultMap><!-- resultMap表示要引用的映射关系 -->
<select id="selectAll3" resultType="com.springboot.mybatisdemo2.pojo.UserInfo" resultMap="resultMap">select * from user_info
</select>
增加
新增数据,传递参数:
// 新增数据:传递参数
Integer insertByUsernameAndPassword(String username, String password);<insert id="insertByUsernameAndPassword">insert into user_info (username, password) values (#{username}, #{password})
</insert>@Test
void insertByUsernameAndPassword() {System.out.println(userInfoMapperXML.insertByUsernameAndPassword("444", "444"));
}
新增数据,传递对象:
// 新增数据:传递对象
Integer insertByUserInfo(UserInfo userInfo);<insert id="insertByUserInfo">insert into user_info (username, password) values (#{username}, #{password})
</insert>@Test
void insertByUserInfo() {UserInfo userInfo = new UserInfo();userInfo.setUsername("444");userInfo.setPassword("444");System.out.println(userInfoMapperXML.insertByUserInfo(userInfo));
}
新增数据,获取自增id:
// 新增数据:获取自增的id
Integer insertGetGeneratedKey(UserInfo userInfo);<insert id="insertGetGeneratedKey" useGeneratedKeys="true" keyProperty="id">insert into user_info (username, password) values (#{username}, #{password})
</insert>@Test
void insertGetGeneratedKey() {UserInfo userInfo = new UserInfo();userInfo.setUsername("444");userInfo.setPassword("444");System.out.println("受影响的行数: " + userInfoMapperXML.insertGetGeneratedKey(userInfo));System.out.println("获取自增的id: " + userInfo.getId());
}
删除
删除数据,传递id:
// 删除数据:传递参数
Integer deleteById(Integer id);<delete id="deleteById">delete from user_info where id = #{id}
</delete>@Test
void deleteById() {System.out.println(userInfoMapperXML.deleteById(40));
}
删除数据,传递对象:
// 删除数据:传递对象
Integer deleteByUserInfo(UserInfo userInfo);<delete id="deleteByUserInfo">delete from user_info where username = #{username} and password = #{password}
</delete>@Test
void deleteByUserInfo() {UserInfo userInfo = new UserInfo();userInfo.setUsername("admin");userInfo.setPassword("admin");System.out.println(userInfoMapperXML.deleteByUserInfo(userInfo));
}
更新
更新数据,传递参数
// 更新数据:传递参数
Integer updateByUsernameAndPassword(Long id, String username, String password);<update id="updateByUsernameAndPassword">update user_info set username = #{username}, password = #{password} where id = #{id}
</update>@Test
void updateByUsernameAndPassword() {System.out.println(userInfoMapperXML.updateByUsernameAndPassword(33 L, "admin", "admin"));
}
多表查询
由于实际开发中,多表查询的使用频率不是很高,因此这里不再演示了。 感兴趣可以看看别的博主文章
#{} 和 ${} 的区别
1 @Select("select username, `password`, age, gender, phone from user_info where
id= #{id} ")
2 UserInfo queryById(Integer id);
1 select username, `password`, age, gender, phone from user_info where id= ?
@Select("select username, `password`, age, gender, phone from user_info where id= ${id} ") 2 UserInfo queryById(Integer id);
可以看到, 这次的参数是直接拼接在SQL语句中了
@Select("select username, `password`, age, gender, phone from user_info where
username= #{name} ")
UserInfo queryByName(String name);
@Select("select username, `password`, age, gender, phone from user_info where
username= ${name} ")
UserInfo queryByName(String name);

@Select("select username, `password`, age, gender, phone from user_info where
username= '${name}' ")
UserInfo queryByName(String name);

从上⾯两个例⼦可以看出:#{} 使⽤的是预编译SQL, 通过 ? 占位的⽅式, 提前对SQL进⾏编译, 然后把参数填充到SQL语句中. #{} 会根据参数类型, ⾃动拼接引号 '' .${} 会直接进⾏字符替换, ⼀起对SQL进⾏编译. 如果参数为字符串, 需要加上引号 '' .参数为数字类型时, 也可以加上, 查询结果不变, 但是可能会导致索引失效, 性能下降.
预编译SQL,编译⼀次之后会将编译后的SQL语句缓存起来,后⾯再次执⾏这条语句时,不会再次编译(只是输⼊的参数不同), 省去了解析优化等过程, 以此来提⾼效率
@Select("select username, `password`, age, gender, phone from user_info where
username= '${name}' ")
List<UserInfo> queryByName(String name);
@Test
void queryByName() {List<UserInfo> userInfos = userInfoMapper.queryByName("admin");System.out.println(userInfos);
}
1 @Test
2 void queryByName() {
3 List<UserInfo> userInfos = userInfoMapper.queryByName("' or 1='1");
4 System.out.println(userInfos);
5 }

可以看出来, 查询的数据并不是⾃⼰想要的数据. 所以⽤于查询的字段,尽量使⽤ #{} 预查询的⽅式SQL注⼊是⼀种⾮常常⻅的数据库攻击⼿段, SQL注⼊漏洞也是⽹络世界中最普遍的漏洞之⼀.如果发⽣在⽤⼾登录的场景中, 密码输⼊为 ' or 1='1 , 就可能完成登录

@Select("select id, username, age, gender, phone, delete_flag, create_time,
update_time " +"from user_info order by id ${sort} ")
List<UserInfo> queryAllUserBySort(String sort);
使⽤ ${sort} 可以实现排序查询, ⽽使⽤ #{sort} 就不能实现排序查询了.注意: 此处 sort 参数为String类型, 但是SQL语句中, 排序规则是不需要加引号 '' 的, 所以此时的 ${sort} 也不加引号

@Select("select id, username, age, gender, phone, delete_flag, create_time,
update_time " +"from user_info where username like '%#{key}%' ")
List<UserInfo> queryAllUserByLike(String key);
@Select("select id, username, age, gender, phone, delete_flag, create_time,
update_time " +"from user_info where username like concat('%',#{key},'%')")
List<UserInfo> queryAllUserByLike(String key);
数据库连接池
我们前面在学习JDBC时,每次增删改查操作都需要去手动创建连接,当SQL语句执行完毕之后,就需要去手动释放连接,整个过程是比较消耗资源的,特别是频繁创建与销毁连接时。前面学习多线程时,也是需要手动创建新线程,后来学习了如何使用线程池之后,就可以直接向线程池中拿线程,使用完之后,继续放到线程池中,这样的做法就是避免了频繁创建与销毁带来的时间开销。同样,数据库连接也可以存放到数据库连接池中,SpringBoot项目也是封装了数据库连接池:

<!-- springboot3.x版本 -->
<dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-3-starter</artifactId><version>1.2.21</version>
</dependency><!-- springboot2.x版本 -->
<dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.17</version>
</dependency>
再次启动测试用例,观察控制台的输出结果: