mybatis是一款半自动化的ORM框架 ,之所以说半自动,是因为使用mybatis时,需要手动写SQL语句,不像全自动的hibernate,只需要做好对象和数据库字段的映射关系,就可以CRUD操作。由于Mybatis支持解析SQL语句,所以相较于hibernate,灵活性大大增强。
一、Mybatis工作原理
要使用mybatis,需要定义映射对象pojo,需要定义接口mapper,还需要定义实现SQL的xml文件,那这三者之间是怎么联系到一起完成mybatis的工作的呢?下面通过手写mybatis,在实现的过程中,可以理解mybatis怎么实现SQL语句的解析(示例只实现了查询SQL的解析),也可以理解mybatis怎么将持久化对象、接口mapper、xml文件联系到一起的。
手写Mybatis,主要重新实现mybatis解析配置文件,建立数据库连接过程、由调用DAO接口方法查找xml中对应SQL方法过程、替换SQL中参数过程、查询值映射到POJO过程。
1.1 数据库准备
- PostgreSql数据库
- test_db库
- test_db库中的user表,user表建表脚本如下
create table if not exists "user"
(
id real not null
constraint user_pk
primary key,
name varchar(128),
age integer,
"createTime" timestamp default now(),
"updateTime" timestamp default now()
)
1.2 配置数据库数据源信息
resources目录下新增mybatis-config-datasource.xml,配置内容比较简单,如下:
<?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>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="org.postgresql.Driver"/>
<property name="url" value="jdbc:postgresql://10.21.70.46:5432/test_db"/>
<property name="username" value="cloudify"/>
<property name="password" value="cloudify"/>
</dataSource>
</environment>
</environments>
<!--定义扫描的xml格式SQL文件-->
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
</mappers>
</configuration>
1.2 定义接口
定义一个类似DAO一样的接口。
public interface UserDao {
User queryUserInfoById(Long id);
}
1.3 定义POJO对象
本样例比较简单,暂不支持<map>方式映射对象和表字段的对应关系,直接set/get反射方式,来查找映射关系,所以bean对象的属性名在定义时需要和表字段名一致。
public class User {
private Float id;
private String name;
private Integer age;
private Date createTime;
private Date updateTime;
public Float getId() {
return id;
}
public void setId(Float id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public Date getUpdateTime() {
return updateTime;
}
public void setUpdateTime(Date updateTime) {
this.updateTime = updateTime;
}
}
1.4 UserMapper.xml文件
如下代码,其中namespace中是接口的映射关系,下面通过dom4j解析UserMapper.xml,通过namespace命名空间+id,对应上了接口方法和具体SQL语句。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cc.dao.UserDao">
<select id="queryUserInfoById" parameterType="java.lang.Long" resultType="com.cc.pojo.User">
select * from public."user" where id = #{id}
</select>
<select id="queryUserList" resultType="com.cc.pojo.User">
select id,name,age,createTime,updateTime from user where age=#{age}
</select>
</mapper>
1.5 解析xml配置文件
解析数据源的配置文件
- 获取配置文件mybatis-config-datasource.xml的文件流,inputStream
public class Resources {
/**
* resource为文件相对于resources目录的相对路径
*
* @param resource
* @return
* @throws IOException
*/
public static Reader getResourceReader(String resource) throws IOException{
return new InputStreamReader(getResourceAsStream(resource));
}
private static InputStream getResourceAsStream(String resource) throws IOException{
ClassLoader[] classLoaders = getClassLoaders();
for (ClassLoader classLoader : classLoaders){
InputStream inputStream = classLoader.getResourceAsStream(resource);
if (inputStream != null){
return inputStream;
}
}
throw new IOException("Could not find resource "+resource);
}
private static ClassLoader[] getClassLoaders(){
return new ClassLoader[]{
ClassLoader.getSystemClassLoader(),
Thread.currentThread().getContextClassLoader()};
}
}
- 解析配置文件myabtis-config-datasource.xml中的数据源配置信息,并封装成Configuration类
public class SqlSessionFactoryBuilder {
public DefaultSqlSessionFactory build(Reader reader){
SAXReader saxReader = new SAXReader();
try {
Document document = saxReader.read(new InputSource(reader));
Configuration configuration = parseConfiguration(document.getRootElement());
//创建工厂对象,将配置信息configuration传递给工厂对象
return new DefaultSqlSessionFactory(configuration);
} catch (DocumentException e) {
e.printStackTrace();
}
return null;
}
private Configuration parseConfiguration(Element root){
Configuration configuration = new Configuration();
configuration.setDataSource(dataSource(root.selectNodes("//dataSource")));
configuration.setConnection(connection(configuration.getDataSource()));
configuration.setMapperElement(mapperElement(root.selectNodes("mappers")));
return configuration;
}
private Map<String,String> dataSource(List<Element> list){
Map<String,String> dataSource = new HashMap<>(4);
Element element = list.get(0);
List content = element.content();
for (Object o : content){
Element e = (Element) o;
String name = e.attributeValue("name");
String value = e.attributeValue("value");
dataSource.put(name,value);
}
return dataSource;
}
/**
* 关注该方法,configuration对象中已包含connection连接
* @param dataSource
* @return
*/
private Connection connection(Map<String,String> dataSource){
try {
Class.forName(dataSource.get("driver"));
return DriverManager.getConnection(dataSource.get("url"),dataSource.get("username"),dataSource.get("password"));
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
}
return null;
}
private Map<String,XNode> mapperElement(List<Element> list){
Map<String,XNode> map = new HashMap<>();
Element element = list.get(0);
List content = element.content();
for (Object o : content){
Element e = (Element) o;
String resource = e.attributeValue("resource");
try {
Reader reader = Resources.getResourceReader(resource);
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(new InputSource(reader));
Element root = document.getRootElement();
String namespace = root.attributeValue("namespace");
//select
List<Element> selectNodes = root.selectNodes("select");
for (Element node : selectNodes){
String id = node.attributeValue("id");
String parameterType = node.attributeValue("parameterType");
String resultType = node.attributeValue("resultType");
String sql = node.getText().trim();
//? 匹配
Map<Integer,String> parameterMap = new HashMap<>();
Pattern pattern = Pattern.compile("(#\\{(.*?)})");
Matcher matcher = pattern.matcher(sql);
for (int i=1; matcher.find();i++){
String g1 = matcher.group(1);
String g2 = matcher.group(2);
parameterMap.put(i,g2);
sql = sql.replace(g1,"?");
}
XNode xNode = new XNode();
xNode.setNamespace(namespace);
xNode.setId(id);
xNode.setSql(sql);
xNode.setParameter(parameterMap);
xNode.setParameterType(parameterType);
xNode.setResultType(resultType);
map.put(namespace+"."+id,xNode);
}
} catch (IOException | DocumentException error) {
error.printStackTrace();
}
}
return map;
}
}
1.6 Factory工厂类的实现
提供sqlSession连接
public class DefaultSqlSessionFactory implements SqlSessionFactory{
private final Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration){
this.configuration = configuration;
}
@Override
public SqlSession openSession() {
return new DefaultSqlSession(configuration.getConnection(),configuration.getMapperElement());
}
}
1.7 sqlSession的实现
提供了查询方法,以及查询结果由数据库字段映射到bean字段
public class DefaultSqlSession implements SqlSession{
private Connection connection;
private Map<String, XNode> mapperElement;
public DefaultSqlSession(Connection connection,Map<String,XNode> mapperElement){
this.connection = connection;
this.mapperElement = mapperElement;
}
@Override
public <T> T selectOne(String statement) {
try {
XNode xNode = mapperElement.get(statement);
PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
ResultSet resultSet = preparedStatement.executeQuery();
List<T> objects = resultSet2Obj(resultSet,Class.forName(xNode.getResultType()));
return objects.get(0);
} catch (Exception e){
e.printStackTrace();
}
return null;
}
@Override
public <T> T selectOne(String statement, Object parameter) {
XNode xNode = mapperElement.get(statement);
Map<Integer,String> parameterMap = xNode.getParameter();
try {
PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
buildParameter(preparedStatement,parameter,parameterMap);
ResultSet resultSet = preparedStatement.executeQuery();
List<T> objects = resultSet2Obj(resultSet,Class.forName(xNode.getResultType()));
return objects.get(0);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
public <T> List<T> selectList(String statement) {
XNode xNode = mapperElement.get(statement);
try {
PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
ResultSet resultSet = preparedStatement.executeQuery();
return resultSet2Obj(resultSet,Class.forName(xNode.getResultType()));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
public <T> List<T> selectList(String statement, Object parameter) {
XNode xNode = mapperElement.get(statement);
Map<Integer,String> parameterMap = xNode.getParameter();
try {
PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
buildParameter(preparedStatement,parameter,parameterMap);
ResultSet resultSet = preparedStatement.executeQuery();
return resultSet2Obj(resultSet,Class.forName(xNode.getResultType()));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
public void close() {
}
private void buildParameter(PreparedStatement preparedStatement,Object parameter,Map<Integer,String> parameterMap) throws SQLException, IllegalAccessException {
int size = parameterMap.size();
//单个参数
if (parameter instanceof Long){
for (int i =1;i<=size;i++){
preparedStatement.setLong(i,Long.parseLong(parameter.toString()));
}
}
if (parameter instanceof Integer){
for (int i=1;i<=size;i++){
preparedStatement.setInt(i,Integer.parseInt(parameter.toString()));
}
}
if (parameter instanceof String){
for (int i = 1;i<=size;i++) {
preparedStatement.setString(i,parameter.toString());
}
}
Map<String,Object> fieldMap = new HashMap<>();
//对象参数
Field[] declaredFields = parameter.getClass().getDeclaredFields();
for (Field field : declaredFields){
String name = field.getName();
field.setAccessible(true);
Object obj = field.get(parameter);
fieldMap.put(name,obj);
}
for (int i = 1; i <= size; i++) {
String parameterDefine = parameterMap.get(i);
Object obj = fieldMap.get(parameterDefine);
if (obj instanceof Short) {
preparedStatement.setShort(i, Short.parseShort(obj.toString()));
continue;
}
if (obj instanceof Integer) {
preparedStatement.setInt(i, Integer.parseInt(obj.toString()));
continue;
}
if (obj instanceof Long) {
preparedStatement.setLong(i, Long.parseLong(obj.toString()));
continue;
}
if (obj instanceof String) {
preparedStatement.setString(i, obj.toString());
continue;
}
if (obj instanceof Date) {
preparedStatement.setDate(i, (java.sql.Date) obj);
}
}
}
private <T> List<T> resultSet2Obj(ResultSet resultSet,Class<?> clazz){
List<T> list = new ArrayList<>();
try {
ResultSetMetaData metaData = resultSet.getMetaData();
int columnCount = metaData.getColumnCount();
//每次遍历一行值
while (resultSet.next()){
T obj = (T) clazz.newInstance();
for (int i =1;i<=columnCount;i++){
Object value = resultSet.getObject(i);
String columnName = metaData.getColumnName(i);
String setMethod = "set" + columnName.substring(0, 1).toUpperCase() + columnName.substring(1);
Method method;
if (value instanceof Timestamp){
method = clazz.getMethod(setMethod, Date.class);
} else {
method = clazz.getMethod(setMethod,value.getClass());
}
method.invoke(obj,value);
}
list.add(obj);
}
} catch (Exception e){
e.printStackTrace();
}
return list;
}
}
二、代码分析
可以分析下,功能代码怎么通过调用interface接口方法,映射到mapper.xml文件中的具体SQL、数据库字段怎么对应上java bean中的字段?
2.1 接口方法对应到mapper.xml具体SQL
主要是看解析mapper.xml存储的地方,如下截图标红的地方,mapper.xml中每个SQL端存储的xNode对象,key为:namespace+"."+id,这就是interface接口方法的签名啊,所以可以通过调用接口方法对应到具体的SQL语句。
2.2 数据库字段和JavaBean字段的映射
这里主要用到了resultType设置的结果类的反射方法。
2.3 SQL中的字段名怎么替换
在解析过程中,会将SQL中的#{paramName}或者${paramName}替换成SQL的通配符"?"。
三、mybatis的缓存
3.1 一级缓存
默认开启,同一个sqlSession级别共享缓存,在一个sqlSession的生命周期内,执行2次相同的SQL查询,则第二次SQL查询会直接读取缓存数据,而不走数据库。当然,在第一次和第二次查询之间,执行了DML(INSERT/UPDATE/DELETE)操作,则一级缓存会被清空,第二次SQL查询仍然会走数据库。
一级缓存在下面的情况下会被清除
- 在同一个sqlSession下执行增删改操作时(不必提交),则会清除一级缓存
- SqlSession提交或关闭,会清除一级缓存
- 对mapper.xml的某个CRUD标签,设置属性flushCache=true,这样会导致MappedStatement的一级缓存和二级缓存都失效。
- 在全局配置文件中设置<setting name="localCacheScope" value="STAEMENT">,会使一级缓存失效,二级缓存不受影响。
3.2 二级缓存
默认关闭,可以通过全局配置文件中的<setting name="cacheEnabled" value="true">,开启二级缓存总开关,然后在某个具体的mapper.xml中增加<cache />,即开启该mapper.xml的二级缓存。
开启二级缓存后,多个SqlSession共享一个mapper的二级缓存。SqlSession需要提交,查询数据才会被刷新到二级缓存当中。
四、PageHelper分页插件
一直想搞明白,为什么在查询语句的之前增加PageHelper.startPage(pageNum,pageSize),就能实现分页查询?今天来跟进源码的方式(配合断点),来看下,执行PageHelper.startPage()方法后,查询究竟发生了什么改变。
首先看下PageHelper.startPage(pageNum,pageSize)这个方法,做了什么事情。
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
//将pageNum和pageSize存储到Page对象
Page<E> page = new Page(pageNum, pageSize, count);
page.setReasonable(reasonable);
page.setPageSizeZero(pageSizeZero);
Page<E> oldPage = getLocalPage();
if (oldPage != null && oldPage.isOrderByOnly()) {
page.setOrderBy(oldPage.getOrderBy());
}
//存储page对象
setLocalPage(page);
return page;
}
可以配合贴出代码中注释来看,pageNum和pageSize被存储到了Page对象,然后调用setLocalPage()方法,继续进入setLocalPage()方法。
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal();
protected static boolean DEFAULT_COUNT = true;
public PageMethod() {
}
//存储到ThreadLocal中
protected static void setLocalPage(Page page) {
LOCAL_PAGE.set(page);
}
由上面提出的源码,可以看出page对象别存储到了ThreadLocal中,本地线程变量,可以看下另一篇文章《ThreadLocal原理》。每条线程调用get方法,可以取到线程本身的page对象信息,线程之间做到隔离,互不影响。
到这里,PageHelper.startPage()方法功能完成,下面需要看的是在mybatis执行查询时,怎么用到这个存储到ThreadLocal中的page对象的。
mybatis执行方法,从MapperProxy这个代理类开始看,如果用的是mybatis-plus,可以从其重写类MybatisMapperProxy开始看,提出其invoke方法。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
if (this.isDefaultMethod(method)) {
return this.invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
//这里可以通过上面手写mybatis源码,了解到怎么找到映射方法
MybatisMapperMethod mapperMethod = this.cachedMapperMethod(method);
return mapperMethod.execute(this.sqlSession, args);
}
继续进入到execute()这个执行方法里,方法源码就不全部贴出了,主要看下select中executeForMany()方法
最终,会跟进到Plugin这个代理的invoke方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass());
//关注这个拦截器方法,跟进去,发现PageHelper也实现了这个拦截器方法
return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) : method.invoke(this.target, args);
} catch (Exception var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
}
拦截器方法,PageHelper也实现了这个拦截器,到这里,基本可以猜测到PageHelper怎么通过设置pageNum和pageSize,就可以实现分页查询,我们继续,进入到PageHelper的拦截器类PageInterceptor.java的intercept()方法。
代码比较多,截图了从ThreadLocal本地线程变量中获取page对象信息的方法。
跟进去,就可以看到调用ThreadLocal的get()方法,获取到对应线程的page对象信息。
public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
String sql = boundSql.getSql();
//获取ThreadLocal中本线程的Page信息
Page page = this.getLocalPage();
String orderBy = page.getOrderBy();
if (StringUtil.isNotEmpty(orderBy)) {
pageKey.update(orderBy);
sql = OrderByParser.converToOrderBySql(sql, orderBy);
}
//getPageSql()方法,为组装分页查询SQL方法,提供了不同数据库分页查询SQL的封装工具类,如Oracle、PG、MySQL等
return page.isOrderByOnly() ? sql : this.getPageSql(sql, page, pageKey);
}
继续往下跟,就是组装sql的过程,提供了不同数据库的组装sql方法,在getPageSql()方法中,比较简单,到这里,就清楚PageHelper的整个工作流程。