在Java程序里面去操作数据库,最原始的办法是使用JDBC的API。需要分为六步:
- 注册驱动
- 通过DriverManager获取一个Connection
- 通过Connection创建一个Statement对象。
- 通过Statement的execute()方法执行SQL,返回结果集ResultSet
- 将ResultSet结果集转换成POJO对象。
- 关闭各种资源
像Connection获取,结果集的封装,资源关闭这些代码在开发的过程中会出现大量冗余,所以出现了很多对JDBC进行封装的框架来简化操作。
Apache DbUtils:
DbUtils 解决的最核心的问题就是结果集的映射,提供了一系列的支持泛型的ResultSetHandler,帮助我们把 ResultSet 封装成JavaBean。
DbUtils提供了一个QueryRunner类,它对数据库的增删改查的方法进行了封装, 在 QueryRunner 的构造函数里面,我们可以传入一个数据源DataSource,让框架帮我们管理连接。
queryRunner = new QueryRunner(dataSource);
String sql = "select * from blog";
List<Blog> list = queryRunner.query(sql, new BeanListHandler<>(Blog.class));
注意:DbUtils要求数据库的字段跟对象的属性名称完全一致,才可以实现自动映射。
Spring JDBC
Spring也对原生的JDBC进行了封装,并且给我们提供了一个模板方法JdbcTemplate,来简化我们对数据库的操作。
和dbutils类似,spring帮助我们管理datasource和connection,并且提供了RowMapper接口,我们只要实现RowMapper接口,并且重写mapRow()方法,就可以将结果集转换成Java对象。
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.setEmail(resultSet.getString("email"));
return employee;
}
}
jdbcTemplate = new JdbcTemplate( new DruidDataSource());
list = jdbcTemplate.query(" select * from emp_table", new EmployeeRowMapper());
上面两种都是对JDBC笔记简单的封装,虽然简化了操作,但是缺少一些扩展功能,比方说SQL语句还是需要在代码里硬编码,不便于修改,无法生成动态SQL,没有缓存等
这就有了功能更加丰富的ORM框架(Object Relational Mapping),帮助我们解决程序对象和关系型数据库的相互映射的问题。
MyBatis使用
MyBatis 就是一个“半自动化”的ORM框架(一般称Hibernate为全自动化),它的封装程度没有Hibernate那么高,不会自动生成全部的SQL语句,但是相对性能也更好一些 (框架封装的越多,说明内部处理越多,相对来说性能越差,原生的JDBC API的性能反而是最好的)
先看看在单独使用Mybatis的时候我们是怎么配置的
- 引入Mybatis jar包
- 创建一个全局配置文件mybatis-config.xml
- 创建映射器文件,Mapper.xml,通常来说一张表对应一个Mapper文件
使用demo
public void Test() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
try {
//执行SQL有两种方法:
//1.通过SqlSession接口上的方法,传入Statement ID来执行SQL
//2.定义一个Mapper接口。这个接口全路径必须跟Mapper.xml里面的namespace对应起来,方法也要跟Statement ID一一对应
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlogById(1);
System.out.println(blog);
} finally {
session.close();
}
}
从上面的代码可以看出MyBatis的几大核心对象:SqlSessionFactoryBuiler、SqlSessionFactory、SqlSession 和Mapper对象
核心对象
1)SqlSessionFactoryBuilder
用来构建 SqlSessionFactory 的(建造者模式),SqlSessionFactory作为会话工厂只需要一个就够了,所以只要创建了SqlSessionFactory,Builder就可以销毁了 所以它的生命周期只存在于方法的局部。
2)SqlSessionFactory
SqlSessionFactory 是用来创建SqlSession的,每次应用程序访问数据库,都需要创建一个会话。所以 SqlSessionFactory 应该存在于应用的整个生命周期中(作用域是应用作用域),并且是单例的。
3)SqlSession
SqlSession是一个会话,因为它不是线程安全的,所以不能在线程间共享。每次请求开始的时候都需要创建一个SqlSession对象,在请求结束的时候要及时关闭它 作用域:一次请求或者交互中
4)Mapper
Mapper(实际上是一个代理对象 后面会分析)的作用是发送SQL来操作数据库,是从SqlSession中获取的,所以它的作用域应该和SqlSession相关,在一个 SqlSession事务方法之内才有效
对象 | 生命周期 |
SqlSessionFactoryBuiler | 方法局部(method) |
SqlSessionFactory(单例) | 应用级别(application) |
SqlSession | 请求和操作(request/method) |
Mapper | 方法(method) |
核心配置解读
<?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" />
<!-- 控制全局缓存(二级缓存)-->
<setting name="cacheEnabled" value="true"/>
<!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过select标签的 fetchType来覆盖-->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- Mybatis 创建具有延迟加载能力的对象所用到的代理工具,默认JAVASSIST -->
<!--<setting name="proxyFactory" value="CGLIB" />-->
<setting name="localCacheScope" value="SESSION"/>
</settings>
<typeAliases>
<typeAlias alias="blog" type="com.chenpp.domain.Blog" />
</typeAliases>
<typeHandlers>
<typeHandler handler="com.chenpp.type.MyTypeHandler"></typeHandler>
</typeHandlers>
<!-- 对象工厂 -->
<!-- <objectFactory type="com.chenpp.objectfactory.CPObjectFactory">
<property name="name" value="cpp"/>
</objectFactory>-->
<!-- <plugins>
<plugin interceptor="com.chenpp.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
configuration 是整个配置文件的根标签,对应 MyBatis 里面最重要的配置类 Configuration(单例)
properties
用来配置参数信息,比如最常见的数据库连接信息。
为了避免直接把参数写死在 xml 配置文件中,我们可以把这些参数单独放在properties 文件中,用 properties 标签引入进来,然后在xml 配置文件中用${}引用就可以了。
settings
setttings里面是MyBatis的一些核心配置,各种参数的配置
https://mybatis.org/mybatis-3/zh/configuration.html#settings
属性名 | 描述 | 有效值 | 默认值 |
cacheEnabled | 全局地开启或关闭配置文件中的所有映射器已经配置的任何缓存。 | true | false | true |
lazyLoadingEnabled | 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态。 | true | false | false |
aggressiveLazyLoading | 当开启时,任何方法的调用都会加载该对象的所有属性。 否则,每个属性会按需加载(参考 lazyLoadTriggerMethods)。 | true | false | false |
defaultExecutorType | 配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(prepared statements); BATCH 执行器将重用语句并执行批量更新。 | SIMPLE REUSE BATCH | SIMPLE |
lazyLoadTriggerMeth ods |
指定哪个对象的方法触发一次延迟加载。 | 用逗号分隔的方法列表。 | equals,clone hashCode,toString |
localCacheScope | MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。 默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据 | SESSION | STATEMENT | SESSION |
logImpl | 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。 | SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING | 未设置 |
typeAliases
类型别名,用来简化全路径类名的拼写。可以指定单个类,也可以指定一个package,自动转换。
MyBatis里面有系统预先定义好的类型别名,在TypeAliasRegistry中。
typeHandlers
用于把Java对象转换为数据库的值,或者把数据库的值转换成 Java 对象;比放说(String和varchar)
对于一些基础类型,MyBatis里内置了一些基本的TypeHandler, 都注册在TypeHandlerRegistry,他们都继承了抽象类BaseTypeHandler,我们也可以实现自己的TypeHandler
需要实现以下四个抽象方法
使用的时候,需要先在mybatis-config.xml里注册对应的TypeHandler
<!-- mybatis-config.xml -->
<typeHandlers>
<typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>
然后在我们需要使用的字段上指定对应的typeHandler就可以了
#插入
<insert id="insertBlog" parameterType="blog">
insert into blog (bid, name, author_id)
values (#{bid,jdbcType=INTEGER}, #{name,jdbcType=VARCHAR,typeHandler=com.chenpp.type.MyTypeHandler}, #{authorId,jdbcType=CHAR})
</insert>
#查询结果映射
<resultMap id="BaseResultMap" type="blog">
<id column="bid" property="bid" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR" typeHandler="com.chenpp.type.MyTypeHandler"/>
<result column="author_id" property="authorId" jdbcType="INTEGER"/>
</resultMap>
objectFactory
ObjectFactory用于创建实体类的实例,当我们把数据库返回的结果集转换为实体类的时候,就会使用ObjectFactory通过反射来创建对象,里面定义了4个方法
public interface ObjectFactory {
void setProperties(Properties var1);
<T> T create(Class<T> var1);
<T> T create(Class<T> var1, List<Class<?>> var2, List<Object> var3);
<T> boolean isCollection(Class<T> var1);
}
ObjectFactory 有一个默认的实现类DefaultObjectFactory,创建对象的方法最终都调用了instantiateClass(),是通过反射来完成实例化的
如果想要修改对象工厂在映射实体类时的行为,就可以创建自己的对象工厂,只需要继承DefaultObjectFactory然后重写create()方法就可以了
plugins
插件是MyBatis的一个很强大的机制,跟很多其他的框架一样,MyBatis预留了插件的接口,让MyBatis更容易扩展。这个会在后面详细分析
environments、environment
environments标签用来管理数据库的环境,比如我们可以有开发环境、测试环境、生产环境等不同环境的数据库。可以在不同的环境中使用不同的数据库。这里面有两个关键的标签,一个是事务管理器,一个是数据源
transactionManager
如果配置的是JDBC,就会直接使用 JDBC 的提交和回滚设置,它依赖于从数据源得到的连接来管理事务作用域
如果配置成MANAGED,会把事务交给容器来管理,比如JBOSS,Weblogic等。如果直接在本地环境运行程序,配置成MANAGE的话不会有任何事务。
dataSource
dataSource 元素使用标准的 JDBC 数据源接口来配置 JDBC 连接对象的资源。有三种内建的数据源类型(也就是UNPOOLED,POOLED,JNDI)
UNPOOLED– 这个数据源的实现只是每次请求时打开和关闭连接。
POOLED–使用来连接池管理 JDBC 连接对象,复用连接
JNDI – 这个数据源的实现是为了能在如 EJB 或应用服务器这类容器中使用
在跟Spring集成的时候,事务和数据源都会交给Spring来管理。
mappers
<mappers>标签配置的是我们的映射器,也就是Mapper.xml的路径。这里配置的目的是让MyBatis在启动的时候去扫描这些映射器,创建映射关系。我们有四种指定Mapper文件的方式:
- 使用相对于类路径的资源引用(resource)
- 使用完全限定资源定位符(URL)
- 使用映射器接口实现类的完全限定类名
- 将包内的映射器接口实现全部注册为映射器(最常用)
Mapper映射配置文件
一共有8个主要标签
cache – 给定命名空间的缓存配置(是否开启二级缓存)。
cache-ref – 其他命名空间缓存配置的引用。
resultMap – 用来描述如何从数据库结果集中来加载对象 数据库结果集和java对象的映射关系
sql – 可被其他语句引用的可重用语句块。
增删改查标签:
insert ,update ,delete ,select
MyBatis的扩展使用
动态SQL
基于OGNL表达式,帮助我们更方便的拼接SQL
MyBatis的动态标签主要有四类: if, choose(when,otherwise),trim (where, set),foreach。
if —— 用于做判断,条件写在test中
choose (when, otherwise) —— 用于做条件选择
<select id="findActiveBlogLike" resultType="Blog"> SELECT * FROM BLOG WHERE state = ‘ACTIVE’ <choose> <when test="title != null"> AND title like #{title} </when> <when test="author != null and author.name != null"> AND author_name like #{author.name} </when> <otherwise> AND featured = 1 </otherwise> </choose> </select>
trim, where, set——会帮助我们去掉多余的where,and,逗号之类的符号,使用trim还可以指定获取去掉前缀/后缀
where 元素只会在至少有一个子元素的条件返回 SQL 子句的情况下才去插入“WHERE”子句。而且,若语句的开头为“AND”或“OR”,where 元素也会将它们去除。
如果 where 元素没有按正常套路出牌,我们可以通过自定义 trim 元素来定制 where 元素的功能。比如,和 where 元素等价的自定义 trim 元素为:<trim prefix="WHERE" prefixOverrides="AND |OR "> ... </trim>
foreach —— 适用于需要遍历集合的时候
<select id="selectPostIn" resultType="domain.blog.Post"> SELECT * FROM POST P WHERE ID in <foreach item="item" index="index" collection="list" open="(" separator="," close=")"> #{item} </foreach> </select>
批量SQL
使用for each拼接长SQL
缺点:MySQL 的服务端对于接收的数据包有大小限制,max_allowed_packet 默认是4M,需要修改对应的参数配置
使用BatchExecutor进行批操作
在我们的全局配置文件中,可以配置默认的 Executor 的类型。其中有一种BatchExecutor
<setting name="defaultExecutorType" value="BATCH"/>
也可以在创建会话的时候指定执行器类型
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
BatchExecutor底层是就是调用了JDBC的ps.addBatch()方法(在BatchExecutor调用了StatementHandler的batch(),在里面调用了statement的addBatch()方法)
嵌套(关联)查询/ N+1 / 延迟加载
对于一对一的关联查询有两种配置方式
嵌套结果:
<!-- 根据文章查询作者,一对一查询的结果,嵌套结果 -->
<resultMap id="BlogWithAuthorResultMap" type="com.chenpp.domain.associate.BlogAndAuthor">
<id column="bid" property="bid" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<!-- 联合查询,将author的属性映射到ResultMap -->
<association property="author" javaType="com.chenpp.domain.Author">
<id column="author_id" property="authorId"/>
<result column="author_name" property="authorName"/>
</association>
</resultMap>
<!-- 根据文章查询作者,一对一,嵌套结果,无N+1问题 -->
<select id="selectBlogWithAuthorResult" resultMap="BlogWithAuthorResultMap" >
select b.bid, b.name, b.author_id, a.author_id , a.author_name
from blog b
left join author a
on b.author_id=a.author_id
where b.bid = #{bid, jdbcType=INTEGER}
</select>
嵌套查询
<!-- 另一种联合查询(一对一)的实现,但是这种方式有“N+1”的问题 -->
<resultMap id="BlogWithAuthorQueryMap" type="com.chenpp.domain.associate.BlogAndAuthor">
<id column="bid" property="bid" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<association property="author" javaType="com.chenpp.domain.Author"
column="author_id" select="selectAuthor"/> <!-- selectAuthor 定义在下面-->
</resultMap>
<!-- 根据文章查询作者,一对一,嵌套查询,存在N+1问题,可通过开启延迟加载解决 -->
<select id="selectBlogWithAuthorQuery" resultMap="BlogWithAuthorQueryMap" >
select b.bid, b.name, b.author_id, a.author_id , a.author_name
from blog b
left join author a
on b.author_id=a.author_id
where b.bid = #{bid, jdbcType=INTEGER}
</select>
<!-- 嵌套查询 -->
<select id="selectAuthor" parameterType="int" resultType="com.chenpp.domain.Author">
select author_id authorId, author_name authorName
from author where author_id = #{authorId}
</select>
对于第二种嵌套查询,因为是分两次查询的,当我们查询了博客信息之后,会再发送一条SQL到数据库查询作者信息。如果查询出N条博客记录,那么就需要再执行N次作者的SQL查询 这就是所谓的N+1问题
在MyBatis里面可以通过开启延迟加载的开关来解决这个问题
<!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过select标签的 fetchType来覆盖-->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- Mybatis 创建具有延迟加载能力的对象所用到的代理工具,默认JAVASSIST -->
<setting name="proxyFactory" value="CGLIB" />
简单来说,lazyLoadingEnabled决定了是否延迟加载。
aggressiveLazyLoading决定了是不是对象的所有方法都会触发查询。
逻辑翻页与物理翻页
在我们查询数据库的操作中,有两种翻页方式,一种是逻辑翻页(假分页),一种是物理翻页(真分页)。逻辑翻页的原理是把所有数据查出来,在内存中删选数据。 物理翻页是真正的翻页,比如MySQL 使用limit ,Oracle使用rownum 。
逻辑翻页
MyBatis 里面有一个逻辑分页对象 RowBounds,里面主要有两个属性,offset 和limit(从第几条开始,查询多少条).
我们可以在Mapper接口的方法上加上这个参数, 而不需要修改xml里的SQL语句达到逻辑翻页的效果。
public List<Blog> selectBlogList(RowBounds rowBounds);
物理翻页
1.直接传入参数(或者包装一个page对象),在SQL语句中翻页。
<select id="selectBlogPage" parameterType="map" resultMap="BaseResultMap">
select * from blog limit #{curIndex} , #{pageSize}
</select>
2.使用翻页的插件,比如PageHelper,后面会介绍下MyBatis插件的原理和PageHelper的实现
简单地来说,就是根据 PageHelper 的参数,改写我们的 SQL语句