mybatis的延迟加载是如何实现的?

更新于
2026-10-09 08:45:50
1阅读来源:SEO基础
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计1522个文字,预计阅读时间需要7分钟。

mybatis的延迟加载是如何实现的?

MyBatis的延迟加载主要用于解决N+1查询问题,即当查询关联数据时,如果不使用延迟加载,每次查询主表数据时都会额外执行一次关联表的查询,导致性能低下。

延迟加载原理

延迟加载的核心思想是懒加载,即在需要的时候才去加载关联数据。MyBatis通过在查询主表数据时,不立即加载关联数据,而是将关联数据的查询语句封装起来,当真正需要访问关联数据时,才执行查询。

使用方法

1. 配置映射文件:在MyBatis的映射文件中,对于关联查询的SQL语句,使用``或``标签,并设置`fetchType=lazy`属性。

2.开启延迟加载:在MyBatis的配置文件中,设置``标签下的``。

优点

- 提高性能:减少数据库访问次数,降低系统负载。

- 减少内存消耗:只有真正需要时才加载关联数据,节省内存资源。

缺点及规避问题

- 性能问题:在访问延迟加载的数据时,可能会出现性能问题,尤其是在数据量大时。- 线程安全问题:在多线程环境下,可能会出现数据不一致的问题。- 避免问题: - 缓存:使用缓存可以减少数据库访问次数,提高性能。 - 线程安全:在访问延迟加载的数据时,确保线程安全,避免数据不一致。

总结

延迟加载是一种提高性能和降低内存消耗的有效方法,但在使用时需要注意其优缺点,合理配置和使用。

本文主要介绍下mybatis的延迟加载,从原理上介绍下怎么使用、有什么好处能规避什么问题。延迟加载一般用于级联查询(级联查询可以将主表不能直接查询的数据使用自定义映射规则调用字表来查,主查询查完之后通过某个column列或多个列将查询结果传递给子查询,子查询再根据主查询传递的参数进行查询,最后将子查询结果进行映射)。mybatis的懒加载是通过创建代理对象来实现的,只有当调用getter等方法的时候才会去查询子查询,查询后完成设值再获取值。

1. 什么时候会创建代理对象

private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException { this.useConstructorMappings = false; // reset previous mapping result final List<Class<?>> constructorArgTypes = new ArrayList<>(); final List<Object> constructorArgs = new ArrayList<>(); // 创建result接收对象 Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix); if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) { // 处理其他属性properties final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings(); for (ResultMapping propertyMapping : propertyMappings) { // issue gcode #109 && issue #149 创建代理 if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) { resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs); break; } } } // 使用有参构造函数创建了对象 this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result return resultObject; }

通过mybatis代码propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()发现只有当存在嵌套查询select子句和isLazy=true的时候才会创建代理,那么isLazy=true是什么条件,从创建ResultMapping的代码中可以看到boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager")); 只有手动设置fetchType=lazy或者全局设置configuration的lazyLoadingEnabled=true,两者缺一不可。

关于代理是通过Javassist创建的,下面有一个简单的例子

public class HelloMethodHandler implements MethodHandler { private Object target; public HelloMethodHandler(Object o) { this.target = o; } @Override public Object invoke(Object self, Method thisMethod, Method proceed, Object[] args) throws Throwable { String methodName = thisMethod.getName(); if (methodName.startsWith("get")) { System.out.println("select database...."); // 进行sql查询到结果并set设置值 ((Student)self).setName("monian"); } return proceed.invoke(self, args); } public static void main(String[] args) throws Exception { Student student = new Student(); ProxyFactory proxyFactory = new ProxyFactory(); proxyFactory.setSuperclass(Student.class); Constructor<Student> declaredConstructor = Student.class.getDeclaredConstructor(); Object o = proxyFactory.create(declaredConstructor.getParameterTypes(), new Object[]{}); ((Proxy)o).setHandler(new HelloMethodHandler(student)); Student proxy = (Student)o; System.out.println(proxy.getName()); } }

mybatis的原理就是通过创建一个代理对象,当通过这个代理对象调用getter、is、equals、clone、toString、hashCode等方法时会调用select子查询,然后完成设置,最后取值就像早就获取到一样。

2. 如何使用

public class UserDO { private Integer userId; private String username; private String password; private String nickname; private List<PermitDO> permitDOList; public UserDO() {} } <resultMap id="BaseMap" type="org.apache.ibatis.study.entity.UserDO"> <id column="user_id" jdbcType="INTEGER" property="userId" /> <result column="username" jdbcType="VARCHAR" property="username" /> <result column="password" jdbcType="VARCHAR" property="password" /> <result column="nickname" jdbcType="VARCHAR" property="nickname"/> <collection property="permitDOList" column="user_id" select="getPermitsByUserId" fetchType="lazy"> </collection> </resultMap> <resultMap id="PermitBaseMap" type="org.apache.ibatis.study.entity.PermitDO"> <id column="id" jdbcType="INTEGER" property="id"/> <result column="code" jdbcType="VARCHAR" property="code"/> <result column="name" jdbcType="VARCHAR" property="name"/> <result column="type" jdbcType="TINYINT" property="type"/> <result column="pid" jdbcType="INTEGER" property="pid"/> </resultMap> <select id="getByUserId2" resultMap="BaseMap"> select * from user where user_id = #{userId} </select> <select id="getPermitsByUserId" resultMap="PermitBaseMap"> select p.* from user_permit up inner join permit p on up.permit_id = p.id where up.user_id = #{userId} </select>

通过fetchType=lazy指定子查询getPermitsByUserId使用懒加载,这样的话就不用管全局配置lazyLoadingEnabled是true还是false了。当然这里可以直接用多表关联查询不使用子查询,使用方法在上一篇文章

测试代码

mybatis的延迟加载是如何实现的?

public class Test { public static void main(String[] args) throws IOException { try (InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml")) { // 构建session工厂 DefaultSqlSessionFactory SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession sqlSession = sqlSessionFactory.openSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); UserDO userDO = userMapper.getByUserId2(1); System.out.println(userDO); } } }

结果如下,打了断点可以看到原userDO对象已被代理并且permitDOList是null需要调用get方法才会去查询拿到值,咳咳这边之前直接运行显示是已经把permitDOList查询出来了,想了半天啥原因后来才发现println会调用userDO对象的toString方法,而toString方法也会走代理方法直接去调用子查询的

3.延迟加载的好处

延迟加载主要能解决mybatis的N+1问题,什么是N+1问题其实叫1+N更为合理,以上面的业务例子来说就是假设一次查询出来10000个用户,那么还需要针对这10000个用户使用子查询getPermitsByUserId获取每个用户的权限列表,需要10000次查询,总共10001次,真实情况下你可能并不需要每个子查询的结果,这样就浪费数据库连接资源了。如果使用延迟加载的话就相当于不用进行这10000次查询,因为它是等到你真正使用的时候才会调用子查询获取结果。

本文共计1522个文字,预计阅读时间需要7分钟。

mybatis的延迟加载是如何实现的?

MyBatis的延迟加载主要用于解决N+1查询问题,即当查询关联数据时,如果不使用延迟加载,每次查询主表数据时都会额外执行一次关联表的查询,导致性能低下。

延迟加载原理

延迟加载的核心思想是懒加载,即在需要的时候才去加载关联数据。MyBatis通过在查询主表数据时,不立即加载关联数据,而是将关联数据的查询语句封装起来,当真正需要访问关联数据时,才执行查询。

使用方法

1. 配置映射文件:在MyBatis的映射文件中,对于关联查询的SQL语句,使用``或``标签,并设置`fetchType=lazy`属性。

2.开启延迟加载:在MyBatis的配置文件中,设置``标签下的``。

优点

- 提高性能:减少数据库访问次数,降低系统负载。

- 减少内存消耗:只有真正需要时才加载关联数据,节省内存资源。

缺点及规避问题

- 性能问题:在访问延迟加载的数据时,可能会出现性能问题,尤其是在数据量大时。- 线程安全问题:在多线程环境下,可能会出现数据不一致的问题。- 避免问题: - 缓存:使用缓存可以减少数据库访问次数,提高性能。 - 线程安全:在访问延迟加载的数据时,确保线程安全,避免数据不一致。

总结

延迟加载是一种提高性能和降低内存消耗的有效方法,但在使用时需要注意其优缺点,合理配置和使用。

本文主要介绍下mybatis的延迟加载,从原理上介绍下怎么使用、有什么好处能规避什么问题。延迟加载一般用于级联查询(级联查询可以将主表不能直接查询的数据使用自定义映射规则调用字表来查,主查询查完之后通过某个column列或多个列将查询结果传递给子查询,子查询再根据主查询传递的参数进行查询,最后将子查询结果进行映射)。mybatis的懒加载是通过创建代理对象来实现的,只有当调用getter等方法的时候才会去查询子查询,查询后完成设值再获取值。

1. 什么时候会创建代理对象

private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException { this.useConstructorMappings = false; // reset previous mapping result final List<Class<?>> constructorArgTypes = new ArrayList<>(); final List<Object> constructorArgs = new ArrayList<>(); // 创建result接收对象 Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix); if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) { // 处理其他属性properties final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings(); for (ResultMapping propertyMapping : propertyMappings) { // issue gcode #109 && issue #149 创建代理 if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) { resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs); break; } } } // 使用有参构造函数创建了对象 this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result return resultObject; }

通过mybatis代码propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()发现只有当存在嵌套查询select子句和isLazy=true的时候才会创建代理,那么isLazy=true是什么条件,从创建ResultMapping的代码中可以看到boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager")); 只有手动设置fetchType=lazy或者全局设置configuration的lazyLoadingEnabled=true,两者缺一不可。

关于代理是通过Javassist创建的,下面有一个简单的例子

public class HelloMethodHandler implements MethodHandler { private Object target; public HelloMethodHandler(Object o) { this.target = o; } @Override public Object invoke(Object self, Method thisMethod, Method proceed, Object[] args) throws Throwable { String methodName = thisMethod.getName(); if (methodName.startsWith("get")) { System.out.println("select database...."); // 进行sql查询到结果并set设置值 ((Student)self).setName("monian"); } return proceed.invoke(self, args); } public static void main(String[] args) throws Exception { Student student = new Student(); ProxyFactory proxyFactory = new ProxyFactory(); proxyFactory.setSuperclass(Student.class); Constructor<Student> declaredConstructor = Student.class.getDeclaredConstructor(); Object o = proxyFactory.create(declaredConstructor.getParameterTypes(), new Object[]{}); ((Proxy)o).setHandler(new HelloMethodHandler(student)); Student proxy = (Student)o; System.out.println(proxy.getName()); } }

mybatis的原理就是通过创建一个代理对象,当通过这个代理对象调用getter、is、equals、clone、toString、hashCode等方法时会调用select子查询,然后完成设置,最后取值就像早就获取到一样。

2. 如何使用

public class UserDO { private Integer userId; private String username; private String password; private String nickname; private List<PermitDO> permitDOList; public UserDO() {} } <resultMap id="BaseMap" type="org.apache.ibatis.study.entity.UserDO"> <id column="user_id" jdbcType="INTEGER" property="userId" /> <result column="username" jdbcType="VARCHAR" property="username" /> <result column="password" jdbcType="VARCHAR" property="password" /> <result column="nickname" jdbcType="VARCHAR" property="nickname"/> <collection property="permitDOList" column="user_id" select="getPermitsByUserId" fetchType="lazy"> </collection> </resultMap> <resultMap id="PermitBaseMap" type="org.apache.ibatis.study.entity.PermitDO"> <id column="id" jdbcType="INTEGER" property="id"/> <result column="code" jdbcType="VARCHAR" property="code"/> <result column="name" jdbcType="VARCHAR" property="name"/> <result column="type" jdbcType="TINYINT" property="type"/> <result column="pid" jdbcType="INTEGER" property="pid"/> </resultMap> <select id="getByUserId2" resultMap="BaseMap"> select * from user where user_id = #{userId} </select> <select id="getPermitsByUserId" resultMap="PermitBaseMap"> select p.* from user_permit up inner join permit p on up.permit_id = p.id where up.user_id = #{userId} </select>

通过fetchType=lazy指定子查询getPermitsByUserId使用懒加载,这样的话就不用管全局配置lazyLoadingEnabled是true还是false了。当然这里可以直接用多表关联查询不使用子查询,使用方法在上一篇文章

测试代码

mybatis的延迟加载是如何实现的?

public class Test { public static void main(String[] args) throws IOException { try (InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml")) { // 构建session工厂 DefaultSqlSessionFactory SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession sqlSession = sqlSessionFactory.openSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); UserDO userDO = userMapper.getByUserId2(1); System.out.println(userDO); } } }

结果如下,打了断点可以看到原userDO对象已被代理并且permitDOList是null需要调用get方法才会去查询拿到值,咳咳这边之前直接运行显示是已经把permitDOList查询出来了,想了半天啥原因后来才发现println会调用userDO对象的toString方法,而toString方法也会走代理方法直接去调用子查询的

3.延迟加载的好处

延迟加载主要能解决mybatis的N+1问题,什么是N+1问题其实叫1+N更为合理,以上面的业务例子来说就是假设一次查询出来10000个用户,那么还需要针对这10000个用户使用子查询getPermitsByUserId获取每个用户的权限列表,需要10000次查询,总共10001次,真实情况下你可能并不需要每个子查询的结果,这样就浪费数据库连接资源了。如果使用延迟加载的话就相当于不用进行这10000次查询,因为它是等到你真正使用的时候才会调用子查询获取结果。