Java 杂谈Java学习笔记Dao

【MyBatis】MyBatis介绍及基本使用

2018-08-15  本文已影响2人  颜洛滨

MyBatis介绍及基本使用

前言

关于MyBatis,其实去年学习的时候就开始使用了,不过当时由于刚开始学习框架,对一些东西的理解不是很好,所有在学习MyBatis的时候,虽然有想法想把笔记整理出来作为博客发布,但是一直不敢动手,一方面是对MyBatis的使用不是很熟悉,另一方面是懒吧,囧,这几天趁着有时间重新回顾了MyBaits并且加上接触一些项目,所以就把学习的过程笔记整理出来。

关于MyBatis

MyBatis的前身是Ibatis,是一款优秀的持久层框架官网地址,其对JDBC进行了一系列的封装,提供了自动化的参数注入以及结果集抽取,然而又与Hibernate不同,MyBatis并没有提供全自动的SQL生成,不过,这也提供了极大地便利,我们可以根据情况编写合适的SQL,尤其是对于大牛来说,可以写出性能高一点的SQL,当然,这个我还不行,囧。

关于MyBatis,就不做过多的介绍了,其背景对于我们使用也没有太大的意义

MyBatis基本使用

MyBatis是持久层框架,所有,必不可少要与数据库打交道,这里我使用的是MySQL,首先建立基本的测试库以及测试表

数据来自刘增辉老师的《MyBatis从入门到精通》


create database mybatis default character set utf8 collate utf8_general_ci;
use mybatis;

# 测试表
create table `country`(
  `id` int not null auto_increment,
  `country_name` varchar(255) null ,
  `country_code` varchar(255) null ,
  primary key (`id`)
);

# 测试数据
insert into country (country_name, country_code)
  values ('中国', 'CN'), 
        ('美国', 'US'), 
        ('俄罗斯', 'RU'), 
        ('英国', 'GB'), 
        ('法国', 'FR');

导入MyBatis以及数据库连接池依赖

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.4.6</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.13</version>
</dependency>

在MyBatis中,需要一个配置MyBatis的配置文件,用于指导MyBatis如何进行工作,如加载mapper文件的位置,类型处理器等等的操作,一般将其放置项目根目录就行

config.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="UNPOOLED">
                <property name="driver" value="com.mysql.jdbc.Driver" />
                <property name="url" value="jdbc:mysql://100.69.4.49:1080/mybatis?useUnicode=true&amp;characterEncoding=utf8" />
                <property name="username" value="root" />
                <property name="password" value="huanfeng" />
            </dataSource>
        </environment>
    </environments>

    <!--指定配置文件路径-->
    <mappers>
        <mapper resource="mapper/countryMapper.xml"/>
    </mappers>
</configuration>

有了MyBatis配置文件之后,还需要多个mapper文件,一般一个mapper对应一个dao对象,也就是一个mapper对应一个对象的操作

这里对应上面的Country表,建立一个CountryMapper.xml文件,放置路径位于mapper/,也可以自己指定,然后修改上面的<mappers>中的路径即可

CountryMapper.xml

<?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">

<!--namespace要全局唯一,用于区分不同的mapper,一般是用对应的接口的全限定名-->
<mapper namespace="mapper.CountryMapper">
    <!--查询语句,id在同个mapper文件中也要唯一,一般是方法的名称-->
    <select id="selectAll" resultType="Country">
        select
            id,
            country_name as countryName,
            country_code as countryCode
        from country;
    </select>
</mapper>

对应的实例类Country.java

public class Country {
    private Long id;
    private String countryName;
    private String countryCode;

    // get() set() toString()
}

虽然在简单的一个操作中,不需要配置接口文件也行,不过,一般一个mapper文件都会对应一个接口文件,这里就是CountryMapper.java

public interface CountryMapper {
    List<Country> selectAll();
}

接下来是加载配置文件并且启动MyBatis

public class CountryMapperTest{
    private static SqlSessionFactory sqlSessionFactory;

    @BeforeClass
    public static void init() {
        try {
            // 通过MyBatis自带的Resources工具来加载配置文件
            Reader reader = Resources.getResourceAsReader("config.xml");
            // 通过SqlSessionFactoryBuilder()来构建sqlSessionFactory对象
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void testSelectAll() {
        // 打开一个session对象
        SqlSession sqlSession = sqlSessionFactory.openSession();
        try {
            // 通过session对象来获取mapper
            CountryMapper countryMapper = sqlSession.getMapper(CountryMapper.class);
            List<Country> countryList = countryMapper.selectAll();
            System.out.println(countryList);
        }finally {
            // 记得关闭session
            sqlSession.close();
        }
    }
}

输出结果

[
    Country{id=1, countryName='中国', countryCode='CN'}, 
    Country{id=2, countryName='美国', countryCode='US'}, 
    Country{id=3, countryName='俄罗斯', countryCode='RU'}, 
    Country{id=4, countryName='英国', countryCode='GB'}, 
    Country{id=5, countryName='法国', countryCode='FR'}
]

至此,一个简单的实例就跑起来了

比较常用的几个配置

在config.xml中,有非常多的配置可以进行设置,具体可以参考官方的配置,其中有几个比较常用的罗列如下

<configuration>

    <!--
        指定实体类别名,
        在前面的配置中,如果我们的实体类存在于包中,那么在mapper文件中,需要使用的时候
        需要使用类的全限定名来指定,配置了别名之后,可以直接使用实体类名就行
        当然,实体类名也是需要全局唯一的,如果不唯一的话,就使用全限定类名来指定
    -->
    <typeAliases>
        <package name="domain"/>
    </typeAliases>

    <!--
        指定接口的位置,需要注意与<mapper resource="" />的区别
        需要注意的是,package是接口文件的包名,不是mapper.xml文件的位置
        不过一般打包的时候,会将mapper.xml与接口文件放在一起,所以本质也是一样
    -->
    <mappers>
        <package name="mapper"/>
    </mappers>
</configuration>

看到上面的例子,可能会觉得很奇怪,为什么通过接口文件就能进行操作,其实仔细分析也比较清楚,MyBatis通过扫描接口之后,通过动态代理技术生成对应的实例类,并且将其缓存起来。

MyBatis源码简单剖析

下面的内容是跟踪一个sqlSessionFactory的构建过程


SqlSessionFactory = new SqlSessionFactoryBuilder.build(reader)

// 其中build过程如下
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      return build(parser.parse());
      // 省略其他操作
}

// parse过程如下
public Configuration parse() {
    // 省略其他操作
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
}

// parseConfiguration过程如下
private void parseConfiguration(XNode root) {
    // 省略其他操作
    mapperElement(root.evalNode("mappers"));
}

// mapperElement过程
private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          // 如果是 <mapper package name="" />则解析对应的包内的所有接口
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        }
        // 省略其他操作
      }
    }
}

// addMappers过程如下
public void addMappers(String packageName) {
    mapperRegistry.addMappers(packageName);
}

// addMappers

public void addMappers(String packageName) {
    addMappers(packageName, Object.class);
}

public void addMappers(String packageName, Class<?> superType) {
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    // 获取所有的class对象
    Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
    for (Class<?> mapperClass : mapperSet) {
      addMapper(mapperClass);
    }
}

// addMapper
public <T> void addMapper(Class<T> type) {
    // 检查是否是接口
    if (type.isInterface()) {
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
        // 参见下面代码
        knownMappers.put(type, new MapperProxyFactory<T>(type));
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
    }
}

// mapperRegistry定义
public class MapperRegistry {
  private final Configuration config;
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
}

// mapperProxyFactory
public class MapperProxyFactory<T> {
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
}

到了这里配置文件解析以及mapper接口的查找完成,接下来就是获取以及实例化的过程了,也就是调用sqlSession.getMapper的过程了

sqlSession.getMapper(CountryMapper.class);

// getMapper过程
public <T> T getMapper(Class<T> type) {
    return configuration.<T>getMapper(type, this);
}

// 可以看到,其实就是从mapperRegistry中获取mapper了
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
}

// 通过mapperProxyFactory来实例化
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
}

// mapperProxyFactory#newInstance过程
public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
}

// newInstance过程
// 从这里就可以看到,其实就是通过JDK的动态代理来实例化接口对象
protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

到了这里,接口的实现就完成了,但是,分析的过程还没有结束,因为SQL语句和接口中的方法还没有对应上,继续分析

// 在JDK动态代理中,我们是需要传入一个InvocationHandler的实现类的,这里就是mapperProxy了
public class MapperProxy<T> implements InvocationHandler, Serializable {
}

// invoke方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      // 如果是Object的方法,则直接调用
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
        // 默认方法
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    // 将方法缓存
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    // 调用接口的方法
    return mapperMethod.execute(sqlSession, args);
}

// 缓存方法
private MapperMethod cachedMapperMethod(Method method) {
    MapperMethod mapperMethod = methodCache.get(method);
    if (mapperMethod == null) {
      mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
      methodCache.put(method, mapperMethod);
    }
    return mapperMethod;
}

// MapperMethod
public class MapperMethod {
    private final SqlCommand command;
    private final MethodSignature method;
}

// sqlCommand,主要就是sql的绑定啦
public static class SqlCommand {

    private final String name;
    private final SqlCommandType type;

    public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
      final String methodName = method.getName();
      final Class<?> declaringClass = method.getDeclaringClass();
      MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
          configuration);
      if (ms == null) {
        if (method.getAnnotation(Flush.class) != null) {
          name = null;
          type = SqlCommandType.FLUSH;
        }
      } else {
        name = ms.getId();
        type = ms.getSqlCommandType();
      }
    }
}

// 解析mapper语句
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
        Class<?> declaringClass, Configuration configuration) {
      String statementId = mapperInterface.getName() + "." + methodName;
      if (configuration.hasStatement(statementId)) {
        return configuration.getMappedStatement(statementId);
      } else if (mapperInterface.equals(declaringClass)) {
        return null;
      }
      for (Class<?> superInterface : mapperInterface.getInterfaces()) {
        if (declaringClass.isAssignableFrom(superInterface)) {
          // 递归解析
          MappedStatement ms = resolveMappedStatement(superInterface, methodName,
              declaringClass, configuration);
          if (ms != null) {
            return ms;
          }
        }
      }
      return null;
    }
}

// execute方法
public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    // 根据SQL中的类型来执行不同的操作
    switch (command.getType()) {
      // 其他的操作
      
      // select操作
      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;
    }
    return result;
}

关于解析SQL这些的话,留到以后再做分析

至此,一个从实例化到调用到方法与SQL的绑定的过程就分析完成了。

总结

本小节主要介绍了MyBatis以及MyBatis的基本使用,虽然MyBatis的配置看起来比较繁琐,但是当需要操作的接口对象多的时候,MyBatis的优势就凸显出来了,这些配置相对而言,也就变得比较简单了,此外,我们顺便剖析了一下MyBatis中的源码,等后面有机会,再深入分析一下,这里先挖个坑。

上一篇下一篇

猜你喜欢

热点阅读