MyBatis 速成手册

2年前 (2022) 程序员胖胖胖虎阿
380 0 0

MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Ordinary Java Object,普通的 Java对象)映射成数据库中的记录。

既然MyBatis有如此多的优势,那么下面就一起来看看MyBatis的用法吧。

入门案例

现有一张数据表:

mysql> use mybatis;
Database changed
mysql> select * from tbl_employee;
+----+-----------+--------+-------------+
| id | last_name | gender | email |
+----+-----------+--------+-------------+
|
  1 | tom | 0      | tom@163.com |
+----+-----------+--------+-------------+
1 row in set (0.00 sec)

该如何通过MyBatis对其进行查询?首先创建对应的Java类:

package com.wwj.mybatis.bean;

public class Employee {
 
 private Integer id;
 private String lastName;
 private String email;
 private String gender;
 
 public Integer getId() {
  return id;
 }
 public void setId(Integer id) {
  this.id = id;
 }
 public String getLastName() {
  return lastName;
 }
 public void setLastName(String lastName) {
  this.lastName = lastName;
 }
 public String getEmail() {
  return email;
 }
 public void setEmail(String email) {
  this.email = email;
 }
 public String getGender() {
  return gender;
 }
 public void setGender(String gender) {
  this.gender = gender;
 }
 
 @Override
 public String toString() {
  return "Employee [id=" + id + ", lastName=" + lastName + ", email=" + email + ", gender=" + gender + "]";
 }
}

其次编写MyBatis的全局配置文件(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">

<configuration>
  <environments default="development">
  <environment id="development">
   <transactionManager type="JDBC"/>
         <dataSource type="POOLED">
           <property name="driver" value="com.mysql.jdbc.Driver"/>
           <property name="url" value="jdbc:mysql:///mybatis"/>
          <property name="username" value="root"/>
           <property name="password" value="123456"/>
         </dataSource>
     </environment>
 </environments>
 <mappers>
  <mapper resource="EmployeeMapper.xml"/>
 </mappers>
</configuration>

全局配置文件中主要配置的是数据源信息,然后是最后的mappers标签,该标签配置的是sql语句的映射文件。

所以我们接着创建sql语句的映射文件(EmployeeMapper.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.wwj.mybatis.bean.Employee">
  <select id="selectEmp" resultType="com.wwj.mybatis.bean.Employee">
     select id,last_name lastName,email,gender from tbl_employee where id = #{id}
   </select>
</mapper>

其中mapper标签的namespace属性设置的是需要映射的类全名;select标签表示查询语句,其中的id属性是该sql的唯一标识,resultType表示返回值的类型;然后在select标签中编写需要执行的sql语句。

一切准备就绪后,开始编写测试代码:

@Test
void test() throws IOException {
 String resource = "mybatis-config.xml";
 InputStream inputStream = Resources.getResourceAsStream(resource);
 SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
 SqlSession session = sessionFactory.openSession();
 Employee employee = session.selectOne("com.wwj.mybatis.bean.Employee.selectEmp",1);
 System.out.println(employee);
 session.close();
}

通过全局的配置文件去创建一个SqlSessionFactory,并通过该对象获得SqlSession,就可以使用SqlSession进行增删改查了。这里调用了selectOne方法,表示从数据表中查询一行数据,其中的第一个参数需要填入刚才在sql映射文件中设置的id,但为了避免该id与其它sql语句重复,一般都会在id前加上namespace;第二个参数则是需要传入sql的参数。

增删改查的新方式

在入门案例中我们已经成功通过MyBatis查询了数据表的数据,但是这种方式的缺点也是显而易见的,为此,MyBatis提供了一种更加完美的方式来操作数据表。

定义一个接口:

package com.wwj.mybatis.dao;

public interface EmployeeMapper {
 
 public Employee getEmpById(Integer id);
}

MyBatis神奇的地方就在于你不需要去实现该接口,只需要将该接口与对应的sql映射文件绑定即可,MyBatis会自动创建代理对象调用对应的方法。

所以我们需要对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="com.wwj.mybatis.dao.EmployeeMapper">
  <select id="getEmpById" resultType="com.wwj.mybatis.bean.Employee">
     select id,last_name lastName,email,gender from tbl_employee where id = #{id}
   </select>
</mapper>

首先是namespace,现在该属性应该指向的是接口的全类名;然后是select标签的id属性,该属性也应该指向接口中对应的方法名,其它地方不变。

测试代码:

@Test
void test2() throws IOException{
 String resource = "mybatis-config.xml";
 InputStream inputStream = Resources.getResourceAsStream(resource);
 SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
 SqlSession session = sessionFactory.openSession();
 //Mybatis会为接口自动创建代理对象,并由代理对象执行增删改查
 EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
 Employee employee = mapper.getEmpById(1);
 System.out.println(employee);
 session.close();
}


这里同样是通过全局配置文件创建SqlSessionFactory,并通过该对象获得SqlSession,不同的是,这里需要通过SqlSession对象调用getMapper方法去获得指定的接口的实现类,该实现类是MyBatis自动生成的代理对象,并通过该对象调用指定的方法完成数据表的操作。

通过properties标签引入外部属性文件

这个操作已经是再熟悉不过了,这里直接贴出代码:

<?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">

<configuration>
 
 <!-- 使用properties标签引入外部的属性文件内容 -->
 <properties resource="dbconfig.properties"></properties>
 
  <environments default="development">
  <environment id="development">
   <transactionManager type="JDBC"/>
         <dataSource type="POOLED">
           <property name="driver" value="${jdbc.driver}"/>
           <property name="url" value="${jdbc.url}"/>
          <property name="username" value="${jdbc.username}"/>
           <property name="password" value="${jdbc.password}"/>
         </dataSource>
     </environment>
 </environments>
 <mappers>
  <mapper resource="EmployeeMapper.xml"/>
 </mappers>
</configuration>

typeAliases

这是一个全局配置文件的标签,通过该标签能够为某个Java类型设置别名,比如:

<typeAliases>
 <!-- type:指定要起别名的类型全类名,默认别名为类名字母小写:employee
   也可以使用alias属性为其设置指定的别名
 -->

 <typeAlias type="com.wwj.mybatis.bean.Employee" alias="employee"/>
</typeAliases>
这样我们就可以修改sql映射文件中的返回值类型:
<select id="getEmpById" resultType="employee">
    select * from tbl_employee where id = #{id}
</select>

该标签还能够批量起别名,比如:

<typeAliases>
 <!--
  批量起别名
  name:指定包名(为当前包及其子包下的所有类都起一个默认别名)
 -->

 <package name="com.wwj.mybatis.bean"/>
</typeAliases>

若是子包下也有一个重名的类,则MyBatis会因为别名重复而报错,为此,可以通过@Alias注解在类上设置新的别名。

environments

全局配置文件标签,可以通过其子标签environment配置多种环境,比如:

<environments default="development">
 <environment id="development">
  <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
         <property name="driver" value="${jdbc.driver}"/>
          <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
          <property name="password" value="${jdbc.password}"/>
        </dataSource>
    </environment>
     
    <environment id="test">
     <transactionManager type="JDBC"></transactionManager>
     <dataSource type="POOLED"></dataSource>
    </environment>
</environments>

environment需要配置两个子标签:transactionManager和dataSource,否则就会报错。

transactionManager标签的type属性可以指定两个属性值:

  1. JDBC:这个配置直接使用了 JDBC 的提交和回滚设施,它依赖从数据源获得的连接来管理事务作用域

  2. MANAGED:这个配置几乎没做什么。它从不提交或回滚一个连接,而是让容器来管理事务的整个生命周期(比如 JEE 应用服务器的上下文), 默认情况下它会关闭连接

dataSource标签的type属性可以指定三个属性值:

  1. UNPOOLED :这个数据源的实现会每次请求时打开和关闭连接。虽然有点慢,但对那些数据库连接可用性要求不高的简单应用程序来说,是一个很好的选择。性能表现则依赖于使用的数据库,对某些数据库来说,使用连接池并不重要,这个配置就很适合这种情形。

  2. POOLED:这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。这种处理方式很流行,能使并发 Web 应用快速响应请求

  3. JNDI:这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用

mappers

sql映射文件标签,用于将sql映射注册到全局配置中,比如:

<mappers>
 <mapper resource="EmployeeMapper.xml"/>
</mappers>

使用子标签mapper注册一个sql映射。

mapper还有一个用法, 就是注册接口,比如:

<mappers>
 <mapper class="com.wwj.mybatis.dao.EmployeeMapper"/>
</mappers>

需要注意的是,若要通过这样的方式注册接口,则需要将该sql映射文件名设置为与接口名一致,并与接口放在同一目录下。

mappers同样支持批量注册,使用package标签即可,若使用批量注册,sql映射文件也需要与接口同目录,否则MyBatis将无法找到文件而报错。

增删改查

熟悉了MyBatis配置文件中的一些标签后,我们来看看MyBatis是如何实现增删改查操作的?

插入数据

仿照着实现查询的方式,我们很容易写出增删改查,先修改接口:

package com.wwj.mybatis.dao;

public interface EmployeeMapper {
 
 public Employee getEmpById(Integer id);
 
 public void addEmp(Employee employee);
 
 public void updateEmp(Employee employee);
 
 public void deleteEmp(Integer id);
}

在接口中新添加了三个方法,分别对应着增删改。

然后修改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="com.wwj.mybatis.dao.EmployeeMapper">
  <select id="getEmpById" resultType="employee">
     select * from tbl_employee where id = #{id}
   </select>
   
   <insert id="addEmp" parameterType="com.wwj.mybatis.bean.Employee">
    insert into tbl_employee(last_name,email,gender) values(#{lastName},#{email},#{gender})
   </insert>
   
   <update id="updateEmp">
    update tbl_employee set last_name = #{lastName},email = #{email},gender = #{gender} where id = #{id}
   </update>
   
   <delete id="deleteEmp">
    delete from tbl_employee where id = #{id}
   </delete>
</mapper>

这已经很简单了,无需做过多解释,唯一要说的是parameterType属性,该属性的作用是指定方法参数的类型,该属性可以省略不写。这样准备工作就完成了,来测试一下:

@Test
void test3() throws IOException{
 String resource = "mybatis-config.xml";
 InputStream inputStream = Resources.getResourceAsStream(resource);
 SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
 SqlSession session = sessionFactory.openSession();
 EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
 Employee employee = new Employee(null,"Jack","jack@163.com","1");
 mapper.addEmp(employee);
 session.commit();
 session.close();
}

需要注意的是,SqlSession是不会自动提交数据的,所以在进行增删改操作的时候,最后一定要记得提交。

删除数据

做删除操作只需要改变一下调用的方法即可:

@Test
void test3() throws IOException{
 String resource = "mybatis-config.xml";
 InputStream inputStream = Resources.getResourceAsStream(resource);
 SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
 SqlSession session = sessionFactory.openSession();
 EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
 mapper.deleteEmp(2);
 session.commit();
 session.close();
}

修改数据

修改数据同样是这样:

@Test
void test3() throws IOException{
 String resource = "mybatis-config.xml";
 InputStream inputStream = Resources.getResourceAsStream(resource);
 SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
 SqlSession session = sessionFactory.openSession();
 EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
 Employee employee = new Employee(1,"Jack","jack@163.com","0");
 mapper.updateEmp(employee);
 session.commit();
 session.close();
}

Mybatis还直接允许增删改操作直接定义以下返回值:Integer、Long、Boolean,直接在接口方法上定义即可,比如:

public interface EmployeeMapper {
 
 public Long addEmp(Employee employee);
}

查询数据

查询就不说了,入门案例已经写过了。

获取自增主键的值

MyBatis支持获取自增主键的值,而且非常方便:

<insert id="addEmp" useGeneratedKeys="true" keyProperty="id">
   insert into tbl_employee(last_name,email,gender) values(#{lastName},#{email},#{gender})
</insert>

只需要设置useGeneratedKeys属性值为true即可,MyBatis就会将主键的值封装到keyProperty属性指定的参数中,这里指定了id,表示将自增主键的值获取出来并存入Employee对象的id属性中,接下来要想获取主键的值就可以直接获取该对象的id属性了。

参数处理

下面介绍一下MyBatis对于参数的处理,在前面的案例中,我们都是通过#{参数名}的方式来获取参数,而事实上,对于单个的参数,MyBatis不会对其进行特殊处理,这个参数名是可以随便写的,写个a、b、c都可以,比如:

<select id="getEmpById" resultType="employee">
    select * from tbl_employee where id = #{aaa}
</select>

那么对于多参数的方法该如何处理呢?

package com.wwj.mybatis.dao;

public interface EmployeeMapper {
 
 //新增一个多参数的方法
 public Employee getEmpByIdAndLastName(String id,String lastName);
 
 public Employee getEmpById(Integer id);
 
 public void addEmp(Employee employee);
 
 public void updateEmp(Employee employee);
 
 public void deleteEmp(Integer id);
}


在接口中新增一个getEmpByIdAndLastName方法,并添加对应的sql配置:
<select id="getEmpByIdAndLastName" resultType="employee">
  select * from tbl_employee where id = #{id} and last_name = #{lastName}
</select>

可能大家会想当然地这样写,但告诉大家,这样是错误的。

MyBatis会对多个参数进行特殊处理,将这些参数封装成一个map,所以我们需要从map中取值,如下:

<select id="getEmpByIdAndLastName" resultType="employee">
  select * from tbl_employee where id = #{param1} and last_name = #{param2}
</select>

MyBatis有着自己的封装规则,它将传递过来的参数封装到map中,指定键为:param1、param2、param3...,而这些键对应的就是我们需要取出的值。

当然,你也可以这样写:

<select id="getEmpByIdAndLastName" resultType="employee">
  select * from tbl_employee where id = #{0} and last_name = #{1}
</select>

通过索引获取map中的值。

命名参数

这两种获取参数方式的缺点是显而易见的,当参数足够多时,这样的参数名会使开发者眼花缭乱,为此,我们应该使用命名参数的方式进行参数值的获取。

这里需要借助@Param注解对map的键重命名:

//新增一个多参数的方法
public Employee getEmpByIdAndLastName(@Param("id")String id,@Param("lastName")String lastName);

这样我们就能直接通过指定的参数名从map中获取值了:

<select id="getEmpByIdAndLastName" resultType="employee">
  select * from tbl_employee where id = #{id} and last_name = #{lastName}
</select>

处理POJO

若是传入的参数过多,而且正好符合业务逻辑的模型对象,我们就可以直接传入POJO,在取出数据值的时候就可以直接使用POJO的属性名:#{POJO属性名}

而如果多个参数并不符合模型对象,为了方便,我们也可以自己将其封装成一个map,比如:

//新增一个多参数的方法
public Employee getEmpByIdAndLastName(Map<String, Object> map);

那么你在传递参数的时候就需要传递一个map:

@Test
void test4() throws IOException{
 String resource = "mybatis-config.xml";
 InputStream inputStream = Resources.getResourceAsStream(resource);
 SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
 SqlSession session = sessionFactory.openSession();
 //Mybatis会为接口自动创建代理对象,并由代理对象执行增删改查
 EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
 Map<String, Object> map = new HashMap<String, Object>();
 map.put("id", "1");
 map.put("lastName", "Jack");
 Employee employee = mapper.getEmpByIdAndLastName(map);
 System.out.println(employee);
 session.close();
}

而在取出map数据的时候就可以直接通过键获取:

<select id="getEmpByIdAndLastName" resultType="employee">
  select * from tbl_employee where id = #{id} and last_name = #{lastName}
</select>

特别需要注意的是,Mybatis会对Collection类型及数组做特殊处理,比如:

public Employee getEmpById(List<Integer> ids);

该参数是一个id的集合,若是想从该集合中获取第一个id的值,则需要#{list[0]}

因为对于Collection类型,包括:Collection、List、Set,MyBatis都会将其封装到map中,其对应的键为collection,如果是List类型,还可以使用list作为键,数组的键为array

#{}与${}的区别

MyBatis中除了可以使用#{}取值外,还可以使用${},它们的用法是相同的,那么这两种方式到底有什么区别呢?

我分别用#{}和${}进行取值,并执行测试代码,下面是运行结果:

DEBUG 04-16 13:46:27,992 ==> Preparing: select * from tbl_employee where id = ? and last_name = ? (BaseJdbcLogger.java:145) 

DEBUG 04-16 14:31:13,054 ==> Preparing: select * from tbl_employee where id = 1 and last_name = Jack (BaseJdbcLogger.java:145)

会发现,两者的区别在于,${}会将传递的参数显示到生成的sql语句上。

select元素之resultMap

select标签用来定义查询操作,其中有三个比较重要的参数:

  1. id:唯一标识符,用来引用这条语句,需要与接口的方法名一致

  2. parameterType:参数类型,可以不传,MyBatis会根据TypeHandler自动判断

  3. resultType:返回值类型,别名或者全类名,如果返回的是集合,则写的是集合中元素的类型;不能与resultMap同时使用

在前面的案例中,我们已经使用过select标签,接下来让我们深入了解一下该标签。

比如在接口中定义这样的一个方法:

public List<Employee> getEmpsByLastNameLike(String lastName);

该方法的返回值是集合类型,那么在编写sql配置的时候就得这样写:

<select id="getEmpsByLastNameLike" resultType="employee">
  select * from tbl_employee where last_name like #{lastName}
</select>

resultType写的是集合中的元素类型。

若凡返回值是map类型,比如:

public Map<String,Object> getEmpMapById(Integer id);

sql配置应该这样写:

<select id="getEmpMapById" resultType="map">
  select * from tbl_employee where id = #{id}
</select>

resultType写的是map,为什么能够直接写map而不用写全类名,因为MyBatis已经帮我们取好了别名。

通过这样的方式,MyBatis会将表中的列名和记录作为键和值一一封装到map中。

我们还可以通过返回map来查询表中的某一行数据,并直接将其封装成对象返回:

@MapKey("id")
public Map<Integer,Employee> getEmpByLastNameLike(String lastName);

该方法能将查询到的每一行数据封装成一个对象,其键由@MapKey注解指定,比如这里是指定以id为键,接下来是sql配置:

<select id="getEmpByLastNameLike" resultType="employee">
  select * from tbl_employee where last_name like #{lastName}
</select>

resultType同样是集合元素类型,但是通过该sql查询返回的结果将是以id为键,对应的行数据为值的map集合。

当然,select标签可不止id、parameterType和resultType这三个参数,下面介绍一下该标签的另一个重要属性参数:resulyMap。

我们知道,通过resultType指定返回值类型后,MyBatis会自动将得到的数据封装进去,然而当数据表的列名和返回值属性不一致时,就会导致封装失败,这时候我们会有两种办法:一种就是设置别名;另一种就是使用resultMap来自定义返回值类型。

比如:

public Employee getEmpById(Integer id);

按照之前的写法,应该是这样:

<select id="getEmpById" resultType="employee">
    select * from tbl_employee where id = #{id}
 </select>

但现在,倘若Employee类中的某些属性名和表中的列名并不一致,此时MyBatis将无法进行自动封装,我们就需要通过resultMap来自定义封装规则。

<!-- 自定义某个JavaBean的封装规则 -->
<resultMap type="employee" id="emp">
 <!--
   指定主键列的封装规则
   column:指定哪一列
   property:指定对应的JavaBean属性
  -->

  <id column="id" property="id"/>
  <!-- 指定非主键列的封装规则 -->
   <result column="last_name" property="lastName"/>
   <result column="email" property="email"/>
   <result column="gender" property="gender"/>
  </resultMap>
 
<select id="getEmpById" resultMap="emp">
 select * from tbl_employee where id = #{id}
</select>

事实上,对于主键列,你也可以使用result标签来指定规则, 但是使用id标签的好处就是MyBatis会知道该属性是主键从而自动设置一些功能,对于一些属性名和列名已经一致的属性,你也可以不进行自定义,MyBatis还是会自动进行封装的。

resultMap可以用来解决数据表的联合查询问题,现有两张数据表,一张员工表:

+-----------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------+--------------+------+-----+---------+----------------+
|
 id | int(11) | NO | PRI | NULL | auto_increment |
| last_name | varchar(255) | YES |     | NULL |                |
|
 gender | char(1) | YES | | NULL | |
| email | varchar(255) | YES |     | NULL |                |
|
 d_id | int(11) | YES | MUL | NULL | |
+-----------+--------------+------+-----+---------+----------------+

一张部门表:

+-----------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------+--------------+------+-----+---------+----------------+
|
 id | int(11) | NO | PRI | NULL | auto_increment |
| dept_name | varchar(255) | YES |     | NULL |                |
+-----------+--------------+------+-----+---------+----------------+

其中员工表的d_id列与部门表的id列存在外键约束关系。

若有这样的需求,查询员工表的某位员工信息,同时查出它的部门信息,该需求涉及到联合查询的问题,resultMap则能够很好地解决这一问题。

首先我们创建两张数据表对应的类,员工类:

package com.wwj.mybatis.bean;

public class Employee {
 
 private Integer id;
 private String lastName;
 private String email;
 private String gender;
 private Department department;
    
    public Employee() {
  
 }

 public Department getDepartment() {
  return department;
 }

 public void setDepartment(Department department) {
  this.department = department;
 }
    
 public Integer getId() {
  return id;
 }
 public void setId(Integer id) {
  this.id = id;
 }
 public String getLastName() {
  return lastName;
 }
 public void setLastName(String lastName) {
  this.lastName = lastName;
 }
 public String getEmail() {
  return email;
 }
 public void setEmail(String email) {
  this.email = email;
 }
 public String getGender() {
  return gender;
 }
 public void setGender(String gender) {
  this.gender = gender;
 }
}


部门类:

package com.wwj.mybatis.bean;

public class Department {
 
 private Integer id;
 private String departmentName;
 
 public Integer getId() {
  return id;
 }
 public void setId(Integer id) {
  this.id = id;
 }
 public String getDepartmentName() {
  return departmentName;
 }
 public void setDepartmentName(String departmentName) {
  this.departmentName = departmentName;
 }
}


那么接下来就非常简单了,在接口中定义查询方法:

public Employee getEmpAndDept(Integer id);

编写sql配置:

<resultMap type="employee" id="empy">
  <id column="id" property="id"/>
  <result column="last_name" property="lastName"/>
  <result column="gender" property="gender"/>
  <result column="did" property="dept.id"/>
  <result column="dept_name" property="dept.departmentName"/>
</resultMap>
<select id="getEmpAndDept" resultMap="empy">
 select * from tbl_employee e,tbl_dept d where e.d_id=d.id and e.id=#{id}
</select>


这里通过级联属性为部门类的属性定义规则。

除了级联属性,你也可以使用association标签来定义规则:

<resultMap type="employee" id="empy">
  <id column="id" property="id"/>
  <result column="last_name" property="lastName"/>
  <result column="gender" property="gender"/>
  <!--
   association可以指定联合的Java对象
  property:指定哪个属性为联合的Java对象
  javaType:指定联合Java对象的类型
 -->

  <association property="dept" javaType="com.wwj.mybatis.bean.Department">
   <id column="did" property="id"/>
   <result column="dept_name" property="departmentName"/>
  </association>
</resultMap>
<select id="getEmpAndDept" resultMap="empy">
  select * from tbl_employee e,tbl_dept d where e.d_id=d.id and e.id=#{id}
</select>

而有些情况,我们在查询员工信息的时候并不需要查询它的部门信息,此时可以使用延迟加载,实现非常简单,在MyBatis的配置文件中进行两个配置即可:

<settings>
 <setting name="lazyLoadingEnabled" value="true"></setting>
 <setting name="aggressiveLazyLoading" value="false"/>
</settings>

此时当我们不去查询员工的部门信息时,Mybatis就不会发送sql去查询部门表,避免了资源的浪费。

而当查询对象中包含集合数据时,比如:

package com.wwj.mybatis.bean;

public class Department {
 
 private Integer id;
 private String departmentName;
 private List<Employee> emps;
 
 public List<Employee> getEmps() {
  return emps;
 }
 public void setEmps(List<Employee> emps) {
  this.emps = emps;
 }
 public Integer getId() {
  return id;
 }
 public void setId(Integer id) {
  this.id = id;
 }
 public String getDepartmentName() {
  return departmentName;
 }
 public void setDepartmentName(String departmentName) {
  this.departmentName = departmentName;
 }
 @Override
 public String toString() {
  return "Department [id=" + id + ", departmentName=" + departmentName + ", emps=" + emps + "]";
 }
}


一个部门包含若干员工,这些员工将封装到一个集合中,该如何对其进行处理呢?

在接口中定义方法:

public Department getDeptById(Integer id);

sql配置:

<resultMap type="com.wwj.mybatis.bean.Department" id="MyDept">
  <id column="did" property="id"/>
  <result column="dept_name" property="departmentName"/>
  <collection property="emps" ofType="com.wwj.mybatis.bean.Employee">
   <!-- 定义集合中的元素封装规则 -->
   <id column="eid" property="id"/>
   <result column="last_name" property="lastName"/>
   <result column="email" property="email"/>
   <result column="gender" property="gender"/>
  </collection>
 </resultMap>
 <!-- 查询部门的时候将部门对应的所有员工信息查询出来 -->
 <select id="getDeptById" resultMap="MyDept">
  SELECT * FROM tbl_dept d LEFT JOIN tbl_employee e ON d.id=e.d_id WHERE d.id=#{id}
 </select>

其中collection用于定义集合中的元素封装规则,ofType属性指定集合的元素类型。

Mybatis的缓存机制

先来看这样一个场景:

@Test
public void test12()
{
    EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
    Employee employee = mapper.getEmpById(1);
    Employee employee2 = mapper.getEmpById(1);
    System.out.println(employee == employee2);
}

按照我们的想法,两次查询都会从数据库中获取一个对象,结果应该是false才对,但是查看控制台:

DEBUG 09-18 11:39:06,191 ==> Preparing: select * from tbl_employee where id = ? (BaseJdbcLogger.java:145) 
DEBUG 09-18 11:39:06,267 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:145)
DEBUG 09-18 11:39:06,304 <== Total: 1  (BaseJdbcLogger.java:145)
true

结果却是true,这是MyBatis缓存机制的效果,那它有什么好处呢?对于一个项目来说,某些数据是基本不会发生改变且需要在页面之间互相传递的,对于这些数据,它需要在各个页面之间频繁地查询,为此,可以将它们在第一次查询之后就放入缓存中,等接下来需要用到这些数据时,直接从缓存中获取而不是去查询数据库,大大提升了运行效率。

这里的缓存采用的是MyBatis的一级缓存,也叫sqlSession级别缓存,默认是一直开启的,因为是sqlSession级别缓存,所以每个sqlSession都拥有自己的缓存,两个sqlSession之间是不能互相访问自己的缓存的。

比如:

@Test
public void test12()
{
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
    EmployeeMapper mapper2 = sqlSession2.getMapper(EmployeeMapper.class);
    Employee employee = mapper.getEmpById(1);
    Employee employee2 = mapper2.getEmpById(1);
    System.out.println(employee == employee2);
}

输出结果:

DEBUG 09-18 11:50:29,516 ==> Preparing: select * from tbl_employee where id = ? (BaseJdbcLogger.java:145) 
DEBUG 09-18 11:50:29,590 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:145)
DEBUG 09-18 11:50:29,659 <== Total: 1  (BaseJdbcLogger.java:145)
DEBUG 09-18 11:50:29,674 ==> Preparing: select * from tbl_employee where id = ? (BaseJdbcLogger.java:145)
DEBUG 09-18 11:50:29,674 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:145)
DEBUG 09-18 11:50:29,679 <== Total: 1  (BaseJdbcLogger.java:145)
false

若是使用的同一个sqlSession对象,但是在两次查询期间执行了增删改操作,那么查询的数据也不会从一级缓存中获取:

@Test
public void test12()
{
    EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
    Employee employee = mapper.getEmpById(1);
    mapper.addEmp(new Employee(null,"testCache","cache","1"));
    Employee employee2 = mapper.getEmpById(1);
    System.out.println(employee == employee2);
}

输出结果:

DEBUG 09-18 11:59:14,137 ==> Preparing: select * from tbl_employee where id = ? (BaseJdbcLogger.java:145) 
DEBUG 09-18 11:59:14,337 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:145)
DEBUG 09-18 11:59:14,397 <== Total: 1  (BaseJdbcLogger.java:145)
DEBUG 09-18 11:59:14,397 ==> Preparing: insert into tbl_employee (last_name,email,gender) values (?,?,?) (BaseJdbcLogger.java:145)
DEBUG 09-18 11:59:14,407 ==> Parameters: testCache(String), cache(String), 1(String) (BaseJdbcLogger.java:145)
DEBUG 09-18 11:59:14,407 <== Updates: 1  (BaseJdbcLogger.java:145)
DEBUG 09-18 11:59:14,407 ==> Preparing: select * from tbl_employee where id = ? (BaseJdbcLogger.java:145)
DEBUG 09-18 11:59:14,417 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:145)
DEBUG 09-18 11:59:14,417 <== Total: 1  (BaseJdbcLogger.java:145)
false

因为增删改操作可能会对需要查询的数据做处理,若是在查询之间将这条数据就删除了,结果却还从一级缓存中读取出了数据,那不是乱套了吗?

还可以通过sqlSession.clearCache();手动删除sqlSession缓存。

sqlSession级别的缓存显然不是很友好,因为作用范围实在是太小了,为此,MyBatis提供了二级缓存,也叫namespace级别缓存,它的作用范围是全局的。

当开启一个sqlSession并查询数据之后,该数据会被存入一级缓存,然而当sqlSession关闭以后,一级缓存中的数据理应被清空,但是MyBatis会将这些数据重新放入二级缓存,那么该如何使用二级缓存呢?

首先需要配置一下二级缓存:

<settings>
    <!-- 开启二级缓存 -->
    <setting name="cacheEnabled" value="true"/>
</settings>

然后在Mapper.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.wwj.dao.EmployeeMapper">
    <cache eviction="FIFO" flushInterval="60000" readOnly="false" size="1024">
    </cache>
    ......
</mapper>

这里详细介绍一下cache标签的属性:

  • eviction:缓存的回收策略
    • LRU:最近最少使用的,移除最长时间没有被使用的对象
    • FIFO:先进先出,按对象进入缓存的顺序来移除它们
    • SOFT:软引用,移除基于垃圾回收器状态和软引用规则的对象
    • WEAK:弱引用,更积极地移除基于垃圾回收器状态和弱引用规则的对象
      默认回收策略为LRU
  • flushInterval:缓存的刷新间隔(设置一个毫秒值),即多长时间清空一次缓存,MyBatis默认是不清空缓存的
  • readOnly:缓存是否只读
    • true:只读 若设置为只读,则MyBatis会认为所有从缓存中获取数据的操作都是只读操作,不会修改数据 为了加快获取数据,会直接将数据在缓存中的引用交给开发者;优点:效率高,缺点:不安全
    • false:非只读 若设置为非只读,则MyBatis会认为从缓存中获取的数据可能会被修改 所以会通过序列化与反序列化克隆一份新的数据交给开发者;优点:安全,缺点:效率低
  • size:缓存存放多少个元素
  • type:指定自定义缓存全类名,将实现Cache接口类的全类名作为属性值传入即可,这里type使用默认值
因为我们将readOnly设置为了false,所以需要让Employee类实现Serializable接口。
@Test
public void test13(){
    SqlSession sqlSession = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
    EmployeeMapper mapper2 = sqlSession2.getMapper(EmployeeMapper.class);
    Employee employee = mapper.getEmpById(1);
    sqlSession.close();
    Employee employee2 = mapper2.getEmpById(1);
    sqlSession2.close();
}
运行结果:


DEBUG 09-18 12:50:55,598 Cache Hit Ratio [com.wwj.dao.EmployeeMapper]: 0.0  (LoggingCache.java:62) 
DEBUG 09-18 12:50:55,646 ==> Preparing: select * from tbl_employee where id = ? (BaseJdbcLogger.java:145)
DEBUG 09-18 12:50:55,736 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:145)
DEBUG 09-18 12:50:55,788 <== Total: 1  (BaseJdbcLogger.java:145)
DEBUG 09-18 12:50:55,894 Cache Hit Ratio [com.wwj.dao.EmployeeMapper]: 0.5  (LoggingCache.java:62)
可以看到MyBatis的确只发了一条sql语句,使用二级缓存需要注意的一点是,只有当sqlSession被关闭之后,数据才会被放入二级缓存,所以如果是这样就不对了:
@Test
public void test13()
{
    SqlSession sqlSession = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
    EmployeeMapper mapper2 = sqlSession2.getMapper(EmployeeMapper.class);
    Employee employee = mapper.getEmpById(1);
    Employee employee2 = mapper2.getEmpById(1);
    System.out.println(employee == employee2);
    sqlSession.close();
    sqlSession2.close();
}
还需要注意的是,二级缓存是namespace级别的缓存,所以只有当你在对应的Mapper.xml文件中配置了cache节点,该Mapper才有二级缓存。
以上便是有关MyBatis的全部内容了。

本文分享自微信公众号 - Java后端(web_resource)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

版权声明:程序员胖胖胖虎阿 发表于 2022年10月27日 上午6:16。
转载请注明:MyBatis 速成手册 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...