Mybatis学习

        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的整个工作流程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值