Mybatis应用实践和分析
起源
从原生JDK Jdbc
开始,帮我们解决了数据库连接、SQL参数编译问题,但是仍有很多痛点:
1、重复代码
2、资源管理(创建和释放)
3、结果集处理(每个POJO都需要手动映射)
4、SQL耦合
JDK JDBC
Connection conn = null;
Statement stmt = null;
// 注册 JDBC 驱动
Class.forName("com.mysql.jdbc.Driver");
// 打开连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/gp-mybatis", "root", "123456");
// 执行查询
stmt = conn.createStatement();
String sql = "SELECT bid, name, author_id FROM blog where bid = 1";
ResultSet rs = stmt.executeQuery(sql);
// 获取结果集
Blog blog = new Blog();
while (rs.next()) {
Integer bid = rs.getInt("bid");
String name = rs.getString("name");
Integer authorId = rs.getInt("author_id");
blog.setAuthorId(authorId);
blog.setBid(bid);
blog.setName(name);
}
rs.close();
stmt.close();
conn.close();
总结
缺点:
1、重复代码
2、资源管理
3、结果集处理
4、SQL耦合
Spring JDBC
Spring提供了一个模板方法JdbcTemplate,封装了各种的execute、query、update等方法。简化了JDBC的使用,可以避免常见的异常,封装了JDBC核心流程,开发者只需要提供SQL、提取结果集即可。是线程安全的。
JdbcTemplate
public class JdbcTemplate extends JdbcAccessor implements JdbcOperations {
public JdbcTemplate(DataSource dataSource) {
this.setDataSource(dataSource);
this.afterPropertiesSet();
}
...
}
public interface JdbcOperations {
@Nullable
<T> T execute(ConnectionCallback<T> var1) throws DataAccessException;
@Nullable
<T> T execute(StatementCallback<T> var1) throws DataAccessException;
void execute(String var1) throws DataAccessException;
<T> List<T> query(String var1, RowMapper<T> var2) throws DataAccessException;
@Nullable
<T> T queryForObject(String var1, RowMapper<T> var2) throws DataAccessException;
...
}
另外也提供了结果集转换的接口,开发者只需要实现接口方法mapRow,把结果集转换为对象。
RowMapper
@FunctionalInterface
public interface RowMapper<T> {
@Nullable
T mapRow(ResultSet var1, int var2) throws SQLException;
}
例子
public class EmployeeRowMapper implements RowMapper {
@Override
public Object mapRow(ResultSet resultSet, int i) throws SQLException {
Employee employee = new Employee();
employee.setEmpId(resultSet.getInt("emp_id"));
employee.setEmpName(resultSet.getString("emp_name"));
employee.setGender(resultSet.getString("gender"));
employee.setEmail(resultSet.getString("email"));
return employee;
}
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class EmployeeRowMapperTest {
@Autowired
JdbcTemplate jdbcTemplate;
List<Employee> list;
@Test
public void EmployeeTest() {
list = jdbcTemplate.query("select * from tbl_emp", new EmployeeRowMapper());
System.out.println(list);
}
}
总结
优点:
1、无需关心资源管理
2、
缺点:
1、SQL耦合
2、当项目的表慢慢越多的时候,每张表转为POJO都需要定义一个RowMapper,导致类文件数量膨胀
思考
Spring JDBC 对 JDBC 做了轻量级封装的框架,帮我们解决的问题:
1、对数据的增删改查方法进行了封装
2、不用手动创建关闭资源
3、可以帮助我们映射结果集
不足:
1、SQL语句写死在代码里
2、参数只能按固定的顺序传入(数组),是通过占位符替换的。不能传入对象和Map
3、在方法里可以把结果集映射成实体类,但是不能直接把实体类映射成数据库的记录
4、查询没有缓存的功能,性能还不够好
由此引出ORM框架
到了Spring时代,整合了JDBC,对JDBC做了进一步封装。核心类 JdbcTemplate
处理了资源的管理以及SQL的执行封装,RowMapper
提供了结果集自定义映射的方法。帮助我们解决了很大的问题,但仍存在一些不足:
1、SQL语句写死在代码里
2、参数只能按固定的顺序传入(数组),是通过占位符替换的。不能传入对象和Map
3、在方法里可以把结果集映射成实体类,但是不能直接把实体类映射成数据库的记录
4、查询没有缓存的功能,性能还不够好
ORM框架
Object-对象 Mapping-映射 Relational-关系
Mybatis
“半自动化”的ORM框架,是对于Hibernate的全自动化来说的。它的封装程序没有Hibernate那么高,不会自动生成全部的SQL语句,主要是解决SQL和对象的映射问题
前身
前身是 ibatis,2001年开始开发,是 “internet” 和 “abatis” 两个单词的组合。04年捐赠给Apache,2010年更名为Mybatis
https://mybatis.org/mybatis-3/zh/
编程式开发
引包
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.4</version>
</dependency>
全局配置
<?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 resource="db.properties"></properties>
<settings>
<!-- 打印查询语句 -->
<setting name="logImpl" value="STDOUT_LOGGING" />
<!-- 控制全局缓存(二级缓存),默认 true-->
<setting name="cacheEnabled" value="true"/>
<!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过select标签的 fetchType来覆盖-->
<setting name="aggressiveLazyLoading" value="true"/>
<!-- Mybatis 创建具有延迟加载能力的对象所用到的代理工具,默认JAVASSIST -->
<!--<setting name="proxyFactory" value="CGLIB" />-->
<!-- STATEMENT级别的缓存,使一级缓存,只针对当前执行的这一statement有效 -->
<!--
<setting name="localCacheScope" value="STATEMENT"/>
-->
<setting name="localCacheScope" value="SESSION"/>
</settings>
<typeAliases>
<typeAlias alias="blog" type="com.gupaoedu.domain.Blog" />
</typeAliases>
<!-- <typeHandlers>
<typeHandler handler="com.gupaoedu.type.MyTypeHandler"></typeHandler>
</typeHandlers>-->
<!-- 对象工厂 -->
<!-- <objectFactory type="com.gupaoedu.objectfactory.GPObjectFactory">
<property name="gupao" value="666"/>
</objectFactory>-->
<!-- <plugins>
<plugin interceptor="com.gupaoedu.interceptor.SQLInterceptor">
<property name="gupao" value="betterme" />
</plugin>
<plugin interceptor="com.gupaoedu.interceptor.MyPageInterceptor">
</plugin>
</plugins>-->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/><!-- 单独使用时配置成MANAGED没有事务 -->
<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="BlogMapper.xml"/>
<mapper resource="BlogMapperExt.xml"/>
</mappers>
</configuration>
Mapper.xml
测试
private SqlSessionFactory sqlSessionFactory;
@Before
public void prepare() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}
/**
* 使用 MyBatis API方式
* @throws IOException
*/
@Test
public void testStatement() throws IOException {
SqlSession session = sqlSessionFactory.openSession();
try {
Blog blog = (Blog) session.selectOne("com.gupaoedu.mapper.BlogMapper.selectBlogById", 1);
System.out.println(blog);
} finally {
session.close();
}
}
/**
* 通过 SqlSession.getMapper(XXXMapper.class) 接口方式
* @throws IOException
*/
@Test
public void testSelect() throws IOException {
SqlSession session = sqlSessionFactory.openSession(); // ExecutorType.BATCH
try {
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlogById(1);
System.out.println(blog);
} finally {
session.close();
}
}
核心特性
- 使用连接池对连接进行管理
- SQL 和代码分离,集中管理
- 重复SQL提取
- 参数映射和动态SQL
- 结果集映射
- 缓存管理
- 插件机制
核心对象的生命周期
SqlSessionFactoryBuilder
方法局部
它是用来构建SqlsessionFactory的,而SqlsessionFactory只需要一个,所以构建完成后它的使命就完成了。
所以它的生命周期只存在于方法的局部。
主要解析全局配置和Mapper文件。
SqlSessionFactory
应用级别
它是用来创建Sqlsesion会话的,每次应用访问数据库,都需要创建一个会话。SqlSessionFactory是存在于应用的整个生命周期。创建Sqlsession只需要一个实例来完成,否则就会产生很多的混乱和资源浪费。需要采用单例模式。
SqlSession
一次请求或一次操作
SqlSession是一个会话,它不是线程安全的,不能在线程间共享。所以我们在请求开始的时候创建一个SqlSession对象,在请求结束或方法执行完毕时要及时关闭,
Mapper
方法
实际是一个代理对象,是从Sqlsession中获取的。它的作用是发送SQL来操作数据库的数据,它应该在一个Sqlsession事务方法之内。
核心配置解读
<configuration>
<properties resource="db.properties"></properties>
<settings>
<!-- 打印查询语句 -->
<setting name="logImpl" value="STDOUT_LOGGING" />
<!-- 控制全局缓存(二级缓存),默认 true-->
<setting name="cacheEnabled" value="true"/>
<!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过select标签的 fetchType来覆盖-->
<setting name="aggressiveLazyLoading" value="true"/>
<!-- Mybatis 创建具有延迟加载能力的对象所用到的代理工具,默认JAVASSIST -->
<!--<setting name="proxyFactory" value="CGLIB" />-->
<!-- STATEMENT级别的缓存,使一级缓存,只针对当前执行的这一statement有效 -->
<!--
<setting name="localCacheScope" value="STATEMENT"/>
-->
<setting name="localCacheScope" value="SESSION"/>
</settings>
<typeAliases>
<typeAlias alias="blog" type="com.gupaoedu.domain.Blog" />
</typeAliases>
<!-- <typeHandlers>
<typeHandler handler="com.gupaoedu.type.MyTypeHandler"></typeHandler>
</typeHandlers>-->
<!-- 对象工厂 -->
<!-- <objectFactory type="com.gupaoedu.objectfactory.GPObjectFactory">
<property name="gupao" value="666"/>
</objectFactory>-->
<!-- <plugins>
<plugin interceptor="com.gupaoedu.interceptor.SQLInterceptor">
<property name="gupao" value="betterme" />
</plugin>
<plugin interceptor="com.gupaoedu.interceptor.MyPageInterceptor">
</plugin>
</plugins>-->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/><!-- 单独使用时配置成MANAGED没有事务 -->
<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="BlogMapper.xml"/>
</mappers>
</configuration>
configuration
properties
配置参数信息,常见的如数据库连接信息。为了避免写死一些参数在xml文件中,可以放到properties,需要的时候引入进来, 在xml配置文件中可以用${}引用
settings
settings中是Mybatis的一些核心配置。比如 一级缓存、二级缓存、延迟加载等
typeAliases
可以配置返回类型的别名,和Linux里的alias一样,主要是简化全路径的拼写。
<typeAliases>
<typeAlias alias="blog" type="com.gupaoedu.domain.Blog" />
</typeAliases>
<select id="selectBlogByBean" parameterType="blog" resultType="blog" >
select bid, name, author_id authorId from blog where name = '${name}'
</select>
typeHandlers
解决Java类型和数据库的JDBC类型不是一一对应的(比如String和varchar、char、text),所以把Java对象转为数据库的值、数据库的值转为Java对象,需要经过一层转换。
在Mybatis已经内置了很多TypeHandler(type包下),全部都注册在TypeHandlerRegistry
objectFactory
当把数据库返回的结果集转为实体类的时候,需要创建对象的实例,通过设置返回的class类型反射创建。
plugins
这是Mybatis一个很强大的机制,和其它框架一样,预留了插件的接口,让Mybatis更容易扩展。
可拦截的4个对象类型:
1、Executor(query、update、commit、rollback、close)
2、ParameterHandler(getParameterObject、setParameters)
3、ResultSetHandler(handleResultSets、handleOutputParameters)
4、StatementHandler(prepare、parameterize、batch、update、query)
environments
environment
transactionManager
dataSource
Mybatis自带了2种数据源,POOLED和UNPOOLED。也可以配置成其它数据库,比如C3P0\HiKari等。
为什么要是用连接池?
除了连接池,像比如线程池、内存池、对象池,这些池化技术达到的目的基本一样:
1、减少连接的创建和关闭(复用连接)
如果没有池化技术,每次连接都是一个物理连接,创建和关闭都会消耗应用和服务器的性能;
如果使用连接池,在应用关闭连接时,物理连接并没有真正关闭连接,只是回到了连接池中。
那么从这个角度考虑,一般连接池都会有初始化连接数、最大连接数、回收时间等等这些配置参数。提供【提前创建、资源重用、数量控制、超时管理】等等这些功能
mapper
配置的是映射器,也就是Mapper.xml 路径。这是配置目的是让Mybatis在启动的时候扫描这些映射器,创建映射关系。
有4种指定Mapper文件的方式:
1、使用相对于类路径的资源引用(resource)
<mappers>
<mapper resource="BlogMapper.xml"/>
</mappers>
2、使用完全限定资源定位符(绝对路径,URL)
<mappers>
<mapper resource="file:///D//work//BlogMapper.xml"/>
</mappers>
3、使用映射器接口类的完全限定类名
<mappers>
<mapper resource="com.gupaoedu.mapper.BlogMapper"/>
</mappers>
4、将包内的映射器接口实现全部注册为映射器(最常见)
<mappers>
<mapper resource="com.gupaoedu.mapper/>
</mappers>
动态SQL
if
choose(when otherwise)
trim(set)
foreach
where
批量操作
循环单次发送
如果是几万条的执行,意味着需要创建几万次会话连接。即使在同一个会话中,也有重复编译和执行发送SQL的开销。
批量发送
foreach标签
批量新增
insert into blog (bid, name, author_id)
values
<foreach collection="list" item="blog" index="index" separator=",">
(#{blog.bid}, #{blog.name}, #{blog.authorId})
</foreach>
批量更新
update blog
set
name = case bid
when ? then ?
when ? then ?
end,
author_id = case bid
when ? then ?
when ? then ?
end
where bid in (?, ?)
update blog
set
name = case bid
<foreach collection="list" item="blog" index="index" separator=" " >
when #{blog.bid} then #{blog.name}
</foreach>
end
where bid in
<foreach collection="list" item="blog" index="index" separator="," open="(" close=")">
#{blog.bid}
</foreach>
优点:可以减少和数据库的交互次数,并且避免了开启和结束事务的时间消耗
缺点:当数据量特别大的时候,拼接出来的SQL语句过大。MYSQL的服务端对接收的数据包有大小限制,
max_allowed_packet
默认是4M,可以手动修改
Batch Executor
-
SimpleExecutor
每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。
-
ReuseExecutor
执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建。用完后,不关闭Statement对象,而是放置于Map中。简而言之,就是重复使用Statement对象
-
BatchExecutor
执行update(JDBC批处理不支持select),将所有sql都添加到批量处理中(addBatch),等统一处理。它缓存了多个 Statement对象,每个Statement对象都是addBatch完毕后,等逐一执行executeBatch批处理。
和JDBC批处理相同。
executeBatch是一批语句访问一次数据库(具体发送多少·和服务端设置的max_allowed_packet
有关)。
BatchExecutor底层是对JDBC ps.addBatch() 和 ps.executeBatch 的封装
关联查询与延迟加载
映射结果有2个标签,resultType 和 resultMap
- resultType
是select标签的一个返回属性,适用于JDK类型(Integer、String等)和实体类,这种情况结果集的列和实体类 的属性可以直接映射。如果返回的字段无法映射,可以resultMap来建立映射关系。
而对于关联查询,多对一、1对多等情况,就需要用到resultMap中的 association
、collection
association
用于一对一、多对一,而collection
用于一对多的关系
嵌套查询
会带来N + 1问题,主查询SQL是1,SQL结果是多条,又会执行N条SQL语句
<!-- 另一种联合查询(一对一)的实现,但是这种方式有“N+1”的问题 -->
<resultMap id="BlogWithAuthorQueryMap" type="com.gupaoedu.domain.associate.BlogAndAuthor">
<id column="bid" property="bid" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<association property="author" javaType="com.gupaoedu.domain.Author"
column="author_id" select="selectAuthor"/> <!-- selectAuthor 定义在下面-->
</resultMap>
如何解决呢?
使用延迟加载,只要当对象get该属性时,才会二次查询。(原理是动态代理实现)
<!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过select标签的 fetchType来覆盖-->
<setting name="aggressiveLazyLoading" value="true"/>
翻页
逻辑翻页
Mybatis自带的逻辑翻页对象 RowBounds,主要有2个属性:offset、limit(从第几条开始,查询多少条)。
工作原理就是对 ResultSet 的内存处理。
List<Blog> selectBlogList(RowBounds rowBounds);
物理翻页
物理翻页才是真正的翻页,通过数据库支持的语法来翻页查询。
PageHelper
常见问题
1、是用注解还是用xml配置?
注解的缺点就是SQL无法集中管理,复杂的SQL比较难配置。所以建议在业务复杂的项目中使用XML配置的形式,简单项目中使用注解和XML混用的形式。
2、Mapper接口注入 Invalid bound statement (not found) 问题
Mapper接口无法注入,或者mapper statementid 跟 Mapper接口无法绑定的情况,基于绑定的要求规范,可以从这些地方检查:
- 扫描配置,xml文件和Mapper接口没有扫描到
- namespace值是否和接口一致
- 检查对应的sql语句ID是否存在(接口上的方法)
3、怎么获取插入的最新自动生成的ID?
4、什么时候用#{},什么时候用${}
5、XML中怎么使用特殊符号,比如小于 &
6、如何实现模糊查询 LIKE