MyBatis高级映射——灵活地映射复杂的数据类型

作者:禅与计算机程序设计艺术

1.简介

  1. 概述
    在实际项目中,经常会遇到需要从数据库查询出复杂的数据类型,例如pojo对象、map集合、json字符串等。如何将数据库查询出的复杂数据类型映射到pojo对象,或者如何自定义转换器实现映射呢? MyBatis 是一个优秀的ORM框架,它的提供的映射方式能够满足一般的需求。本文重点介绍 MyBatis 的高级映射功能。
  2. 本文假设读者已经对 MyBatis 有一定了解。如若没有,可以参考我的其他博文:《MyBatis 从入门到进阶(一)——基础知识和配置文件详解》和《MyBatis 从入门到进阶(二)——动态 SQL 和参数处理》。

2.背景介绍

2.1 抽象模型
把数据库表当做一个抽象模型,java对象作为它的实例,pojo映射就是把java对象和数据库表做的一个关联。
其中java对象的属性和数据库表的字段对应,而pojo映射又通过配置文件定义,配置了哪些字段需要映射,这些映射关系就存在配置文件中。
pojo映射在执行sql查询时,根据配置文件的映射关系,把查到的记录封装成pojo对象。

pojo映射器内部实现主要有两个过程:
①. 配置文件读取 - 将xml配置文件解析,创建pojo映射关系。
②. sql查询 - 根据pojo映射关系,将sql查询结果映射为pojo对象。

如果数据库查询出来的记录比较简单,比如单个值,则可以使用mybatis默认的类型转换器即可。如果查询出来的是复杂的数据类型,例如pojo对象、map集合、json字符串等,则可以使用mybatis提供的各种映射类型。

在mybatis中提供了四种映射类型:
①. 对象-直接映射 - 使用pojo对象直接进行映射。
②. javabean-bean - 通过javabean来映射。
③. map-map-map - 将map类型转换为pojo对象。
④. json-json-json - 将json字符串转换为pojo对象。
每一种映射类型的具体语法及用法在本文后面给出详细说明。

2.2 类型转换器
在mybatis中,类型转换器是用来转换不同类型数据的工具。
TypeHandler接口是mybatis中定义的类型转换器接口。
提供两种类型的类型转换器:
①. 内置类型转换器 - 默认提供的类型转换器,mybatis已经预先定义好了一些常用的内置类型转换器,例如int、float、string等。可以通过typeHandlers标签添加类型转换器类。
②. 用户自定义类型转换器 - 除了内置类型转换器之外,用户还可以自己编写类型转换器。

3.基本概念术语说明

3.1 数据库字段类型

在mybatis中,数据库字段类型分为以下几种:
①. 主键字段 - 该字段的值保证唯一,主键必须指定,否则无法插入或更新记录。
②. 必填字段 - 不允许为空值的字段,必须指定该字段才能插入或更新记录。
③. 可空字段 - 可以为空值的字段。
④. 外键字段 - 该字段的值与另外一个表中的主键相关联。

3.2 Java对象

Java对象指的是pojo对象,也就是普通的JavaBean。
通过注解或配置文件,mybatis就可以知道Java对象的各个属性到底映射到数据库中的哪些字段上。例如:User user = new User(); user.setUsername(“Tom”); user.setAge(25); user.setId(1);
上面的代码中,User对象有三个属性:username、age、id。

4.核心算法原理和具体操作步骤

4.1 对象-直接映射

通过注解或配置文件将java对象和数据库表做关联,pojo对象上的注解或者xml中的映射标签可以指定pojo对象的哪些属性和数据库表的哪些字段进行映射。例如:

@Table(name="user") // 指定数据库表名
public class User {
@Id // 指定主键字段
private Integer id;
@Column(name="username") // 指定数据库列名
private String username;
@Column(name="password")
private String password;
@Column(name="gender")
private Gender gender;

... getter and setter methods...

}

public enum Gender{ MALE, FEMALE }; //枚举类型

当执行sql查询时,pojo映射器会根据pojo对象上的注解或xml中的映射标签,找到对应的数据库表字段,然后把查到的记录封装到pojo对象中。例如:

List<User> users = session.selectList("select * from user"); // 查询所有用户信息并返回list
for (User user : users){
System.out.println(user); // 打印每个用户的信息
}

执行上面这段代码,会把数据库表的每一条记录封装成User对象,并放入users列表中,再遍历users列表,打印每个用户的信息。

4.2 javabean-bean

用@Results注解或@Result注解,可以将查询结果封装成指定的javabean对象。例如:

public interface BlogDao {

@Select("SELECT * FROM blog WHERE id=#{id}")
@Results({
@Result(property="author", column="author_id", javaType=Author.class,
 one=@One(select="com.domain.dao.AuthorDao.getById")), // 通过one标签关联Author表
@Result(property="categories", column="id", javaType=ArrayList.class, many=@Many(select="com.domain.dao.CategoryDao.getByBlogId")) // 通过many标签关联Category表
})
Blog getById(Integer id);

}

@Results注解用于指定javabean对象属性的映射关系,@Result注解用于指定某个属性的映射关系。通过javaType标签指定该属性的java类型;column标签指定该属性映射的数据库字段;one和many标签分别用于关联一对一和一对多的关系。

通过one标签,可以查询出作者的详细信息,通过javaType和one标签指定作者的java类型为Author类型,one标签的select属性指定了获取作者详情的方法。同样,通过many标签,可以查询出分类的列表,通过javaType和many标签指定分类的java类型为ArrayList类型,many标签的select属性指定了获取分类列表的方法。

例如:

Blog blog = blogDao.getById(1);
System.out.println(blog.getTitle()); // 获取博客标题
System.out.println(blog.getAuthor().getName()); // 获取作者姓名
System.out.println(blog.getCategories().size()); // 获取分类数量

以上代码,通过BlogDao的getById方法查询出id为1的博客信息,包括作者信息和分类信息。

4.3 map-map-map

有时候,查询出来的记录不是pojo对象,而是map集合。例如,要查询用户信息的同时,还需要查询其最近一次登录的时间,则可以通过map-map-map映射类型。

public interface UserInfoService {

@Select("SELECT u.*, l.lastLoginTime FROM userinfo u LEFT JOIN lastlogintime l ON u.userId=l.userId WHERE u.userName=#{userName} AND u.passWord=#{passWord}")
@Results({
 @Result(id=true, property="userId", column="USERID"),
 @Result(property="userName", column="USERNAME"),
 @Result(property="passWord", column="PASSWORD"),
 @Result(property="email", column="EMAIL"),
 @Result(property="phoneNum", column="PHONENUM"),
 @Result(property="regDate", column="REGDATE"),
 @Result(property="lastLoginTime", column="LASTLOGINTIME", typeHandler=CustomTypeHandler.class), // 使用自定义类型转换器映射lastLoginTime字段
 @Result(property="isDeleted", column="ISDELETED")})
Map<String, Object> getUserInfoByUserNameAndPassword(@Param("userName") String userName,
 @Param("passWord") String passWord);

}

在这个例子中,有一个userinfo表和一个lastlogintime表,它们之间存在一对一的关系。因此,查询出来的记录不是pojo对象,而是map集合,所以这里采用的是map-map-map映射类型。

通过@Result注解,可以把查到的记录映射成pojo对象一样的形式。其中,property标签指定属性的名字,column标签指定映射的数据库字段,typeHandler标签指定自定义类型转换器类。

通过自定义类型转换器CustomTypeHandler.class,可以在lastLoginTime字段上加上类型转换器,使得它可以映射为LocalDateTime类型。

public class CustomTypeHandler extends BaseTypeHandler<LocalDateTime> {

@Override
public LocalDateTime getValue(ResultSet rs, int columnIndex) throws SQLException {
 Timestamp timestamp = rs.getTimestamp(columnIndex);
 if(timestamp == null){
return null;
 }else{
return LocalDateTime.ofInstant(timestamp.toInstant(), ZoneId.systemDefault());
 }
}

@Override
public void setNonNullParameter(PreparedStatement ps, int i, LocalDateTime parameter, JdbcType jdbcType) throws SQLException {
 Date date = Date.from(parameter.atZone(ZoneId.systemDefault()).toInstant());
 ps.setDate(i,date);
}

@Override
public LocalDateTime getNullableResult(ResultSet rs, String columnName) throws SQLException {
 Timestamp timestamp = rs.getTimestamp(columnName);
 if(timestamp == null){
return null;
 }else{
return LocalDateTime.ofInstant(timestamp.toInstant(), ZoneId.systemDefault());
 }
}

@Override
public LocalDateTime getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
 Timestamp timestamp = rs.getTimestamp(columnIndex);
 if(timestamp == null){
return null;
 }else{
return LocalDateTime.ofInstant(timestamp.toInstant(), ZoneId.systemDefault());
 }
}

}

自定义类型转换器继承自BaseTypeHandler,并且实现getValue、setNonNullParameter、getNullableResult方法。
自定义类型转换器的作用是将数据库查询的Timestamp类型转换为LocalDateTime类型。

通过Map<String, Object> getUserInfoByUserNameAndPassword(@Param(“userName”) String userName,
@Param(“passWord”) String passWord),可以得到一个map集合。例如:

Map<String, Object> userInfoMap = userInfoService.getUserInfoByUserNameAndPassword("Tom", "123456");
String userId = (String) userInfoMap.get("userId");
String email = (String) userInfoMap.get("email");
LocalDateTime lastLoginTime = (LocalDateTime) userInfoMap.get("lastLoginTime");
System.out.println(userId + "," + email + "," + lastLoginTime); // 获取用户id、邮箱和最后登录时间

此处的userInfoMap就是上面说的查询出来的map集合。

4.4 json-json-json

有时候,要查询的记录可能是json字符串。例如,要查询一个人的地址信息,由于地址信息可能很复杂,因此要转换为json字符串保存到数据库中。

public interface AddressDao {

@Select("SELECT address FROM address where userId=#{userId}")
String getAddressByUserId(Long userId);

}

在这个例子中,address表存储着用户的地址信息。由于地址信息可能非常复杂,因此在数据库中存储的是json字符串。

通过@Results注解,可以把json字符串映射成pojo对象。例如:

public interface PersonDao {

@Select("SELECT personJson FROM person where id=#{id}")
@Results({
@Result(property="personName", column="PERSONNAME"),
@Result(property="address", column="ADDRESS", typeHandler=AddressTypeHandler.class) // 使用自定义类型转换器处理json字符串
})
Person getPersonById(Integer id);

}

在这个例子中,Person类有一个属性address,它是一个json字符串。通过@Result注解,可以把查询出的json字符串映射为address属性。

public class AddressTypeHandler implements TypeHandler<Address>{

@Override
public Address parse(String s) throws SQLException {
 try {
JSONObject jsonObject = JSON.parseObject(s);
Long postcode = jsonObject.getLong("postcode");
String city = jsonObject.getString("city");
String country = jsonObject.getString("country");

return new Address(postcode, city, country);

 } catch (Exception e) {
throw new SQLException("Cannot convert to Address object due to:" + e.getMessage());
 }
}

@Override
public void setParameter(PreparedStatement preparedStatement, int i, Address o, JdbcType jdbcType) throws SQLException {
 JSONObject jsonObject = new JSONObject();
 jsonObject.put("postcode", o.getPostcode());
 jsonObject.put("city", o.getCity());
 jsonObject.put("country", o.getCountry());
 
 preparedStatement.setString(i,jsonObject.toJSONString());
 
}

}

在这个例子中,自定义类型转换器AddressTypeHandler继承自TypeHandler


AddressTypeHandler的parse方法用于解析json字符串,并转化为Address对象;setParameter方法用于设置PreparedStatement的参数。
这样,通过PersonDao的getPersonById方法,可以得到Person对象,其address属性是一个Address对象。

5.未来发展趋势与挑战

5.1 ORM框架的发展趋势

ORM框架的发展趋势是朝着越来越强大的方向发展的,ORM框架不仅可以解决数据的持久层映射问题,而且还能解决诸如分页、权限控制、数据验证等更为复杂的功能。
目前,Hibernate、EclipseLink、Mybatis等都是比较流行的ORM框架。

Mybatis和Hibernate最大的区别是什么呢?两者都属于JavaEE中的ORM框架。但Mybatis只是一个SQL映射框架,只负责SQL语句的转义,而Hibernate则是一个完整的ORM解决方案,它支持关联映射、集合映射、动态SQL、缓存机制等。

5.2 数据类型的映射

在Mybatis中,数据类型主要分为4类:
数字类型:int、long、double、BigDecimal
字符类型:string、char
日期类型:date、time、datetime、timestamp
BLOB和CLOB类型:byte[]、string

在这4类数据类型中,日期类型、BLOB类型和CLOB类型需要特殊处理。Mybatis支持通过类型转换器进行类型转换。

5.2.1 数字类型

如果需要映射数字类型,不需要特别指定javaType,Mybatis默认支持将数据库查询的相应列值转换为java.lang.Integer、java.lang.Long、java.lang.Double、java.math.BigDecimal。

5.2.2 字符类型

需要注意的是,Mybatis默认情况下,将数据库查询的字符类型转换为java.lang.String,不会自动将非ASCII字符转为中文。如果希望自动转为中文,需要配置参数useUnicode=true。

5.2.3 日期类型

Mybatis默认情况下,将数据库查询的日期类型转换为java.util.Date。如果需要自定义日期类型,可以通过typeHandler属性配置自定义类型转换器。例如:

<resultMap id="userMapper" type="User">
<!-- other mappings -->
<result column="birthday" property="birthday" typeHandler="MyCustomTypeHandler"/>
</resultMap>
<typeHandler name="MyCustomTypeHandler" class="com.example.MyCustomTypeHandler"/>
package com.example;

import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.apache.ibatis.type.TypeHandler;

import java.sql.*;

@MappedJdbcTypes(value = { JdbcType.TIMESTAMP }) // 指定jdbc类型为timestamp
@MappedTypes(value = { java.util.Date.class }) // 指定java类型为java.util.Date
public class MyCustomTypeHandler implements TypeHandler<java.util.Date> {

@Override
public java.util.Date getResult(ResultSet resultSet, String s) throws SQLException {
 Timestamp ts = resultSet.getTimestamp(s);
 if(ts!= null) {
return new java.util.Date(ts.getTime());
 } else {
return null;
 }
}

@Override
public void setParameter(PreparedStatement preparedStatement, int i, java.util.Date date,
JdbcType jdbcType) throws SQLException {
 preparedStatement.setTimestamp(i, new Timestamp(date.getTime()));
}

}

在此例中,MyCustomTypeHandler实现org.apache.ibatis.type.TypeHandler接口,用来转换数据库查询的Timestamp类型为java.util.Date类型。

5.2.4 BLOB和CLOB类型

Mybatis默认情况下,将数据库查询的Blob和Clob类型转换为InputStream。如果希望将Blob和Clob直接映射为字节数组或字符串,可以通过typeHandler属性配置自定义类型转换器。例如:

<resultMap id="photoMapper" type="Photo">
<!-- other mappings -->
<result column="data" property="data" typeHandler="ByteArrayTypeHandler"/>
</resultMap>
<typeHandler name="ByteArrayTypeHandler" class="com.example.ByteArrayTypeHandler"/>
package com.example;

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;

import java.io.IOException;
import java.io.InputStream;
import java.sql.Blob;
import java.sql.SQLException;

public class ByteArrayTypeHandler extends BaseTypeHandler<byte[]> {

@Override
public byte[] getBytes(final ResultSet rs, final String columnName) throws SQLException {
 Blob blob = rs.getBlob(columnName);
 if (blob!= null) {
InputStream inputStream = blob.getBinaryStream();
try {
return toByteArray(inputStream);
} finally {
closeQuietly(inputStream);
}
 } else {
return null;
 }
}

@Override
protected byte[] composeNull(final Connection connection) {
 return null;
}

/**
 * Copied from {@link org.apache.commons.io.IOUtils#toByteArray(InputStream)}
 */
private static byte[] toByteArray(final InputStream input) throws IOException {
 if (!(input instanceof java.io.ByteArrayInputStream)) {
return toByteArray(new java.io.ByteArrayOutputStream().writeAllBytes(input));
 } else {
final byte[] byteArray = ((java.io.ByteArrayInputStream) input).buf;
final int offset = ((java.io.ByteArrayInputStream) input).pos;
final int length = ((java.io.ByteArrayInputStream) input).count;
if (offset == 0 && length == byteArray.length) {
return byteArray;
} else {
return copyOfRange(byteArray, offset, offset + length);
}
 }
}

/**
 * Copied from {@link org.apache.commons.lang3.ArrayUtils#copyOfRange(T[],int,int)}
 */
private static byte[] copyOfRange(final byte[] original, final int start, final int end) {
 final int originalLength = original.length;
 if (start > end || start < 0 || end > originalLength) {
throw new IllegalArgumentException();
 }
 final int newLength = end - start;
 final byte[] copy = new byte[newLength];
 arraycopy(original, start, copy, 0, Math.min(newLength, originalLength - start));
 return copy;
}

/**
 * Copied from {@link org.apache.commons.io.IOUtils#closeQuietly(Closeable)}
 */
private static void closeQuietly(final Closeable closeable) {
 try {
if (closeable!= null) {
closeable.close();
}
 } catch (final IOException ignored) {}
}

}

在此例中,ByteArrayTypeHandler实现org.apache.ibatis.type.BaseTypeHandler接口,用来直接将数据库查询的Blob类型转换为字节数组。

5.3 性能优化

Mybatis提供了很多插件来提升系统的性能,例如缓存、分页、延迟加载等。下面介绍几个常用的插件。

5.3.1 一级缓存

一级缓存是Mybatis默认开启的一级缓存。一级缓存能够提升查询效率,因为它能够命中缓存,而不需要再次从数据库查询。但是,一级缓存也有它的局限性。首先,一级缓存空间有限,因此不能存放过多的对象。其次,Mybatis会对每个语句都维护一份缓存,如果一个方法被调用多次,就会产生多份缓存。

设置一级缓存大小:

<settings>
<setting name="cacheEnabled" value="true"/> <!-- 是否启用二级缓存,默认为false -->
<setting name="defaultCacheEvictionPolicy" value="LRU"/> <!-- 默认的回收策略,LRU(least recently used)表示最近最少使用 -->
<setting name="cacheSize" value="512"/> <!-- 一级缓存的大小,单位为字节,默认为256 -->
</settings>

默认情况下,Mybatis的缓存会一直有效,除非手动清空。也可以使用自定义拦截器清空缓存。

5.3.2 二级缓存

二级缓存是Mybatis提供的另一级缓存。二级缓存可以根据一级缓存失效或者查询条件变化来实现。与一级缓存不同的是,二级缓存不需要每个方法都单独维护一份缓存,避免产生多份缓存,能有效降低内存消耗。

配置二级缓存:

<cache>
<ehcache />
</cache>

在这种情况下,Mybatis会将缓存放在Ehcache中。Ehcache是Java开源缓存框架,能够提供高速缓存访问。

设置缓存的过期时间:

<cache>
<cache-eviction max-entries="1000" time-to-live="60000"/>
</cache>

在这种情况下,缓存的大小最多只能有1000条,每隔60秒缓存数据就会过期。

清空缓存:

configuration.getCache("myCache").clear();

SessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(reader);
Configuration configuration = sessionFactory.getConfiguration();
Cache myCache = configuration.getCache("myCache");
myCache.clear();

此外,Mybatis也提供了监听器来监听缓存的命中次数、命中率等情况。

5.3.3 分页插件

分页插件用于拦截StatementHandler执行SELECT语句,根据分页参数设置limit语句。

配置分页插件:

<plugins>
<plugin interceptor="org.mybatis.example.ExamplePlugin">
 <property name="properties">
<props>
<prop key="param1">value1</prop>
<prop key="param2">value2</prop>
</props>
 </property>
</plugin>
</plugins>
ExamplePlugin plugin = new ExamplePlugin();
Properties properties = new Properties();
properties.setProperty("param1","value1");
properties.setProperty("param2","value2");
plugin.setProperties(properties);

插件初始化时,可以传入参数。

使用分页插件:

PageHelper pageHelper = new PageHelper();
Properties properties = new Properties();
properties.setProperty("offsetAsPageNumber","true");
properties.setProperty("rowBoundsWithCount","true");
pageHelper.setProperties(properties);

Executor executor = new SimpleExecutor(configuration);
executor = new CachingExecutor(executor);
executor = new BatchExecutor(executor);
executor = pageHelper.wrapExecutor(executor);
try {
Page page = PageHelper.startPage(pageNo, pageSize);
list = mapper.selectAll();
long total = PageHelper.getTotal(pageSize);
page.setTotal(total);
} finally {
PageHelper.endPage();
}

分页插件可以设置偏移量从零开始还是从1开始计数,是否从第一条记录开始计算总数等参数。分页插件的startPage方法会返回一个分页参数对象,在finally块中调用endPage方法可以输出统计信息。

5.3.4 延迟加载插件

延迟加载插件可以实现懒加载,在真正需要用到某属性的值时才去查询数据库。

配置延迟加载插件:

<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
 <property name="properties">
<props>
<prop key="supportMethodsArguments">true</prop>
<prop key="params">count=countSql</prop>
<prop key="autoRuntimeDialect">true</prop>
</props>
 </property>
</plugin>
</plugins>
PageInterceptor interceptor = new PageInterceptor();
Properties properties = new Properties();
properties.setProperty("supportMethodsArguments", "true");
properties.setProperty("params", "count=countSql");
properties.setProperty("autoRuntimeDialect", "true");
interceptor.setProperties(properties);

参数解释如下:
supportMethodsArguments:是否支持通过Mapper接口参数传递当前页号和页面大小
params:额外参数,多个参数以逗号分隔
autoRuntimeDialect:是否自动检测数据库方言

使用延迟加载插件:

public interface EmployeeMapper {
@SelectProvider(type=EmployeeProvider.class, method="dynamicSQL")
List<Employee> selectEmployees(Map<String, Object> map);
}

public class EmployeeProvider {
public String dynamicSQL(Map<String, Object> map) {
 StringBuilder sb = new StringBuilder();
 sb.append("SELECT * FROM employee ");
 if (!CollectionUtils.isEmpty(map)){
boolean isFirst = true;
Iterator it = map.entrySet().iterator();
while (it.hasNext()) {
Entry entry = (Entry) it.next();
String key = (String) entry.getKey();
Object value = entry.getValue();
if (value!= null) {
 if ("dept".equals(key)) {
sb.append("WHERE dept_id IN ")
.append("(SELECT id FROM department WHERE dept_name LIKE '%" + value + "%')");
 } else if ("empName".equals(key)) {
sb.append((!isFirst? "AND " : "") + " emp_name LIKE '%" + value + "%'");
 } else if ("email".equals(key)) {
sb.append((!isFirst? "AND " : "") + " email LIKE '%" + value + "%'");
 }
}
isFirst = false;
}
 }
 return sb.toString();
}
}

EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
HashMap<String, Object> map = new HashMap<>();
map.put("dept", "财务部");
map.put("empName", "李%");
map.put("email", "@%163.com");
List<Employee> employees = mapper.selectEmployees(map);

EmployeeProvider类的dynamicSQL方法是一个简单的实现,用来生成SQL语句,如果参数不为空,会添加条件。selectEmployees方法接收了一个map参数,可以传送不同的条件来实现分页查询。

5.3.5 其它插件

DruidDataSourceStatCollector:Druid连接池插件,可以查看连接池的状态信息。
PerformanceMonitorPlugin:监控数据库访问的插件,可以统计慢SQL。

6.附录常见问题与解答

6.1 为何Mybatis只能映射pojo对象?

为了更好的维护代码,Mybatis要求所有的SQL语句只能映射pojo对象。pojo对象可以让开发人员更方便地管理业务逻辑和数据,能有效减少错误和不必要的SQL语句。

6.2 为何mybatis不支持任意数据类型?

支持任意数据类型可能会导致运行时的异常,例如日期格式不正确、数据溢出等。如果业务上需要支持更多的数据类型,需要自定义类型转换器。

6.3 为什么不建议在XML中写SQL语句?

XML中写SQL语句会导致代码难以阅读和维护,且不利于代码复用。尽量不要在XML中写SQL语句,可以将SQL语句放在外部的SQL文件中,并通过Mybatis的sqlSessionFactoryBuilder.build()方法加载到mybatis环境中。

6.4 Mybatis的一级缓存和二级缓存的区别?

一级缓存是Mybatis内置的缓存,默认开启,对每个方法都会单独维护一份缓存,导致缓存过多,占用内存过多。而二级缓存可以根据一级缓存的命中情况或者查询条件变化来实现,不会产生多份缓存,能够有效降低内存消耗。

6.5 Mybatis中的Interceptor有什么作用?

Interceptor是一个拦截器,可以对Mybatis请求处理过程进行干预。Interceptor接口只有一个方法execute(),定义了Mybatis请求处理流程中的方法调用顺序。

6.6 Mybatis支持的数据库有哪些?

Mybatis支持各种主流数据库,如MySQL、Oracle、DB2、PostgreSQL、HSQLDB等。

6.7 Mybatis提供的常用API有哪些?

Mybatis中有许多常用的API,如:SqlSessionFactoryBuilder、SqlSessionFactory、SqlSession、MapperScannerConfigurer等。

评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

光剑书架上的书

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值