注意: 开始本课程前,务必先完成MySQL视频课程、JavaWeb JDBC部分、Lombok视频课程学习。
在前面JDBC的学习中,虽然我们能够通过JDBC来连接和操作数据库,但是这实在是太麻烦了,哪怕只是完成一个SQL语句的执行,都需要编写大量的代码,更不用说如果我还需要进行实体类映射,将数据转换为我们可以直接操作的实体类型,JDBC虽然进行了接口的定义,但是还不够方便,我们需要一种更加简洁高效的方式来和数据库进行交互。
接下来隆重介绍我们本章节的主角:
MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Ordinary Java Object,普通的 Java对象)映射成数据库中的记录。
这一块内容很多很杂,各位小伙伴一定要多实践,多尝试,否则会忘得很快。
在正式使用Mybatis之前,我们先来学习一些前置内容和准备工作。
在开始介绍Mybatis之前,我们先来给大家介绍一下XML语言,XML语言发明最初是用于数据的存储和传输,它是由一个一个的标签嵌套而成,一般长这样:
<?xml version="1.0" encoding="UTF-8" ?>
<outer>
<name>阿伟</name>
<desc>怎么又在玩电动啊</desc>
<inner type="1">
<age>10</age>
<sex>男</sex>
</inner>
</outer>
如果你学习过前端知识,你会发现它和HTML几乎长得一模一样!但是请注意,虽然它们长得差不多,但是他们的意义却不同,HTML主要用于通过编排来展示数据,而XML主要是存放数据,它更像是一个配置文件!当然,浏览器也是可以直接打开XML文件的。
一个XML文件存在以下的格式规范:
type="1"
就是inner
标签的一个属性,属性的值由单引号或双引号包括。XML文件也可以使用注释:
<?xml version="1.0" encoding="UTF-8" ?>
<!-- 注释内容 -->
那如果我们的内容中出现了<
或是>
字符,那该怎么办呢?我们就可以使用XML的转义字符来代替:
如果嫌一个一个改太麻烦,也可以使用CD来快速创建不解析区域:
<test>
<name><![CDATA[我看你<><><>是一点都不懂哦>>>]]></name>
</test>
那么,我们现在了解了XML文件的定义,现在该如何去解析一个XML文件呢?比如我们希望将定义好的XML文件读取到Java程序中,这时该怎么做呢?
JDK为我们内置了一个叫做org.w3c
的XML解析库,我们来看看如何使用它来进行XML文件内容解析:
// 创建DocumentBuilderFactory对象
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// 创建DocumentBuilder对象
try {
DocumentBuilder builder = factory.newDocumentBuilder();
Document d = builder.parse("file:mappers/test.xml");
// 每一个标签都作为一个节点
NodeList nodeList = d.getElementsByTagName("test"); // 可能有很多个名字为test的标签
Node rootNode = nodeList.item(0); // 获取首个
NodeList childNodes = rootNode.getChildNodes(); // 一个节点下可能会有很多个节点,比如根节点下就囊括了所有的节点
//节点可以是一个带有内容的标签(它内部就还有子节点),也可以是一段文本内容
for (int i = 0; i < childNodes.getLength(); i++) {
Node child = childNodes.item(i);
if(child.getNodeType() == Node.ELEMENT_NODE) //过滤换行符之类的内容,因为它们都被认为是一个文本节点
System.out.println(child.getNodeName() + ":" +child.getFirstChild().getNodeValue());
// 输出节点名称,也就是标签名称,以及标签内部的文本(内部的内容都是子节点,所以要获取内部的节点)
}
} catch (Exception e) {
e.printStackTrace();
}
当然,学习和使用XML只是为了更好地去认识Mybatis的工作原理,以及如何使用XML来作为Mybatis的配置文件,这是在开始之前必须要掌握的内容(使用Java读取XML内容不要求掌握,但是需要知道Mybatis就是通过这种方式来读取配置文件的)
不仅仅是Mybatis,包括后面的Spring等众多框架都会用到XML来作为框架的配置文件。
那么我们首先来感受一下Mybatis给我们带来的便捷,就从搭建环境开始,中文文档网站:https://mybatis.org/mybatis-3/zh_CN/getting-started.html,还是老规矩,我们直接先下载好Mybatis的依赖Jar包:https://github.com/mybatis/mybatis-3/releases
如果使用Maven可以直接引入:
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.16</version>
</dependency>
依赖变多之后,我们可以将其放到一个单独的文件夹,不然会很繁杂:
依赖导入完成后,我们就可以编写Mybatis的配置文件了(现在不是在Java代码中配置了,而是通过一个XML文件去配置,这样就使得硬编码的部分大大减少,项目后期打包成Jar运行不方便修复,但是通过配置文件,我们随时都可以去修改,就变得很方便了,同时代码量也大幅度减少,配置文件填写完成后,我们只需要关心项目的业务逻辑而不是如何去读取配置文件)我们按照官方文档给定的提示,在项目根目录下新建名为mybatis-config.xml
的文件,并填写以下内容:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
我们发现,在最上方还引入了一个叫做DTD(文档类型定义)的东西,它提前帮助我们规定了一些标签以及这些标签应该具有哪些属性,这些标签是Mybatis专属的配置标签,我们也必须要按照对应的方式来进行配置。
接着我们就可以使用DTD中声明的各种标签进行配置的编写了,最外层需要使用configuration
标签进行囊括,内部首先需要编写环境配置,我们的项目可能会分多个环境进行编写,比如我们在开发的时候可能连接的是自己本地的数据库,但是项目部署到服务器上之后,可能会连接其他的数据库,所以我们可以提供多个环境配置,只不过这里我们只是做开发,只需要配置一个environment
即可:
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/web_study"/>
<property name="username" value="test"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
</configuration>
在environment
中我们可以配置两个部分,一个是事务管理器,还有一个是数据源,事务管理器我们这里直接使用JDBC即可,有关其他事务管理器我们会在后续Spring课程中为大家介绍。数据源中配置就是驱动、连接地址、用户名和密码,按照之前JDBC的方式进行配置即可,最后还有一个数据源类型配置我们也会放在后续Spring阶段再给各位小伙伴进行介绍。
配置文件完成后,接着我们就可以通过Java来使用Mybatis了,首先我们要介绍的是SqlSessionFactoryBuilder
它用于构建SqlSessionFactory
对象,每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的,使用也很简单,这里我们使用的是XML的形式进行配置的:
public static void main(String[] args) throws FileNotFoundException {
//使用build方法来创建SqlSessionFactory,这里我们通过文件输入流传入配置文件
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(new FileInputStream("mybatis-config.xml"));
}
虽然SqlSessionFactory也可以使用Configuration
进行纯Java配置,但是相比XML文件来说,配置起来非常麻烦,要构建大量对象,这里我们就不仅讲解了,各位小伙伴也可以前往官方文档深入了解:https://mybatis.org/mybatis-3/zh_CN/getting-started.html
public SqlSessionFactory build(InputStream inputStream);
//手动指定配置文件中多个环境的其中一个
public SqlSessionFactory build(InputStream inputStream, String environment);
//手动配置一些属性,通过Properties对象来传递
public SqlSessionFactory build(InputStream inputStream, Properties properties);
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties);
既然有了 SqlSessionFactory,顾名思义,我们可以从中获得 SqlSession 的实例,每一个SqlSession都代表一个会话,也就相当于我们之前通过命令行访问MySQL的一个窗口,不同会话之间相互隔离,不受影响:
try (SqlSession session = sqlSessionFactory.openSession(true)) {
//由于SqlSession需要在使用结束后关闭,这里我们也可以使用try-with-resource来编写
//这里的参数是设置自动提交,和之前JDBC一样
}
SqlSession接口中为我们预设了非常多的增删改查操作:
public interface SqlSession extends Closeable {
<T> T selectOne(String statement);
<T> T selectOne(String statement, Object parameter);
<E> List<E> selectList(String statement);
...
int insert(String statement);
int insert(String statement, Object parameter);
int update(String statement);
int update(String statement, Object parameter);
int delete(String statement);
int delete(String statement, Object parameter);
可以看到,针对于增删改查这类操作,Mybatis已经把每一种操作都预设好了,并且查询的返回结果直接就是我们需要的实体类型,相比JDBC来说方便太多了,那么我们该如何去使用这些方法呢?由于Mybatis并不知道我们具体需要执行的SQL语句,以及需要返回哪些数据作为结果,因此我们同样需要编写配置文件来告诉Mybatis我们要做什么。
我们可以在项目目录下创建一个新的mappers
目录,然后创建一个新的文件TestMapper.xml
作为我们的SQL语句映射配置,并添加以下内容:
<?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="testMapper">
</mapper>
这里我们添加了映射器(Mapper)的DTD来方便我们后续的配置,其中mapper标签用于囊括后续编写的所有SQL映射,namespace
属性用于区分不同的映射器(一个项目可以有多个Mapper配置文件,用于分类存放不同业务的SQL映射)以及后续我们会介绍的接口绑定等。接着我们就可以开始尝试编写一个测试用的SQL语句映射了,假设我们需要查询user表中所有数据,首先构建一个实体类:
@Data
public class User { //属性名称必须和数据库中字段名称一一对应,不然会赋值失败
int id;
String name;
int age;
}
接着我们在配置文件中添加一个新的映射,由于这里我们需要使用到select
语句,所以使用名字为select的标签:
<select id="selectAllUser" resultType="com.test.User">
select * from user
</select>
由于一个Mapper文件中可以存在多个SQL语句映射,这里的id
是用于区分其他SQL语句映射的,后面的resultType
代表SQL语句查询结果需要转换的实体类型,其他未使用的参数我们会在后续课程中逐步介绍。标签的中间就是我们具体要进行查询的SQL语句了,这和我们之前使用JDBC是差不多的。
现在我们已经完成了Mapper配置文件的编写,接着我们需要将其添加到一开始的Mybatis配置文件中,使得Mybatis可以在一开始的时候正常加载。
<configuration>
...
<mappers>
<mapper url="file:mappers/TestMapper.xml"/>
</mappers>
</configuration>
最后在程序中使用我们定义好的SQL语句映射也很简单,假设现在我们需要执行刚刚编写好的查询操作,只需要通过SqlSession提供的预设方法即可:
try (SqlSession session = sqlSessionFactory.openSession(true)) {
List<User> users = session.selectList("selectAllUser"); //直接填写我们刚刚编写的映射id
users.forEach(System.out::println); //直接查询并自动转换为对应类型
}
Mybatis非常智能,只需要配置一个映射关系,就能够直接将查询结果转化为一个实体类,属性会自动按照字段名称进行一一对应,免去了我们之前使用JDBC查询实体类的很多步骤。
从这一部分开始我们就正式来学习一下Mybatis的使用方式。
前面我们带各位小伙伴大概熟悉了一下Mybatis的配置流程,这一节我们来详细介绍一下如何配置查询操作。由于SqlSessionFactory
一般只需要创建一次,因此我们可以创建一个工具类来集中创建SqlSession
,这样会更加方便一些:
public class MybatisUtil {
//在类加载时就进行创建
private static SqlSessionFactory sqlSessionFactory;
static {
try {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(new FileInputStream("mybatis-config.xml"));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
/**
* 获取一个新的会话
* @param autoCommit 是否开启自动提交(跟JDBC是一样的,如果不自动提交,则会变成事务操作)
* @return SqlSession对象
*/
public static SqlSession openSession(boolean autoCommit){
return sqlSessionFactory.openSession(autoCommit);
}
}
现在我们只需要在main方法中直接使用工具类就能快速创建一个新的会话,然后查询结果了:
try(SqlSession sqlSession = MybatisUtil.openSession(true)) {
List<User> users = sqlSession.selectList("selectUser");
users.forEach(System.out::println);
}
查询操作在XML配置中使用一个select标签进行囊括,这里我们通过一个例子来介绍一下最基本的查询需要配置的参数,假设我们现在需要编写一个根据ID查询用户的操作,首先我们需要指定它的id,建议把id名称起的有代表性一点:
<select id="selectUserById">
</select>
接着是我们需要进行查询的参数,这里我们需要根据用户ID查询,那么传入的参数就是一个int类型的参数,参数也可以是字符串类型的,类型名称:
_int
这样前面添加下划线。String
、int
(Integer的缩写)、Long
当然,如果各位小伙伴觉得非常麻烦,我们也可以直接不填这个属性,Mybatis会自动判断:
<select id="selectUserById" parameterType="int">
</select>
接下来就是编写我们的SQL语句了,由于这里我们需要通过一个参数来查询,所以需要填入一个占位符,通过使用#{xxx}
或是${xxx}
来填入我们给定的属性,名称我们先随便起一个:
<select id="selectUserById" parameterType="int">
select * from user where id = #{id}
</select>
实际上Mybatis也是通过PreparedStatement
首先进行一次预编译,来有效地防止SQL注入问题,但是如果使用${xxx}
就不再是通过预编译,而是直接传值,因此对于常见的一些查询参数,我们一般都使用#{xxx}
来进行操作保证安全性。
最后我们查询到结果后,一般都是将其转换为对应的实体类对象,所以说这里我们之间填写之前建好的实体类名称,使用resultType属性来指定:
<select id="selectUserById" parameterType="int" resultType="com.test.User">
select * from user where id = #{id}
</select>
当然,如果你觉得像这样每次都要写一个完整的类名太累了,也可以为它起个别名,我们只需要在Mybatis的配置文件中进行编写即可:
<typeAliases>
<typeAlias type="com.test.User" alias="User"/>
</typeAliases>
也可以直接扫描整个包下的所有实体类,自动起别名,默认情况下别名就是类的名称:
<typeAliases>
<package name="com.test.entity"/>
</typeAliases>
这样,SQL语句映射配置我们就编写好了,接着就是Java这边进行调用了:
//这里我们填写刚刚的id,然后将我们的参数填写到后面
User user = session.selectOne("selectUserById", 1);
System.out.println(user);
这样就可以成功查询到ID为1的用户信息了:
可以看到Mybatis直接省去了我们之前使用JDBC读取ResultSet的部分,直接转换为对应的实体类,当然,如果你不需要转换为实体类,Mybatis也为我们提供了多种转换方案,比如转换为一个Map对象:
//使用Map类型变量进行接受,Key为String类型,Value为Object类型
Map<String, Object> user = session.selectOne("selectUserById", 1);
System.out.println(user);
我们可以尝试接着来写一个同时查询ID和年龄的查询操作,为了方便这里我们就不写parameterType属性了:
<select id="selectUserByIdAndAge" resultType="com.test.User">
select * from user where id = #{id} and age = #{age}
</select>
因为这里需要多个参数,我们可以使用一个Map或是具有同样参数的实体类来传递,显然Map用起来更便捷一些,注意key的名称需要与我们编写的SQL语句中占位符一致:
User user = session.selectOne("selectUserByIdAndAge", Map.of("id", 1, "age", 18));
System.out.println(user);
是不是感觉还是挺简单的?我们接着来看下面这种情况,实体类中定义的属性名称和我们数据库中的名称似乎有点不太一样,这会导致Mybatis自动处理出现问题:
@Data
public class User {
int uid;
String username;
int age;
}
运行后发现,Mybatis虽然可以查询到对应的记录,但是转换的实体类数据并没有被添加上去,这是因为数据库字段名称与类中字段名称不匹配导致的,我们可以手动配一个resultMap来解决这种问题,直接在Mapper中添加:
<select id="selectUserByIdAndAge" resultMap="user">
select * from user where id = #{id} and age = #{age}
</select>
<resultMap id="user" type="com.test.User">
<!-- 因为id为主键,这里也可以使用<id>标签,有助于提高性能 -->
<result column="id" property="uid"/>
<result column="name" property="username"/>
</resultMap>
这里我们在resultMap标签中配置了一些result标签,每一个result标签都可以配置数据库字段和类属性的对应关系,这样Mybatis就可以按照我们的配置来正确找到对应的位置并赋值了,没有手动配置的字段会按照之前默认的方式进行赋值。配置完成后,最终只需要将resultType改为resultMap并指定对应id即可,然后就能够正确查询了。
这里有一个RowBounds参数,用于实现分页效果,但是其分页功能是对查询到的数据进行划分,非常鸡肋,这里不进行介绍,了解即可。
我们再来尝试编写一下查询一个列表,查询列表时,resultType无需设置为list这种类型,而是使用List内部所包含的类型,所以这里还是填写com.test.User
类型或是Map类型:
<select id="selectUsers" resultType="com.test.User">
select * from user;
</select>
由于返回的结果是一个列表,这里我们需要使用selectList
方法来执行,如果使用之前的selectOne
会导致异常:
List<User> user = session.selectList("selectUsers");
System.out.println(user);
我们同样可以进行简单的条件查询,比如我们想要查询所有年龄大于等于18岁的用户:
<select id="selectUsersByAge" resultType="com.test.User">
select * from user where age > #{age};
</select>
注意由于这里是XML配置,其中一些字符被用作标签表示,无法代表其原本的意思,比如小于、大于符号,分别需要使用<
和>
来进行转义。
List<User> user = session.selectList("selectUsersByAge", 18);
我们接着来看一个比较特殊的选择方法selectMap
,它可以将查询结果以一个Map的形式表示,只不过这和我们之前说的Map不太一样,它返回的Map是使用我们想要的属性作为Key,然后得到的结果作为Value的Map,它适用于单个数据查询或是多行数据查询:
//最后一个参数为我们希望作为key的属性
Map<String, User> user = session.selectMap("selectUserById", 1, "id");
此时得到的结果就是:
可以看到这个Map中确实使用的是id作为Key,然后查询得到的实体对象作为Value。
还有一个比较特殊的选择操作是selectCursor
,它可以得到一个Cursor
对象,同样是用于列表查询的,只不过使用起来和我们之前JDBC中的ResultSet比较类似,也是通过迭代器的形式去进行数据的读取,官方解释它主要用于惰性获取数据,提高性能:
public interface Cursor<T> extends Closeable, Iterable<T> { ... }
可以看到它本身是实现了Iterable接口的,表明它可以获取迭代器或是直接使用foreach来遍历:
Cursor<User> cursor = session.selectCursor("selectUsers");
for (User user : cursor) {
System.out.println(user);
}
只不过这种方式在大部分请情况下还是用的比较少,我们主要还是以selectOne
和selectList
为主。
最后还有一个普通的select
方法,它支持我们使用Lambda的形式进行查询结果的处理:
session.select("selectUsers", context -> { //使用ResultHandler来处理结果
System.out.println(context.getResultObject());
});
结果会自动进行遍历并依次执行我们传入的Lambda表达式。
通过前面的学习,我们已经知道如何使用Mybatis进行各种查询操作。我们知道,Mybatis在执行完查询语句后,会自动将查询的结果转换为我们所需要的实体类,那么它具体是怎么做的呢?
实际上Mybatis一开始会通过我们实体类默认的无参构造得到一个最初的对象,然后通过反射进行赋值,我们可以手动编写一个带调试信息的无参构造:
public User() {
System.out.println("????");
}
可以看到Mybatis确实调用了我们的无参构造方法来构建对象,属性则是通过反射进行赋值,这里截取部分Mybatis源代码进行演示:
//这里的object就是刚刚构造好的实体类对象,prop是要设置的值的字段信息,value就是要设置的值
private void setBeanProperty(PropertyTokenizer prop, Object object, Object value) {
try {
//Invoker是Mybatis内部编写一个用于反射设置对象属性值的工具
Invoker method = metaClass.getSetInvoker(prop.getName());
Object[] params = { value };
try {
method.invoke(object, params); //通过Invoker为传入的实体类对象赋值
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
} catch (Throwable t) {
...
}
}
由于Mybatis默认情况下直接通过无参构造来创建实体类对象,如果我们的类中存在其他的构造方法覆盖掉默认的无参构造,那么Mybatis会选择可用的构造方法来进行构造。但是如果存在多个构造方法,Mybatis会出现问题:
@ToString
public class User {
...
public User(int id) {
this.id = id;
}
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
运行时出现错误:
Exception in thread "main" org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: org.apache.ibatis.executor.ExecutorException: No constructor found in com.test.User matching [java.lang.Integer, java.lang.String, java.lang.Integer]
### The error may exist in file:mappers/TestMapper.xml
此时由于类中存在多个构造方法,而Mybatis不知道该如何选择,那么就会告诉我们找不到合适的构造方法,要解决这种问题也很简单,我们不需要删除这些多余的构造方法,只需添加一个无参构造或是全参构造即可,注意全参构造必须与查询结果字段参数一一对应。但是注意,Mybatis仅仅是使用这种方式进行对象的构建,而字段的赋值无论是什么构造方法,都会使用反射进行一次赋值:
public User(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age + 20; //这里我们让age在赋值时增加一次
}
我们会发现,就算像这样进行了修改,最终的结果依然是被赋值为数据库中的结果,也就是说构造方法在默认情况下仅仅只是用于构造一个单纯的对象罢了。
如果需要让Mybatis完全使用构造方法进行对象构建与赋值工作,那么我们需要在XML中手动编写配置,同样需要使用resultMap来完成:
<select id="selectUserById" resultMap="test">
select * from user where id = #{id}
</select>
<resultMap id="test" type="com.test.User">
<constructor>
</constructor>
</resultMap>
这一次我们在resultMap中添加constructor标签,表示我们的查询结果直接使用指定的构造方法来处理。接着我们需要配置一下constructor里面的内容,使其符合我们指定构造方法的定义,比如现在我们有一个这样的构造方法:
public User(int id, String name) {
this.id = id;
this.name = name + "同学";
}
那么对应的XML配置编写为,使用arg标签来代表每一个参数,主键可以使用idArg来表示,有助于优化性能:
<constructor>
<idArg column="id" javaType="_int"/>
<arg column="name" javaType="String"/>
</constructor>
注意参数的顺序,必须和构造方法的顺序一致,否则会导致Mybatis无法确认。指定构造方法后,若此字段被填入了构造方法作为参数,将不会通过反射给字段单独赋值,而构造方法中没有传入的字段,依然会被反射赋值。
之前我们演示了,如何创建一个映射器来将结果快速转换为实体类,但是这样可能还是不够方便,我们每次都需要去找映射器对应操作的名称,而且还要知道对应的返回类型,再通过SqlSession
来执行对应的方法,能不能再方便一点呢?
我们可以通过namespace
来将各种操作绑定到一个接口上,然后使用方法的形式来表示,注意接口的参数和返回值必须正确对应,否则可能会出现问题:
public interface TestMapper {
User selectUserById(int id);
}
接着将Mapper文件的命名空间修改为我们的接口完整名称:
<select id="selectUserById" resultType="com.test.User">
select * from user where id = #{id}
</select>
这里建议将对应的xml配置也放到放到同包中,作为内部资源:
作为内部资源后,我们需要修改一下配置文件中的mapper文件目录,不使用url而是resource表示是Jar内部的文件:
<mappers>
<mapper resource="com/test/mapper/TestMapper.xml"/>
</mappers>
现在我们可以直接通过SqlSession
获取我们编写接口的实现类,这个实现类是由Mybatis根据我们的配置自动生成的,不需要我们做任何事情:
try(SqlSession sqlSession = MybatisUtil.openSession(true)) {
TestMapper mapper = sqlSession.getMapper(TestMapper.class); //直接获取实现类
//这里调用我们编写的接口方法
mapper.selectUser().forEach(System.out::println);
}
是不是感觉非常强大?
那肯定有人好奇,TestMapper明明是一个我们自己定义接口啊,Mybatis也不可能提前帮我们写了实现类啊,那这接口怎么就出现了一个实现类呢?我们可以通过调用getClass()
方法来看看实现类是个什么:
TestMapper testMapper = sqlSession.getMapper(TestMapper.class);
System.out.println(testMapper.getClass());
我们发现,得到的类名称很奇怪class jdk.proxy2.$Proxy4
,它其实是通过动态代理生成的,相当于在程序运行过程中动态生成了一个实现类,而不是预先定义好的,有关Mybatis这一部分的原理,我们放在最后一节进行讲解。
我们接着来看更方便的用法,有些时候,我们的查询操作可能需要不止一个参数:
<select id="selectUserByIdAndAge" resultType="com.test.entity.User">
select * from user where id = #{id} and age = #{age}
</select>
一种最简单的方式就是和之前一样,我们使用一个Map作为参数,然后将这些参数添加到Map中进行传递:
User selectUserByIdAndAge(Map<String, Object> map);
TestMapper mapper = session.getMapper(TestMapper.class);
System.out.println(mapper.selectUserByIdAndAge(Map.of("id", 1, "age", 18)));
只不过,这样编写实在是太复杂了,要是由一种更简单的方式就好了,我们也可以直接将这两个参数定义到形参列表中:
User selectUserByIdAndAge(int id, int age);
只不过这种方式查询的话,Mybatis会并不能正确获取对应的参数:
### Cause: org.apache.ibatis.binding.BindingException: Parameter 'id' not found. Available parameters are [arg1, arg0, param1, param2]
at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
这是因为Java代码编译后形参名称无法保留,导致Mybatis无法确定具体哪个参数交什么名字,所以默认情况下它们将会以 param 加上它们在参数列表中的位置来命名,比如:#{param1}、#{param2}等,这里id实际上就是param1:
select * from user where id = #{param1} and age = #{param2}
当然,如果你实在需要使用对应的属性名称,我们也可以手动添加一个@Param
注解来指定某个参数的名称:
User selectUserByIdAndAge(@Param("id") int id, @Param("age") int age);
这样Mybatis就可以正确识别了。