1、说说什么是Mybatis?
MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
总之,Mybatis是一个可靠、灵活且易于使用的框架,它可以帮助开发人员更高效地与数据库进行交互,提升开发效率和代码的可维护性。
2、什么是ORM?为什么说Mybatis是半ORM?
ORM(Object Relational Mapping),对象关系映射,是一种为了解决关系型数据库数据与简单Java对象(POJO)的映射关系的技术。简单来说,ORM是通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系型数据库中。
Mybatis被称为半自动ORM(对象关系映射)映射工具,主要是因为它在对象和数据库表之间的映射过程中需要开发人员手动编写SQL语句。这与全自动的ORM框架(如Hibernate)不同,在全自动的ORM框架中,开发人员通常只需定义实体类和配置文件,框架会自动生成对应的SQL语句,从而实现对象与数据库表之间的映射。
3、Mybatis的优点有哪些?
MyBatis 是一种优秀的持久层框架,相较于传统的 JDBC,它具有许多优点:
-
解耦合:MyBatis 将数据库操作逻辑与业务操作解耦,使开发人员可以更专注于业务逻辑的处理。
-
简化代码:MyBatis 使得代码结构更清晰、代码量更少,开发人员只需编写 SQL 就能访问数据库,不需要关心数据库连接等额外操作。
-
对象映射:MyBatis 可以将数据库表的字段按照业务规则直接映射到数据对象(DO),无需手动转换数据,简化了开发流程。
其他优点包括:
-
多数据源支持:MyBatis 支持多种数据源,如 POOLED、UNPOOLED、JNDI,并可以整合其他数据库连接池如 HikariCP、Druid、C3p0 等。
-
动态 SQL 支持:MyBatis 提供强大的动态 SQL 功能,如 if/foreach 等常用的动态标签,可以大大减少代码的开发量。
-
缓存支持:MyBatis 支持事务性的一级缓存、二级缓存和自定义缓存。一级缓存是以 session 为生命周期,默认开启;二级缓存则可以根据配置的算法来计算过期时间(FIFO、LRU 等)。
总的来说,MyBatis 的优点包括解耦合、简化代码、对象映射、多数据源支持、动态 SQL 支持和缓存支持,这些特性使得 MyBatis 成为许多开发人员首选的持久层框架之一。
看一段用JDBC和Mybatis写的代码,就能顿悟了:
JDBC 示例代码以及相关说明:
public void testJdbc() {
String url = "dbLink";
try (Connection conn = DriverManager.getConnection(url, "root", "password")) {
// 加载MySQL驱动
Class.forName("com.mysql.cj.jdbc.Driver");
int author = 1;
String date = "2018.06.10";
String sql = "SELECT id, title, content, create_time FROM article WHERE author_id = " + author
+ " AND create_time > '" + date + "'";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
List<Article> articles = new ArrayList<>(rs.getRow());
while (rs.next()) {
Article article = new Article();
article.setId(rs.getInt("id"));
article.setTitle(rs.getString("title"));
article.setContent(rs.getString("content"));
article.setCreateTime(rs.getDate("create_time"));
articles.add(article);
}
System.out.println("Query SQL ==> " + sql);
System.out.println("Query Result: ");
articles.forEach(System.out::println);
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
}
}
在上面的代码中,testJdbc()
方法展示了使用 JDBC 进行数据库操作的流程。首先,建立数据库连接,执行 SQL 查询语句,遍历结果集并将数据映射到 Article
对象中,最后输出查询结果。
这里是提供的 MyBatis 示例代码,并附上了简要说明:
public void testMyBatis() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
inputStream.close();
try (SqlSession session = sqlSessionFactory.openSession()) {
ArticleDao articleDao = session.getMapper(ArticleDao.class);
List<Article> articles = articleDao.findByAuthorAndCreateTime(1, "2018-06-10");
System.out.println(articles);
}
}
interface ArticleDao {
@Select("SELECT id, title, content, create_time FROM article WHERE author_id = #{id} AND create_time > #{time}")
List<Article> findByAuthorAndCreateTime(int id, String time);
}
在上面的代码中,testMyBatis()
方法展示了使用 MyBatis 进行数据库操作的流程。首先,加载 MyBatis 配置文件,创建 SqlSessionFactory
,然后通过 SqlSession
获取 ArticleDao
接口的实现。在 ArticleDao
接口中使用注解 @Select
指定了 SQL 查询语句,并定义了 findByAuthorAndCreateTime
方法。最后,调用该方法进行数据库查询并输出结果。
这段代码展示了 MyBatis 的优点之一:简洁明了的 SQL 映射,使得数据库操作更加直观和高效。
相比较于 MyBatis, JDBC 代码存在一些缺点:
- SQL 与 Java 代码混合在一起,使得代码难以维护和阅读。
- 需要手动处理数据库连接、Statement、ResultSet 的打开、关闭,容易出现资源泄露或错误。
- 没有对象关系映射(ORM)支持,需要手动将数据库结果集映射到 Java 对象中。
总的来说,相较于 MyBatis,纯粹的 JDBC 代码更加繁琐且冗长,缺乏灵活性和易用性,而 MyBatis 提供了更加简洁、清晰和高效的数据库操作方式。
4、MyBatis使用过程?生命周期?
使用 MyBatis 进行数据库操作的步骤:
- 创建
SqlSessionFactory
:
可以从配置文件中加载或者直接编码创建SqlSessionFactory
。
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
- 通过
SqlSessionFactory
创建SqlSession
:
SqlSession
可以理解为程序和数据库之间的桥梁。
SqlSession session = sqlSessionFactory.openSession();
- 执行数据库操作:
可以通过SqlSession
实例来直接执行已映射的 SQL 语句,或者更常用的方式是先获取 Mapper,然后再执行 SQL 语句。
// 直接执行 SQL 语句
Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
// 获取 Mapper 后执行 SQL 语句
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(101);
-
提交事务:
如果是更新、删除语句,需要调用session.commit()
提交事务。 -
关闭会话:
最后一定要记得调用session.close()
关闭会话,释放资源。
这些步骤展示了使用 MyBatis 进行数据库操作的基本流程,简洁明了地展现了与数据库交互的各个关键步骤。
MyBatis 的组件生命周期可以总结如下:
-
SqlSessionFactoryBuilder
:
SqlSessionFactoryBuilder
的实例一旦用于创建SqlSessionFactory
后就不再需要了,因此其生命周期仅存在于方法内部。 -
SqlSessionFactory
:
SqlSessionFactory
用于创建SqlSession
,类似于数据库连接池。由于创建和销毁SqlSessionFactory
会消耗数据库资源,因此最佳的做法是将其作为应用级单例,其生命周期应该与应用程序保持一致。 -
SqlSession
:
SqlSession
类似于 JDBC 中的Connection
,其实例不是线程安全的,不可被共享。因此,最佳的生命周期是在一次数据库操作请求或一个方法内部。 -
Mapper
:
映射器是绑定映射语句的接口,通过SqlSession
获取。通常情况下,映射器接口的生命周期应控制在与SqlSession
相关的事务方法内部,最好在方法级别进行管理。
这些组件的生命周期管理对于有效地利用 MyBatis 进行数据库操作非常重要,能够避免资源浪费和潜在的线程安全问题。
5、 # 和 $ 的区别是什么?什么情况必须用$
#{}和{}是Mybatis中用于动态参数替换的两种方式,其中#{}会进行预编译处理,传入的参数会被替换为?,可以避免SQL注入。而 $ {}不会进行预编译处理,传入的参数直接被替换,需要注意防止SQL注入问题。
#{}和${}在预编译处理中是不一样的。#{}类似jdbc中的PreparedStatement,对于传入的参数,在预处理阶段会使用?代替,可以有效的避免SQL注入。
因此,在能够使用#{}的情况下,应尽量使用#{};但是在一些特殊情况下,如order by、group by等语句后面时,必须使用${}。
6、Mybatis插件的运行原理?
Mybatis插件的运行原理涉及三个关键接口:Interceptor、Invocation和Plugin。
Interceptor是拦截器接口,定义了Mybatis插件的基本功能,包括插件的初始化、拦截方法以及销毁方法。Invocation是调用接口,表示Mybatis在执行SQL语句时的状态,包括SQL语句、参数、返回值等信息。Plugin是插件接口,Mybatis框架在执行SQL语句时,会将所有注册的插件封装成Plugin对象,通过Plugin对象实现对SQL语句的拦截和修改。
插件的运行流程如下:
- Mybatis框架在运行时,会初始化所有实现了Interceptor接口的插件。
- 初始化后,Mybatis框架会将所有插件和原始的Executor对象封装成一个InvocationChain对象(采用责任链模式)。
- 每次执行SQL语句时,Mybatis框架都会通过InvocationChain对象依次调用所有插件的intercept方法,实现对SQL语句的拦截和修改。
- 最后,Mybatis框架将修改后的SQL语句交给原始的Executor对象执行,并将执行结果返回给调用方。
通过这种方式,Mybatis插件可以对SQL语句进行拦截和修改,实现各种功能,例如查询缓存、分页、分库分表等。
代码示例:
当涉及到代码示例时,提供Mybatis插件的具体实现是很有帮助的。以下是一个简单的示例,展示了如何创建一个Mybatis插件:
首先,创建一个实现了Interceptor接口的插件类,重写intercept方法来实现对SQL语句的拦截和修改:
@Intercepts({
@Signature(type= Executor.class, method = "update", args = {MappedStatement.class,Object.class})
})
public class ExamplePlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 在执行SQL语句之前的处理
System.out.println("Before executing SQL statement");
// 执行原始的SQL语句
Object result = invocation.proceed();
// 在执行SQL语句之后的处理
System.out.println("After executing SQL statement");
return result;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 设置属性,如果有需要的话
}
}
然后,在Mybatis的配置文件中注册这个插件:
<plugins>
<plugin interceptor="com.example.ExamplePlugin">
<!-- 可以设置一些属性 -->
</plugin>
</plugins>
通过这个简单的示例,你可以看到如何创建一个Mybatis插件,并在intercept方法中实现对SQL语句的拦截和修改。当然,实际的插件可能会更复杂,根据具体需求来实现相应的逻辑。
7、Mybatis的工作原理?
MyBatis的工作原理可以简要概括为以下几个步骤:
-
配置阶段:定义配置文件(XML或注解),解析配置文件并加载到内存中。
-
代理类生成:根据传入的接口使用JDK动态代理生成代理对象,如MapperProxy,该代理对象负责执行用户定义的SQL逻辑。
-
执行SQL:代理类生成后,开始执行具体的SQL逻辑。MapperMethod负责读取XML或方法注解的配置项,解析和记录被代理方法的入参和出参,执行SQL操作。
-
缓存机制:MyBatis有一级缓存和二级缓存。一级缓存在同一个会话中有效,而二级缓存是跨会话的。MyBatis默认提供多种数据源,包括JNDI、PooledDataSource和UnpooledDataSource,也支持引入第三方数据源。
-
查询数据库:通过数据源获取连接,然后通过StatementHandler处理类来处理和JDBC的交互,包括组装SQL、执行SQL和组装结果。
-
处理查询结果:将查询到的数据库字段映射为DO对象,通过TypeHandler完成结果的封装,包括创建实体类对象、从ResultSet取出字段、映射关系处理和反射调用setter方法将值设置到实体类对象中。
这些步骤共同构成了MyBatis的工作原理,实现了SQL配置与执行的过程。
详解
无论是 MyBatis 还是 Spring,它们的执行过程可以分为启动阶段和运行阶段。
启动阶段:
- 定义配置文件,如 XML 或注解:在这个阶段,我们需要编写配置文件来描述框架的行为,包括数据库连接信息、事务管理方式、映射关系等。或者使用注解来标记和配置相关的类和方法。
- 解析配置文件,将配置文件加载到内存中:框架会读取并解析配置文件,将配置信息加载到内存中,以便后续的运行阶段使用。
运行阶段:
- 读取内存中的配置文件,并根据配置文件实现对应的功能:在这个阶段,框架会读取已加载到内存中的配置信息,并根据配置信息实现相应的功能。对于 MyBatis 来说,它会根据配置文件中的 SQL 映射关系来执行具体的 SQL 操作;而对于 Spring 来说,它会根据配置文件中的 Bean 配置来实例化对象、管理事务、处理依赖注入等。
对于执行 SQL 的逻辑来讲,可以概括为以下步骤:
- 代理类的生成:MyBatis 使用 JDK 动态代理生成接口的代理对象,而 Spring 可以使用 JDK 动态代理或 CGLIB 生成代理对象。
- 执行 SQL:通过代理对象调用方法时,框架会解析配置文件中对应的 SQL 语句,并执行相应的数据库操作。
- 处理查询结果:对于查询操作,框架会将查询结果封装为对象,并返回给调用方。
这些步骤共同构成了 MyBatis 和 Spring 的执行过程,实现了配置解析、功能实现和结果处理等核心逻辑。
给出代码示例
当配置完成之后,假如说我们要执行一个下面一个sql,那么该如何执行呢?
TestMapper testMapper = session.getMapper(TestMapper.class);
Test test = testMapper.findOne(1);
代理类的生成
首先Mybatis会根据我们传入接口通过JDK动态代理,生成一个代理对象TestMapper,生成逻辑如下所示:
public T newInstance(SqlSession sqlSession) {
// mapperProxy实现了Invocationhandler接口,用于JDK动态代理
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
// 通过JDK动态代理生成对象
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
代理类的主要逻辑在MapperProxy中,而代理逻辑则是通过MapperMethod完成的。
对于MapperMethod来说,它在创建的时候是需要读取XML或者方法注解的配置项,所以在使用的时候才能知道具体代理的方法的SQL内容。同时,这个类也会解析和记录被代理方法的入参和出参,以方便对SQL的查询占位符进行替换,同时对查询到的SQL结果进行转换。
执行SQL
代理类生成之后,就可以执行代理类的具体逻辑,也就是真正开始执行用户自定义的SQL逻辑了。
首先会进入到MapperMethod核心的执行逻辑,如下所示:
ublic Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
// ...
return result;
}
通过代码我们可以很清晰的发现,为什么Mybatis的insert,update和delete会返回行数的原因。业务处理上,我们经常通过update==1来判断当前语句是否更新成功。
这里一共做了两件事情,一件事情是通过BoundSql将方法的入参转换为SQL需要的入参形式,第二件事情就是通过SqlSession来执行对应的Sql。下面我们通过select来举例。
缓存
Sqlsession是Mybatis对Sql执行的封装,真正的SQL处理逻辑要通过Executor来执行。Executor有多个实现类,因为在查询之前,要先check缓存是否存在,所以默认使用的是CachingExecutor类,顾名思义,它的作用就是二级缓存。
CachingExecutor的执行逻辑如下所示:
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 放缓存
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 若二级缓存为空,则重新查询数据库
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
二级缓存是和命名空间绑定的,如果多表操作的SQL的话,是会出现脏数据的。同时如果是不同的事务,也可能引起脏读,所以要慎重。
如果二级缓存没有命中则会进入到BaseExecutor中继续执行,在这个过程中,会调用一级缓存执行。
值得一提的是,在Mybatis中,缓存分为PerpetualCache, BlockingCache, LruCache等,这些cache的实现则是借用了装饰者模式。一级缓存使用的是PerpetualCache,里面是一个简单的HashMap。一级缓存会在更新的时候,事务提交或者回滚的时候被清空。换句话说,一级缓存是和SqlSession绑定的。
查询数据库
如果一级缓存中没有的话,则需要调用JDBC执行真正的SQL逻辑。我们知道,在调用JDBC之前,是需要建立连接的,如下代码所示:
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}
我们会发现,Mybatis并不是直接从JDBC获取连接的,通过数据源来获取的,Mybatis默认提供了是那种种数据源:JNDI,PooledDataSource和UnpooledDataSource,我们也可以引入第三方数据源,如Druid等。包括驱动等都是通过数据源获取的。
获取到Connection之后,还不够,因为JDBC的数据库操作是需要Statement的,所以Mybatis专门抽象出来了StatementHandler处理类来专门处理和JDBC的交互,如下所示:
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
String sql = boundSql.getSql();
statement.execute(sql);
return resultSetHandler.<E>handleResultSets(statement);
}
其实这三行代码就代表了Mybatis执行SQL的核心逻辑:组装SQL,执行SQL,组装结果。仅此而已。
具体Sql是如何组装的呢?是通过BoundSql来完成的,具体组装的逻辑大家可以从org.apache.ibatis.mapping.MappedStatement#getBoundSql中了解,这里不再赘述。
处理查询结果
当我们获取到查询结果之后,就需要对查询结果进行封装,即把查询到的数据库字段映射为DO对象。
因为此时我们已经拿到了执行结果ResultSet,同时我们也在应用启动的时候在配置文件中配置了DO到数据库字段的映射ResultMap,所以通过这两个配置就可以转换。核心的转换逻辑是通过TypeHandler完成的。
流程如下所示:
-
创建返回的实体类对象:根据查询结果对应的实体类,创建实体对象实例。如果实体类需要延迟加载,可能会先生成代理类。
-
从 ResultSet 中取出数据库字段:通过 ResultSet 获取查询结果中的数据库字段值。
-
根据 ResultMap 进行映射:根据配置的 ResultMap 中的数据库字段映射关系,将数据库字段和实体类对象进行映射。如果没有明确的映射关系,可能会默认将下划线命名转换为驼峰式命名来进行映射。
-
反射调用 setter 方法:通过 Java 的反射机制,调用实体类对象的 setter 方法,将数据库字段的值设置到实体类对象中。这样就完成了从数据库查询结果到实体对象的数据映射过程。
这个流程确保了从数据库查询结果到实体对象的顺利映射,使得我们可以方便地操作和处理查询到的数据。这种自动映射的机制可以减少开发人员的工作量,提高代码的可维护性和可读性。
8、Mybatis的缓存机制
Mybatis框架提供了两种级别的缓存:一级缓存和二级缓存。这两种缓存机制可以帮助提高查询性能,减少对数据库的频繁访问。
一级缓存(Local Cache)
-
特点:
- 一级缓存是SqlSession级别的缓存,也称为本地缓存,仅在SqlSession范围内有效。
- 一级缓存默认开启,可以通过设置
localCacheScope
参数来控制是否开启或关闭。
-
工作原理:
- 当执行查询操作时,查询结果会被存储在SqlSession的内部缓存中。
- 如果再次执行相同的查询操作,Mybatis会先检查一级缓存中是否存在相同的查询结果,如果存在,则直接返回缓存中的结果,而不再向数据库发送查询请求。
-
生命周期:
- 一级缓存的生命周期与SqlSession相同,即在同一个SqlSession中执行的多次查询会共享同一个一级缓存。
- 一级缓存会在SqlSession关闭或提交时被清空,新的SqlSession会重新创建自己的一级缓存。
二级缓存(Global Cache)
-
特点:
- 二级缓存是Mapper级别的缓存,可跨SqlSession共享缓存数据。
- 二级缓存需要手动配置启用,通过在Mapper.xml文件中配置
<cache>
标签开启。
-
工作原理:
- 当执行查询操作并命中了二级缓存后,查询结果会被缓存在二级缓存中。
- 如果再次执行相同的查询操作,Mybatis会先检查二级缓存中是否存在相同的查询结果,如果存在,则直接返回缓存中的结果,而不再向数据库发送查询请求。
-
生命周期:
- 二级缓存的生命周期与应用程序的整个生命周期相同,可以被多个SqlSession共享。
- 当有任何一个SqlSession对数据进行了更新、插入或删除操作时,会清空涉及到该数据的二级缓存信息,保证缓存数据的一致性。
注意事项:
- 在使用二级缓存时,要注意缓存的粒度和数据的一致性,尤其是对于频繁更新的数据。
- Mybatis的缓存机制是可配置的,可以根据实际情况来灵活设置缓存的开启和关闭,以及缓存的失效策略等。
通过合理配置和使用一级缓存和二级缓存,可以有效地提升系统的性能和降低数据库访问压力。当然,在使用缓存时也需要注意缓存的更新策略,避免数据一致性问题。
详解
当涉及区分一级缓存和二级缓存时,一个更清晰的示例是在同一个SqlSession
中进行查询操作并观察缓存的效果。我将为你提供一个示例来说明一级缓存和二级缓存之间的区别:
假设我们有一个UserMapper
接口和对应的XML映射文件,我们先来看一级缓存的作用:
// Java 代码示例
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 第一次查询,命中数据库
User user1 = userMapper.getUserById(1);
System.out.println(user1);
// 第二次查询,从一级缓存中获取,而非再次访问数据库
User user2 = userMapper.getUserById(1);
System.out.println(user2);
sqlSession.close();
在这个示例中,第一次查询会从数据库中获取数据,并将结果存储在一级缓存中。第二次查询相同的数据时,会直接从一级缓存中获取,而不再发送查询请求到数据库。
接下来,我们再看一下如何配置并使用二级缓存:
<!-- UserMapper.xml -->
<mapper namespace="com.example.UserMapper">
<cache/>
<select id="getUserById" resultType="User" parameterType="int">
SELECT * FROM users WHERE id = #{id}
</select>
</mapper>
// Java 代码示例
SqlSession sqlSession1 = sqlSessionFactory.openSession();
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
// 第一次查询,命中数据库,并将结果存入二级缓存
User user3 = userMapper1.getUserById(2);
System.out.println(user3);
// 提交事务,清空一级缓存
sqlSession1.commit();
sqlSession1.close();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
// 第二次查询,从二级缓存中获取,而非再次访问数据库
User user4 = userMapper2.getUserById(2);
System.out.println(user4);
sqlSession2.close();
在这个示例中,第一次查询会从数据库中获取数据,并将结果存储在二级缓存中。当第一个SqlSession
关闭后,一级缓存被清空,但二级缓存仍然有效。第二个SqlSession
打开后,再次查询相同的数据时,会直接从二级缓存中获取,而不再发送查询请求到数据库。
通过这两个示例,你可以清楚地看到一级缓存和二级缓存在不同场景下的作用和区别。一级缓存是基于SqlSession
的本地缓存,而二级缓存是全局的、可跨SqlSession
共享的缓存机制。
9、Mybatis用的什么连接池?
Mybatis内置了三种数据源,分别是Pooled,Unpooled和JNDI,其中Pooled数据源是具有连接池的。同时Mybatis也可以使用三方数据源,如Druid,Hikari,C3P0等等
Mybatis数据源的类图如下所示:
可以看到,在Mybatis中,会通过工厂模式来获得对应的数据源,那么Mybatis是在执行的哪一步获取的呢?
答案是在执行SQL之前,Mybatis会获取数据库连接Connection,而此时获得的Connection则是应用的启动的时候,已经通过配置项中的文件加载到内存中了:
<dataSource type="org.apache.ibatis.datasource.pooled.PooledDataSource">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
一般情况下,我们不会使用Mybatis默认的PooledDataSource,自带的数据库连接池存在以下三个主要缺点:
-
空闲连接占用资源:连接池维护一定数量的空闲连接,这些连接会占用系统的资源。如果连接池设置过大,会导致资源浪费;如果设置过小,可能会导致系统并发请求时连接不足,从而影响系统性能。
-
连接池大小调优困难:连接池的大小需要根据系统的并发请求量、数据库性能和硬件配置等多方面因素综合考虑。这些因素难以准确预测和调整,使得连接池大小的调优变得困难。
-
连接泄漏:如果应用程序没有正确关闭连接,连接池中的连接就会泄漏,导致连接数量不断增加,最终可能导致系统崩溃。
综上所述,虽然自带的数据库连接池能够满足基本需求,但在面对复杂的系统需求时存在一些局限性。因此,对于专业的数据库连接池来说,如Druid等,它们具有更好的性能、功能和管理特性,可以更好地满足系统对数据库连接管理的需求,推荐在实际开发中选用这些专业的数据库连接池组件。
10、什么是数据源,数据库和数据库连接池?
数据源 (DataSource) 是用于连接到数据库的工厂,它是一个抽象的接口,定义了一些方法用于获取数据库连接。数据源可以被看作是一个规范,它规定了如何获取数据库连接、如何管理连接等。
数据库 (Database) 是指按照一定的数据模型组织起来的数据集合,它可以存储、管理和维护数据。数据库主要由数据表、视图、存储过程、触发器等组成。
数据库连接池 (Database Connection Pool) 通常被用来管理数据库连接,它是在应用程序和数据库之间建立的一个缓冲池。连接池的作用是将数据库连接缓存在内存中,以便下次使用时可以复用,避免重复创建和销毁连接,提高系统性能。连接池可以有效地管理数据库连接,避免资源浪费和连接数量过多导致的性能问题。
一般应用程序连接数据库时,会通过数据源获取数据库连接,数据源负责和实体数据库的连接,如内存数据库、MySQL等。同时,数据源还可以完成对连接的池化,这就是数据库连接池。数据源可以由第三方数据库实现,并且通常由驱动程序供应商实现。数据源有基本实现、连接池实现和分布式事务实现等不同类型的实现。
Druid连接池
在Java应用程序中使用Druid连接池时,可以通过以下代码示例来说明数据源、数据库和数据库连接池的概念:
- 数据源(DataSource):数据源负责管理数据库连接,提供获取连接和释放连接的接口。在Druid中,DataSource是DruidDataSource类的实例。
import com.alibaba.druid.pool.DruidDataSource;
// 创建Druid数据源
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl("jdbc:mysql://localhost:3306/mydatabase");
dataSource.setUsername("username");
dataSource.setPassword("password");
-
数据库(Database):数据库表示实际存储数据的地方,如MySQL、Oracle等。在上述代码示例中,数据库是指URL中的数据库名"mydatabase"。
-
数据库连接池(Database Connection Pool):数据库连接池用于管理和维护数据库连接,以提高性能和资源利用率。Druid连接池是一个功能强大的数据库连接池,可以对连接进行有效管理。
// 设置连接池属性
dataSource.setMaxActive(20); // 最大活动连接数
dataSource.setInitialSize(5); // 初始连接数
dataSource.setMaxWait(60000); // 获取连接最大等待时间
通过以上代码示例,我们定义了一个Druid数据源(DataSource),设置了数据库连接的URL、用户名和密码,并对连接池属性进行了配置,包括最大活动连接数、初始连接数和获取连接的最大等待时间等。这样就将数据源、数据库和数据库连接池的概念结合起来,实现了数据库连接的管理和优化。
11、Mybatis 是否支持延迟加载?实现原理是什么?
MyBatis支持延迟加载的功能。延迟加载允许按需加载关联对象,而不是在查询主对象时立即加载所有关联对象,从而提高性能并减少不必要的数据库访问。
Mybatis支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载
lazyLoadingEnabled=true|false。
原理
它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。
当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的
代码示例
当使用 MyBatis 进行 association(一对一)和 collection(一对多)关联对象的延迟加载时,可以按照以下代码示例进行配置和使用:
- 首先,在 MyBatis 的全局配置文件中开启延迟加载:
<configuration>
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
</configuration>
- 接着,配置映射文件(Mapper XML):
<!-- OrderMapper.xml -->
<mapper namespace="com.example.OrderMapper">
<resultMap id="orderResultMap" type="Order">
<id property="id" column="id" />
<collection property="items" ofType="Item" select="com.example.ItemMapper.selectItemsByOrderId" column="id" />
</resultMap>
<select id="selectOrderById" resultMap="orderResultMap">
SELECT * FROM orders WHERE id = #{id}
</select>
</mapper>
<!-- ItemMapper.xml -->
<mapper namespace="com.example.ItemMapper">
<select id="selectItemsByOrderId" resultType="Item">
SELECT * FROM items WHERE order_id = #{id}
</select>
</mapper>
- 定义实体类和关联对象:
// Order.java
public class Order {
private int id;
private List<Item> items;
// getters and setters
}
// Item.java
public class Item {
private int id;
private String name;
// getters and setters
}
- 编写 Mapper 接口和 SQL 语句:
// OrderMapper.java
public interface OrderMapper {
Order selectOrderById(int id);
}
// ItemMapper.java
public interface ItemMapper {
List<Item> selectItemsByOrderId(int orderId);
}
- 最后,在代码中调用查询方法并使用延迟加载:
// 在代码中调用 OrderMapper 中定义的查询方法
Order order = orderMapper.selectOrderById(123);
// 此时订单对象中的商品列表 items 并未立即加载
// 当访问订单对象中的商品列表时,会触发实际的延迟加载查询操作
List<Item> items = order.getItems();
通过以上完整的示例,你可以按照这个流程配置和使用 MyBatis 的延迟加载功能。记得在全局配置文件中设置lazyLoadingEnabled
为true
,这样就可以确保延迟加载功能生效。
12、Mybatis可以实现动态SQL么?
动态SQL是指根据不同条件生成不同SQL语句,以提高代码的复用性和灵活性。MyBatis提供了一些标签来支持动态SQL的生成,常见的标签包括:
-
<if>
标签:用于根据条件生成SQL语句的一部分。<select id="getUser" parameterType="map" resultType="User"> SELECT * FROM user <where> <if test="name != null and name != ''"> AND name = #{name} </if> <if test="age != null"> AND age = #{age} </if> </where> </select>
-
<choose>
,<when>
,<otherwise>
标签:用于根据不同条件选择不同的SQL语句块。<select id="getUsers" resultType="User"> SELECT * FROM user <where> <choose> <when test="name != null and name != ''"> AND name like #{name} </when> <when test="age != null"> AND age = #{age} </when> <otherwise> AND sex = 'M' </otherwise> </choose> </where> </select>
-
<foreach>
标签:用于遍历集合并生成多个SQL语句块。<update id="updateUsers" parameterType="List"> UPDATE user <set> <foreach collection="list" item="user" separator=","> username=#{user.username}, password=#{user.password} </foreach> </set> WHERE id in <foreach collection="list" item="user" open="(" separator="," close=")"> #{user.id} </foreach> </update>
除了以上标签外,MyBatis还提供了其他标签来支持动态SQL的生成,开发者可以根据实际需求选择和使用。
13、使用MyBatis如何实现分页?
MyBatis中可以通过两种方式来实现分页:基于物理分页和基于逻辑分页。
所谓物理分页,指的是最终执行的SQL中进行分页,即SQL语句中带limit,这样SQL语句执行之后返回的内容就是分页后的结果。
好的,以下是使用MyBatis实现两种分页的示例代码:
- 基于物理分页
<!-- mapper.xml -->
<select id="getUsers" resultType="User">
SELECT * FROM user
WHERE name like #{name}
LIMIT #{start}, #{pageSize}
</select>
// Java代码
public List<User> getUsers(String name, int start, int pageSize) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
return userMapper.getUsers(name, start, pageSize);
}
}
- 基于逻辑分页
<!-- mapper.xml -->
<select id="getUsers" resultType="User">
SELECT * FROM user
WHERE name like #{name}
</select>
// Java代码
public List<User> getUsers(String name, int pageNum, int pageSize) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
int start = (pageNum - 1) * pageSize;
RowBounds rowBounds = new RowBounds(start, pageSize);
return userMapper.getUsers(name, rowBounds);
}
}
其中,第二种方式也可以结合MyBatis-Plus中的分页插件来实现,具体用法可以参考MyBatis-Plus官方文档。
MyBatis中,想要实现分页通常有四种做法:
-
在SQL中添加limit语句:
通过在SQL语句中手动添加limit关键字来实现分页。例如:SELECT * FROM user LIMIT #{start}, #{pageSize}
。 -
基于PageHelper分页插件:
使用PageHelper分页插件可以简化分页操作。只需在查询语句前调用PageHelper.startPage()
方法,然后进行查询操作,PageHelper会自动封装分页信息到PageInfo对象中。示例代码:PageHelper.startPage(1, 10); List<User> userList = userMapper.getUsers(); PageInfo<User> pageInfo = new PageInfo<>(userList);
-
基于RowBounds实现分页:
RowBounds是MyBatis提供的一个分页查询工具,通过设置offset和limit参数来实现分页。示例代码:int offset = 10; // 偏移量 int limit = 5; // 每页数据条数 RowBounds rowBounds = new RowBounds(offset, limit); List<User> userList = sqlSession.selectList("getUsers", null, rowBounds);
-
基于MyBatis-Plus实现分页:
MyBatis-Plus提供了分页插件,可实现简单易用的分页功能,无需手动编写分页SQL语句。通过传入Page对象和其他条件参数,自动计算出分页信息并返回分页结果。示例代码:public interface UserMapper extends BaseMapper<User> { List<User> selectUserPage(Page<User> page, @Param("name") String name); }
需要注意的是,以上四种做法中,基于RowBounds和MyBatis-Plus可以实现逻辑分页,而手动添加limit、PageHelper和MyBatis-Plus都可以实现物理分页。选择使用哪种分页方式取决于数据量大小和个人偏好,对于较大数据量的情况,物理分页更为推荐,以避免查询慢或内存溢出的问题。
物理分页和逻辑分页,工作中推荐使用那种分页呢?
数据小的话无所谓,逻辑分页更简单点,数据量大的话,一定是物理分页,避免查询慢,也避免内存被撑爆、
14、RowBounds分页的原理是什么?
在使用MyBatis的RowBounds进行逻辑分页时,SQL语句不需要指定分页参数,只需正常编写查询语句即可。然后,在查询时,将RowBounds对象作为参数传递给MyBatis的selectList方法。
以下是一个示例:
<select id="getUsers" resultType="User">
SELECT * FROM user
<where>
<if test="name != null">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
</where>
ORDER BY id
</select>
在查询时,将RowBounds对象作为第三个参数传递:
int offset = 10; // 偏移量
int limit = 5; // 每页数据条数
RowBounds rowBounds = new RowBounds(offset, limit);
List<User> userList = sqlSession.selectList("getUsers", null, rowBounds);
实际上,在查询时,会先将所有符合条件的记录返回,然后根据RowBounds中指定的offset和limit进行数据保留,即抛弃掉不需要的数据再返回给应用程序。
需要注意的是,使用RowBounds进行逻辑分页时,数据库仍然会返回所有符合条件的记录,因此对于大量数据的情况下,可能会导致性能问题。在这种情况下,推荐使用物理分页方式,如使用limit语句、PageHelper插件或MyBatis-Plus分页插件。
15、PageHelper分页的原理是什么?
PageHelper是MyBatis中提供的物理分页插件,使用时需要先引入相关依赖,在代码中使用PageHelper.startPage(int pageNum, int pageSize)方法设置分页参数。
以下是一个示例:
<!-- 引入PageHelper依赖 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.2.0</version>
</dependency>
在代码中使用PageHelper.startPage方法设置分页参数:
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
// ...
int pageNum = 1; // 页码
int pageSize = 10; // 每页数据条数
PageHelper.startPage(pageNum, pageSize);
List<User> userList = userMapper.getUserList();
PageHelper会在执行器的query方法执行之前,从ThreadLocal中获取分页参数信息,然后执行分页算法计算起始位置和大小。最后,PageHelper会通过修改SQL语句的方式,在SQL后面动态拼接上limit语句,限定查询的数据范围。
在以上示例中,PageHelper会自动在getUserList方法执行前执行分页算法,并将SQL语句修改为类似如下形式:
SELECT * FROM user LIMIT 0, 10
最后,PageHelper会清除ThreadLocal中的分页参数,确保不会对后续的查询造成影响。
需要注意的是,PageHelper只能用于物理分页,即通过修改SQL语句来实现分页的方式。如果需要使用逻辑分页,可以使用MyBatis的RowBounds或者其他分页插件。
本文参考自二哥的Java进阶之路、三分恶 作者老三、Hollis Java 8Gux、ChatGPT。1
https://www.yuque.com/hollis666/axzrte/gxi0rc
https://javabetter.cn/sidebar/sanfene/collection.html ↩︎