一. 什么是MyBatis?
- Mybatis是一个持久层框架,用于简化JDBC的操作
- MyBatis原本是Apache的一个开源项目ibatis,后来更名为MyBatis
上面我们提到了一个概念----持久层
不知道小伙伴们有没有想到五大注解的关系,类似于下图
其中MyBatis就是Mapper层的框架,是基于JDBC的封装,可以帮助我们更方便的操作数据库.
二. MyBatis入门
MyBatis操作数据库的步骤:
- 创建SpringBoot项目,准备数据库,实体类...
- 引入MyBatis依赖
- 编写SQL语句(注解/XML)
2.1 创建MyBatis项目
创建工程不必多说,但是这次我们要导入MyBatis的依赖
可以在创建项目时直接添加,也可以创建项目后使用Spring Boot Helper插件引入依赖.
下面演示一下创建项目后的引入依赖.
点开pom.xml文件->右键单击,选择Generate,就可以看到下面这样一个小框
选择第一个->点击OK,就可以看到创建项目时的引入依赖界面
接下来需要在SQL中找到MyBatis,因为此处博主操作的是MySQL,所以也需要引入MySQL Driver依赖.
然后就可以在pom.xml中看到引入的依赖了
这时候点击启动项目,会启动失败,提示信息显示没有指定数据源的URL,因此我们需要修改一下配置文件.
2.2 数据库准备&修改Spring配置
这里演示的是.yml配置文件,properties配置文件读者可自行操作.
在修改配置时,我们需要先创建一个数据库(博主创建的数据库名为config1114)
然后在.yml文件中添加下列项(同学们不必记忆,只需要复制粘贴即可):
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/config1114?characterEncoding=utf8&useSSL=false
username: root
password: '******'
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/*Mapper.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
为了演示方便, 这里博主创建了两个表:User表和Student表
接下来我们需要创建实体类,建立Java对象和数据库中表的映射
//Student表对应的实体类
@Data
public class Student {
private Integer id;
private String name;
private Integer age;
private Date createTime;
private Date updateTime;
}
//User表对应的实体类
@Data
public class User {
private Integer id;
private String name;
private Integer age;
}
2.3 写持久层代码
持久层操作数据库有两种方式:
1. 使用注解
2. 使用xml文件
我们简单演示一下第一种,先创建一个持久层的接口UserMapper
@Mapper
public interface UserMapper {
@Select("select * from user")
List<User> selectAll();
}
@Mapper是ibatis(MyBatis的前身)提供的注解,表示MyBatis创建了这个对象,并将该对象交给Spring管理.
这里的Mapper必须是接口类型,因为它的方法不是由程序猿实现的,而是交给MyBatis实现
2.4 测试持久层方法
我们知道,在启动SpringBoot项目后,需要前端进行访问才可以调用后端写的方法,这样做未免太过麻烦.我们可以直接使用单元测试类解决.
右键Generate->点击test
按照上图进行操作,这是我们会发现test目录下多了一个对应的UserMapperTest类
下面是这个类的代码.
@SpringBootTest//测试类进行方法测试时需要启动Spring容器
@Slf4j//使用该注解方便进行日志打印
class UserMapperTest {
@BeforeEach
void setUp() {
log.info("所有测试方法执行前执行");
}
@AfterEach
void tearDown() {
log.info("所有测试方法执行后执行");
}
@Autowired
private UserMapper userMapper;//注入userMapper对象
@Test
void selectAll() {
List<User> userList=userMapper.selectAll();
log.info("查询全部User:{}",userList);
}
}
下面我们点击selectAll方法旁边的倒三角进行测试(Test类的每个方法都可以独立运行).
可以看到打印日志上显示了全部的查询结果(博主自己提前加的数据).
三. 使用注解实现方法
3.1 参数传递
刚才我们简单演示了使用注解查询user表中的全部数据,如果想要指定查询条件该怎么办呢?
这就需要借助#{}向SQL语句中传递参数.
@Select("select * from user where id=#{id}")//第一个id是user表中的字段名,第二个id是传递的参数名
User selectById(Integer id);
现在在测试类中针对selectById()生成一个测试方法.
@Test
void selectById() {
User user=userMapper.selectById(5);
log.info("根据Id查询user:{}",user);
}
可以看到查询结果(也是博主自己偷偷加的数据).
可能有些友友疑惑这个SQL语句是从哪里来的?这实际上是yml配置文件中设置的,用于开发环境中,帮助程序猿调试代码.
同时也可以看到 ,上面的SQL语句和我们使用JDBC时的语句是一样的,MyBatis框架是对JDBC进行了封装.
- 如果SQL语句只需要传递一个参数,则这个参数可以任意命名(但不推荐).
这是再次使用运行测试方法,仍然能拿到刚才的数据
- 可以使用@Param给传递的参数设置别名,这时SQL中的名字必须和注解内的名字对应.
@Select("select * from user where name=#{userName}")//name是user表中的字段名,userName是@Param中的参数
User selectByName(@Param("userName") String name);
针对这个方法生成一个测试方法.
@Test
void selectByName() {
User user=userMapper.selectByName("李白");
log.info("根据name查询user:{}",user);
}
下图为查询结果.
3.2 @Insert
刚才我们展示的是@Select注解,现在来学习Insert操作.
@Insert("insert into user(name,age) values(#{name},#{age})")//name是传递的user对象的name属性,age是user对象的age属性
int insertUser(User user);
- insert,delete,updata这些更新数据库的操作都默认返回int表示表中更新的行数.(实际上是MySQL返回的)
针对这个方法生成一个测试方法.
@Test
void insertUser() {
User user=new User();
user.setName("薛宝钗");
user.setAge(21);
log.info("插入user结果:{}",userMapper.insertUser(user));
}
我们可以根据打印日志查看插入结果.
- 如果传递的参数是一个对象,并且使用了@Param注解,那么#{}中的属性名就必须是对象名.属性的形式
这时候我们再次运行测试方法,会抛出异常
需要对原方法进行更改.
再次运行测试方法,即可运行成功
上面的步骤中,我们只是向数据库插入了一条记录,但是不知道这条记录的主键id,下面演示一下如何获取自增主键,需要借助另一个注解@Options
@Insert("insert into user(name,age) values(#{name},#{age})")
@Options(useGeneratedKeys = true,keyProperty = "id")//此处的id对应的是Java对象的属性名,表示将user表中的自增主键赋给哪个属性
int insertUserWithGetId(User user);
为该方法创建测试方法
@Test
void insertUserWithGetId() {
User user=new User();
user.setName("史湘云");
user.setAge(19);
userMapper.insertUserWithGetId(user);
log.info("插入user:{}",user);
}
可以从打印日志中看到插入该对象后的Id
3.3 @Delete
童靴们肯定都熟悉这个套路了,下面直接进行演示.
Mapper方法实现
@Delete("delete from user where id=#{id}")
int deleteById(Integer id);
测试方法实现
@Test
void deleteById() {
int result=userMapper.deleteById(20);
log.info("删除条目:"+result);
}
测试结果
3.4 @Update
Mapper方法实现
@Update("update user set name=#{name},age=#{age} where id=#{id}")
int updateById(User user);
测试方法实现
@Test
void updateById() {
User user=new User();
user.setName("张爱莲");
user.setAge(28);
user.setId(21);
int result=userMapper.updateById(user);
log.info("更新条目: "+result);
}
测试结果
当然了,如果不放心可以直接查询数据库查看是否更新完成.
3.5 @Select
同学们肯定会疑惑,我们不是一开始演示的就是查询操作嘛.
刚才我们的查询操作中,Java对象的属性名和user表中的字段名是完全一致的,所以从user表中查询的字段值可以直接赋给对象的成员变量,但是如果二者不一致该怎么办呢?
下面有三种解决办法:
- 将查询的数据库字段重命名
- 使用@Results注解
- 修改yml配置文件(推荐)
这时我们借助的是Student表,对应的实体类属性见右图.
如果我们按照原来的方式进行查询 :
先创建一个StudentMapper接口.
@Mapper
public interface StudentMapper {
@Select("select * from student")
List<Student> selectAll();
}
在创建一个对应的测试类并在该类中注入StudentMapper接口.
@SpringBootTest
@Slf4j//使用Slf4j提供的对象进行日志打印
class StudentMapperTest {
@Autowired
private StudentMapper studentMapper;//注入StudentMapper对象
@Test
void selectAll() {
List<Student> studentList=studentMapper.selectAll();
log.info("查询全部Student:{}",studentList);
}
}
点击方法旁边的倒三角,可得到下图中类似的查询结果(我插入的)
可以看到,所有查询出来的对象,其createTime和updateTime成员的值均为null,这是因为没有与数据库中查询的字段对应.
先来演示第一种----对查询的数据库字段重命名
@Select("select id,name,age,create_time as createTime,update_time as updateTime from student")
List<Student> selectAll();
再来运行测试方法,可以看到没有createTime和updateTime属性都被赋上了值.
同学们肯定会觉得,select后面跟上数据库中的字段名未免过于麻烦,不如直接使用*简单,但是实际开发过程中,我们最好还是使用前者,因为*所查询的数据量往往是非常大的,这就导致了效率变低.
下面演示第二种方法----使用@Results注解
@Select("select * from student")
@Results({@Result(column = "create_time",property = "createTime"),
@Result(column = "update_time",property = "updateTime")})
//column对应的是数据库中的字段名,property对应的是Java对象中的属性名
List<Student> selectAll2();
生成该方法的测试方法
@Test
void selectAll2() {
List<Student> studentList=studentMapper.selectAll2();
log.info("查询全部Student:{}",studentList);
}
可以看到,createTime和updateTime成员变量同样拿到了对应的值
如果别的方法也需要使用这种映射关系的话,只需给@Results注解添加一个id值,然后借助@ResultMap注解实现复用
@Select("select * from student where id=#{id}")
@ResultMap("map")
Student selectById(Integer id);
生成测试方法
@Test
void selectById() {
Student student=studentMapper.selectById(1);
log.info("根据id查询Student:{}",student);
}
接下来展示第三种,也是最简单的一种----借助yml配置文件解决
我们只需要给.yml配置文件添加一项信息
重新编写一次查询方法
@Select("select * from student")
List<Student> selectAll3();
@Test
void selectAll3() {
List<Student> studentList=studentMapper.selectAll3();
log.info("查询全部Student:{}",studentList);
}
可以看到,这次即便我们什么也不做,同样获取到了响应的值
这里不得不强调一下企业的命名规范:
- 表名,字段名使用小写字母或数字,单词之间以下划线分割
- Java成员变量名,使用小驼峰的形式,第一个单词首字母小写,另外的单词首字母大写
- Java类名,使用大驼峰的形式,所有单词的首字母均大写
四. 使用XML文件实现方法
学习了使用注解的方式操作数据库,与JDBC相比是不是灰常简单!但是这种方式只能完成一些简单的SQL语句,如果想要实现复杂的功能,需要借助XML文件.
4.1 配置文件&XML文件准备
如果想借助XML实现接口的方法,我们需要先告诉配置文件XML文件的位置.
我们现在resources目录下建一个mapper目录,然后建一个与UserXMLMMapper对应的xml文件.
然后在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">
<!--与这个xml文件对应的接口-->
<mapper namespace="com.example.demo.mapper.UserXMLMapper">
</mapper>
细心的同学会发现我的idea界面上有两个魂斗罗图标,这实际上是MyBatisX插件.童靴们自行在Idea上安装即可.
MyBatisX是基于MyBatis的又一层封装.
4.2 <insert>
刚才我们已经准备好了一个接口UserXMLMapper,现在尝试实现一个增加user的方法.
@Mapper
public interface UserXMLMapper {
int insertUser(User user);
}
在接口中定义好方法后,需要在xml文件中添加一项标签<insert>.
如果我们的操作正确,会发现MyBatisX插件自动给我们添加了一对小鸟,点击它们中任意一个可以跳转到对方的位置.
接下来在<insert>标签中编写SQL语句实现这个方法,与注解中的SQL语句一样,参数需要使用#{}传递.
<insert id="insertUser">
insert into user(name,age) values(#{name},#{age})
</insert>
接着对这个方法进行单元测试.
@SpringBootTest
@Slf4j
class UserXMLMapperTest {
@Autowired
private UserXMLMapper userXMLMapper;//注入UserXMLMapper对象
@Test
void insertUser() {
User user=new User();
user.setName("欧阳修");
user.setAge(78);
int result=userXMLMapper.insertUser(user);
log.info("增加条目:{}",result);
}
}
我们可以在日志打印上发现结果.
在讲解@Insert的时候,我们提到可以使用@Options获取自增主键,那么在xml文件中如何获取呢?
需要给<insert>标签添加额外的参数.
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
insert into user(name,age) values(#{name},#{age})
</insert>
细心的童靴会发现这两个参数与@Options中的参数是一样滴.
这次我们插入对象后打印该对象.
@Test
void insertUser() {
User user=new User();
user.setName("欧阳修");
user.setAge(78);
int result=userXMLMapper.insertUser(user);
log.info("增加条目:{}",result);
log.info("插入对象:{}",user);
}
这样就拿到了自增id
4.3 <delete>
话不多说,我们直接cue流程.
UserXMLMapper方法定义.
int deleteById(Integer id);
<delete>标签实现.
<delete id="deleteById">
delete from user where id=#{id}
</delete>
测试方法编写.
@Test
void deleteById() {
int result=userXMLMapper.deleteById(23);
log.info("删除条目:"+result);
}
4.4 <update>
UserXMLMapper方法定义.
int updateById(User user);
<update>标签实现
<update id="updateById">
update user set name=#{name},age=#{age} where id=#{id}
</update>
单元测试方法实现.
@Test
void updateById() {
User user=new User();
user.setId(13);
user.setName("西施");
user.setAge(24);
int result=userXMLMapper.updateById(user);
log.info("更新条目:"+result);
}
上述的update方法,我们是根据user对象中的id找到了这个记录并进行了修改.当然,我们可以额外传递一个id指定某条记录进行更新.
int updateById2(User user,Integer id);
<update id="updateById2">
update user set name=#{name},age=#{age} where id=#{id}
</update>
编辑并运行下面的测试方法.
@Test
void updateById2() {
User user=new User();
user.setName("林黛玉");
user.setAge(19);
int result=userXMLMapper.updateById2(user,13);
log.info("更新条目:"+result);
}
这时候我们发现代码抛出了异常..仔细观察,是不是和使用@Param时没有指定对象名的异常类似?
这时候name和age字段名需要显式指定 对象名.属性名
<update id="updateById2">
update user set name=#{user.name},age=#{user.age} where id=#{id}
</update>
再次运行测试方法,通过日志可发现修改成功.
童靴们会不会这样想,是不是因为传入的参数id与User对象中的id属性名字发生冲突了,所以MyBatis才会找不到这个参数.
于是乎,博主将传入的id改了个名字iid
但是运行测试方法时,仍然抛出了同样的异常.
如果传入的是多个参数,需要以 对象名.属性名 的方式组织,否则MyBatis会找不到传递的参数.
4.5 <select>
前面方法的实现,标签中只需添加一个属性即可.但是<select>标签至少需要两个属性.
selectAll()方法定义.
List<User> selectAll();
<select>标签实现
<!-- id的值为对应的方法名,resultType指定返回的数据类型-->
<select id="selectAll" resultType="com.example.demo.entity.User">
select * from user
</select>
生成对应的测试方法.
@Test
void selectAll() {
List<User> userList=userXMLMapper.selectAll();
log.info("查询全部user:{}",userList);
}
同样地,当数据库中的字段与对象的属性不一致时,有三种办法来实现.
- 对数据库中查询的字段重命名
- 使用<resultMap>标签
- 开启驼峰命名
这次我们使用的依然是Student表.
第一种,第三种方法我们不再进行演示,与注解的使用是一样的.下文只演示第二种方法.
先来创建一个StudentXMLMapper类,并定义selectAll方法.
@Mapper
public interface StudentXMLMapper {
List<Student> selectAll();
}
再来配置xml文件,并生成对应的<select>标签.
<!-- id是这个resultMap标签的唯一标识,type是要映射的类-->
<resultMap id="map" type="com.example.demo.entity.Student">
<!-- id指定主键及主键对应的属性,result标签中的column指定数据库中的字段,property指定对象的属性名-->
<id column="id" property="id"></id>
<result column="create_time" property="createTime"></result>
<result column="update_time" property="updateTime"></result>
</resultMap>
<!-- 不能继续使用resultType属性了,修改为resultMap属性并指定对应id-->
<select id="selectAll" resultMap="map">
select * from student
</select>
生成对应的测试方法并运行.
@SpringBootTest
@Slf4j
class StudentXMLMapperTest {
@Autowired
private StudentXMLMapper mapper;
@Test
void selectAll() {
List<Student> list=mapper.selectAll();
log.info("查询全部Student:{}",list);
}
}
可以在打印结果中看到,createTime和updateTime正确地显示出来了.
五. 多表联合查询
上面的演示中,我们查询的都是单表,现在来演示一下如何查询多个表.
5.1 数据库准备
这次要用到的是user表和article表.
实际上,查询多个表和查询单个表是一样的操作,关键在于对应的实体类怎么定义.
我们需要在entity包下定义两个实体类对象.
@Data
public class User {
private Integer id;
private String name;
private Integer age;
}
@Data
public class Article {
private Integer id;
private String title;
private String content;
private Integer authorId;
private Date createTime;
private Date updateTime;
}
现在,我们的要求是-----查询全部Article并且显示与其对应的作者名.
于是我们需要定义一个视图类来建立与查询数据的映射.
@Data
public class ArticleView extends Article{
private String name;
}
下面创建一个ArticleVIewMapper类,然后用注解的方式编写SQL语句
@Mapper
public interface ArticleViewMapper {
@Select("select article.*,name from article,user where article.author_id=user.id")
List<ArticleView> selectAll();
}
创建测试类并生成对应的测试方法.
@SpringBootTest
@Slf4j
class ArticleViewMapperTest {
@Autowired
private ArticleViewMapper mapper;
@Test
void selectAll() {
List<ArticleView> viewList=mapper.selectAll();
log.info("查询结果:{}",viewList);
}
}
现在来测试一下.
可以看到,打印结果并不是很理想,我们想要的是Article部分的全部信息,因为ArticleView类是Article类的子类,所以@Data帮助我们写的toString()方法并没有打印Article的信息.
我们可以借助Idea重新生成一个toString()方法,这时代码在运行的时候会以我们写的方法为主.
@Data
public class ArticleView extends Article{
private String name;
@Override
public String toString() {
return "ArticleView{" +
"name='" + name + '\'' +
"} " + super.toString();
}
}
再次进行测试,就可以显示出全部的信息了.