mybatis

Mybatis

MyBatis官网地址:http://www.mybatis.org/mybatis-3/

第一部分 自定持久层框架

1.1 初始化数据库

-- 用户表和记录
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
​
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `password` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `birthday` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
​
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'lucy', '123', '2019-12-12');
INSERT INTO `user` VALUES (2, 'tom', '123', '2019-12-12');
INSERT INTO `user` VALUES (3, 'jack', '123456', '2020-12-02');
​
SET FOREIGN_KEY_CHECKS = 1;

1.2 分析JDBC操作问题

package com.topxin.test;
import com.topxin.pojo.User;
import java.sql.*;
public class Main {
    public static void main(String[] args) throws SQLException {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        try{
            //1.获取驱动
            Class.forName("com.mysql.jdbc.Driver");
            //2.通过驱动获取数据库连接
            connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/zdy_mybatis?characterEncoding=utf-8","root","root");
            //3.定义sql语句
            String sql = "select * from user where username = ?";
            //4.获取预处理statement对象
            preparedStatement = connection.prepareStatement(sql);
            //5.设置参数
            preparedStatement.setString(1,"tom");
            //6.查询
            resultSet  = preparedStatement.executeQuery();
            //7.遍历结果集合
            while (resultSet.next()){
                int id = resultSet.getInt("id");
                String username = resultSet.getString("username");
                //8.封装user
                User user = new User();
                user.setId(id);
                user.setUsername(username);
                System.out.println(user);
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //9.释放资源
            if (resultSet != null) {
                resultSet.close();
            }
            if(preparedStatement != null){
                preparedStatement.close();
            }
           if(connection != null){
               connection.close();
           }
        }
    }
}

JDBC问题总结:

原始jdbc开发存在的问题如下:

1、 数据库连接创建、释放频繁造成系统资源浪费,从而影响系统性能。

2、 Sql语句在代码中硬编码,造成代码不易维护,实际应用中sql变化的可能较大,sql变动需要改变java代码。

3、 使用preparedStatement向占有位符号传参数存在硬编码,因为sql语句的where条件不一定,可能多也可能少,修改sql还要修改代码,系统不易维护。

4、 对结果集解析存在硬编码(查询列名),sql变化导致解析代码变化,系统不易维护,如果能将数据库记录封装成pojo对象解析比较方便

1.3 问题解决思路

①使用数据库连接池初始化连接资源

②将sql语句抽取到xml配置文件中

③使用反射、内省等底层技术,自动将实体与表进行属性与字段的自动映射

1.4 自定义持久层框架设计思路

使用端:

提供核心配置文件:

sqlMapConfig.xml : 存放数据源信息,引入mapper.xml

Mapper.xml : sql语句的配置文件信息

框架端:

1.读取配置文件

读取完成以后以流的形式存在,我们不能将读取到的配置信息以流的形式存放在内存中,不好操作,可

以创建javaBean来存储

(1)Configuration : 存放数据库基本信息、Map<唯一标识,Mapper> 唯一标识:namespace + "."+ id

(2)MappedStatement:sql语句、statement类型、输入参数java类型、输出参数java类型

2.解析配置文件

创建sqlSessionFactoryBuilder类:

方法:sqlSessionFactory build():

第一:使用dom4j解析配置文件,将解析出来的内容封装到Configuration和MappedStatement中

第二:创建SqlSessionFactory的实现类DefaultSqlSession

3.创建SqlSessionFactory:

方法:openSession() : 获取sqlSession接口的实现类实例对象

4.创建sqlSession接口及实现类:主要封装crud方法

方法:selectList(String statementId,Object param):查询所有

selectOne(String statementId,Object param):查询单个

具体实现:封装JDBC完成对数据库表的查询操作

涉及到的设计模式:

Builder构建者设计模式、工厂模式、代理模式

1.5 自定义框架实现

1.4.1 pom.xml


    
    
        mysql
        mysql-connector-java
        5.1.17
    
    
    
        c3p0
        c3p0
        0.9.1.2
    
    
    
        log4j
        log4j
        1.2.8
    
    
    
        junit
        junit
        4.12
        test
    
    
    
        dom4j
        dom4j
        1.6.1
    
    
        jaxen
        jaxen
        1.1.6
    

1.4.2 配置文件(核心)

sqlMapperConfig.xml


    
    
        
        
        
        
    
    
    

UserMapper.xml


    
    
    
    

1.4.3 创建实体

Configuration

public class Configuration {
    //连接数据库信息封装
    private DataSource dataSource;   
    //key:statementId value:封装好的mappedStatement对象
    Map mapperStatementMap = new HashMap();
    ...get/set
}

MapperStatement

public class MapperStatement {
    private String id;    //id标识
    private String resultType;    //返回值类型
    private String parameterType;   //参数值类型
    private String sql;    //sql语句
    ...get/set
}

User

public class User {
    private Integer id;
    private String username;
    private String password;
    private String birthday;
    ...get/set
}

1.4.4 读取解析封装

Resource

读取:Resource获取配置文件流

package com.topxin.io;
​
import java.io.InputStream;
​
public class Resource {
    //根据配置文件的路径,将配置文件加载成字节输入流,存储在内存中
    public static InputStream getResourceAsSteam(String path){
        InputStream resourceAsStream = Resource.class.getClassLoader().getResourceAsStream(path);
        return resourceAsStream;
    }
}

XMlConfigBuilder

解析:将SqlMapperConfig.xml配置文件通过dom4j解析

封装:Configuration对象

注意:Configuration对象同时封装MapperStatement

package com.topxin.config;
​
import com.mchange.v2.c3p0.ComboPooledDataSource;
import com.topxin.io.Resource;
import com.topxin.pojo.Configuration;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
​
import java.beans.PropertyVetoException;
import java.io.InputStream;
import java.util.List;
import java.util.Properties;
​
public class XMlConfigBuilder {
​
    private Configuration configuration;
​
    public XMlConfigBuilder() {
        this.configuration = new Configuration();
    }
​
    /**
     * 该方法即使将配置文件进行解析,封装Configuration
     * @return
     */
    public Configuration parseConfig(InputStream inputStream) throws PropertyVetoException, DocumentException {
        Document document = new SAXReader().read(inputStream);
        //configuration
        Element rootElement = document.getRootElement();
        Properties properties = new Properties();
        List list = rootElement.selectNodes("//property");
        for (Element element : list) {
            String name = element.attributeValue("name");
            String value = element.attributeValue("value");
            properties.setProperty(name,value);
        }
        ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
        comboPooledDataSource.setDriverClass(properties.getProperty("driverClass"));
        comboPooledDataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));
        comboPooledDataSource.setUser(properties.getProperty("username"));
        comboPooledDataSource.setPassword(properties.getProperty("password"));
        configuration.setDataSource(comboPooledDataSource);
        //mapper.xml解析,拿到路径,根据路径---字节输入六---dom4j解析
        List mapperList = rootElement.selectNodes("//mapper");
        for (Element element : mapperList) {
            String mapperPath = element.attributeValue("resource");
            InputStream resourceAsSteam = Resource.getResourceAsSteam(mapperPath);
            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
            xmlMapperBuilder.parse(resourceAsSteam);
        }
        return configuration;
    }
}

XMLMapperBuilder

解析:将UserMapper.xml配置文件通过dom4j解析

封装:MapperStatement对象

注意:根据 statementId的key = namespace +"."+ id

package com.topxin.config;
​
import com.topxin.pojo.Configuration;
import com.topxin.pojo.MapperStatement;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
​
import java.io.InputStream;
import java.util.List;
​
public class XMLMapperBuilder {
​
    private Configuration configuration;
​
    public XMLMapperBuilder(Configuration configuration) {
        this.configuration = configuration;
    }
​
    public void  parse(InputStream inputStream) throws DocumentException {
        Document document = new SAXReader().read(inputStream);
        Element rootElement = document.getRootElement();
        String namespace = rootElement.attributeValue("namespace");
        List list = rootElement.selectNodes("//select");
        for (Element element : list) {
            String id = element.attributeValue("id");
            String resultType = element.attributeValue("resultType");
            String paramterType = element.attributeValue("parameterType");
            String sqlText = element.getTextTrim();
            MapperStatement mapperStatement = new MapperStatement();
            mapperStatement.setId(id);
            mapperStatement.setResultType(resultType);
            mapperStatement.setParameterType(paramterType);
            mapperStatement.setSql(sqlText);
            String key = namespace +"."+ id;
            configuration.getMapperStatementMap().put(key,mapperStatement);
        }
    }
}

1.4.5 SqlSessionFactory

核心方法openSession()

SqlSessionFactoryBuilder

package com.topxin.sqlSession;
​
import com.topxin.config.XMlConfigBuilder;
import com.topxin.pojo.Configuration;
import org.dom4j.DocumentException;
​
import java.beans.PropertyVetoException;
import java.io.InputStream;
​
public class SqlSessionFactoryBuilder {
    public SqlSessionFactory build(InputStream in) throws PropertyVetoException, DocumentException {
        //第一:使用dom4j解析配置文件,将解析出来的内容防撞到Configuration中
        XMlConfigBuilder xMlConfigBuilder = new XMlConfigBuilder();
        Configuration configuration = xMlConfigBuilder.parseConfig(in);
        //第二:创建sqlSessionFactory对象,工厂类,生产sqlSession会话对象
        DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration);
        return  defaultSqlSessionFactory;
    }
}

SqlSessionFactory接口

package com.topxin.sqlSession;
​
public interface SqlSessionFactory {
    public SqlSession openSession();
}

SqlSessionFactory接口的实现类DefaultSqlSessionFactory

package com.topxin.sqlSession;
​
import com.topxin.pojo.Configuration;
​
public class DefaultSqlSessionFactory implements  SqlSessionFactory{
​
    private  Configuration configuration;
​
    public DefaultSqlSessionFactory(Configuration configuration) {
        this.configuration = configuration;
    }
​
    @Override
    public DefaultSqlSession openSession() {
        return new DefaultSqlSession(configuration);
    }
}

1.4.6 SqlSession

invoke()方法

SqlSession接口

package com.topxin.sqlSession;
​
import java.util.List;
​
public interface SqlSession {
​
    //查询所有
    public  List selectList(String statementId,Object... params) throws Exception;
​
    //根据条件查询单个
    public  T selectOne(String statementId,Object... params) throws Exception;
​
    //为Dao接口生产代理实现类
    public  T  getMapper(Class mapperClass);
}

SqlSession接口的实现类

package com.topxin.sqlSession;
​
import com.topxin.pojo.Configuration;
import com.topxin.pojo.MapperStatement;
​
import java.lang.reflect.*;
import java.util.List;
​
public class DefaultSqlSession implements SqlSession {
​
    private Configuration configuration;
​
    public DefaultSqlSession(Configuration configuration){
        this.configuration = configuration;
    }
​
    @Override
    public  List selectList(String statementId, Object... params) throws Exception {
        //SimpleExecutor里的query方法调用
        SimpleExecutor simpleExecutor = new SimpleExecutor();
        MapperStatement mapperStatement = configuration.getMapperStatementMap().get(statementId);
        List list = simpleExecutor.query(configuration, mapperStatement, params);
        return (List) list;
    }
​
    @Override
    public  T selectOne(String statementId, Object... params) throws Exception {
        List objects = selectList(statementId, params);
        if(objects.size() == 1){
            return  (T) objects.get(0);
        }else {
            throw new RuntimeException("查询结果未空或者返回多条结果");
        }
    }
​
    @Override
    public  T getMapper(Class mapperClass) {
        //使用JDK动态代理来为Dao接口涩会给你从代理对象,并返回
        Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
            /**
             * 代理对象调用接口任何方法,都会执行invoke方法
             * @param proxy 当前代理对象的应用
             * @param method  当前被调用的方法引用
             * @param args  传递的参数(user)
             * @return
             * @throws Throwable
             */
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //注意:底层还是去调用JDBC代码//根据不同情况,来调用selectList或者SelectOne
                //准备参数1:statementId:sql语句的唯一标识:namespace.id = 接口权限定名。方法名
                //方法名
                String methodName = method.getName();
                String className = method.getDeclaringClass().getName();
                String statementId = className +"."+ methodName;
                //准备参数 params:args
                //获取被调用方法的反回值类型
                Type genericReturnType = method.getGenericReturnType();
                //判断是否进行了,泛型类型参数化
                if(genericReturnType instanceof ParameterizedType){
                    List objects = selectList(statementId, args);
                    return objects;
                }
                return selectOne(statementId,args);
            }
        });
        return (T) proxyInstance;
    }
} 
  

1.4.7 Executor

Executor接口

package com.topxin.sqlSession;
​
import com.topxin.pojo.Configuration;
import com.topxin.pojo.MapperStatement;
​
import java.sql.SQLException;
import java.util.List;
​
public interface Executor {
​
    public  List query(Configuration configuration, MapperStatement mapperStatement, Object... params) throws SQLException, Exception;
}

BoundSql封装解析后的Sql语句

package com.topxin.sqlSession;
​
import com.topxin.utils.ParameterMapping;
​
import java.util.ArrayList;
import java.util.List;
​
public class BoundSql {
    private String sqlText;     //解析过后的sql
    private List parameterMappings = new ArrayList<>();
    public BoundSql(String sqlText, List parameterMappings) {
        this.sqlText = sqlText;
        this.parameterMappings = parameterMappings;
    }
    public String getSqlText() {
        return sqlText;
    }
    public void setSqlText(String sqlText) {
        this.sqlText = sqlText;
    }
    public List getParameterMappings() {
        return parameterMappings;
    }
    public void setParameterMappings(List parameterMappings) {
        this.parameterMappings = parameterMappings;
    }
}

SimpleExecutor类实现了Executor接口

package com.topxin.sqlSession;
​
import com.topxin.pojo.Configuration;
import com.topxin.pojo.MapperStatement;
import com.topxin.utils.GenericTokenParser;
import com.topxin.utils.ParameterMapping;
import com.topxin.utils.ParameterMappingTokenHandler;
​
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.util.ArrayList;
import java.util.List;
​
public class SimpleExecutor implements  Executor {
​
    @Override
    public  List query(Configuration configuration, MapperStatement mapperStatement, Object... params) throws Exception {
        //1.获取连接
        Connection connection = configuration.getDataSource().getConnection();
        //2,获取sql语句,还需要#{}进行转换?
        String sql = mapperStatement.getSql();
        BoundSql boundSql = getBoundSql(sql);
        //3/获取预处理对象
        PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());
        //4.设置参数
        String parameterType = mapperStatement.getParameterType();//获取到参数
        Class parameterTypeClass = getClassType(parameterType);
        List parameterMappings = boundSql.getParameterMappings();
        for (int i = 0; i < parameterMappings.size(); i++) {
            ParameterMapping parameterMapping = parameterMappings.get(i);
            String content = parameterMapping.getContent();
            //反射
            Field declaredField = parameterTypeClass.getDeclaredField(content);
            declaredField.setAccessible(true);
            Object o = declaredField.get(params[0]);
            preparedStatement.setObject(i+1,o);     //占位符从1开始
        }
        //5.执行sql
        ResultSet resultSet = preparedStatement.executeQuery();
        String resultType = mapperStatement.getResultType();
        Class resultTypeClass = getClassType(resultType);
        ArrayList objects = new ArrayList<>();
        //6.分装返回结果集
        while(resultSet.next()){
            Object o = resultTypeClass.newInstance();
            //元数据
            ResultSetMetaData metaData = resultSet.getMetaData();
            for (int i = 1; i <= metaData.getColumnCount(); i++) {
                //字段名称
                String columnName = metaData.getColumnName(i);
                //字段的值
                Object value = resultSet.getObject(columnName);
                //使用反射,根据数据库表和实体的对象关系完成封装
                PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultTypeClass);
                Method writeMethod = propertyDescriptor.getWriteMethod();
                writeMethod.invoke(o,value);
            }
            objects.add(o);
        }
        return (List) objects;
    }
​
    private Class getClassType(String parameterType) throws ClassNotFoundException {
        if(parameterType != null){
            Class aClass = Class.forName(parameterType);
            return  aClass;
        }
        return null;
    }
​
    /**
     * 完成对#{}的解析共工作,1将#{}使用?,进行代替,2解析#{}里面的
     * @param sql
     * @return
     */
    private BoundSql getBoundSql(String sql) {
        ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
        GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
        //解析后的sql
        String parseSql = genericTokenParser.parse(sql);
        //#{}里面解析出来的参数名称
        List parameterMappings = parameterMappingTokenHandler.getParameterMappings();
        BoundSql boundSql = new BoundSql(parseSql,parameterMappings);
        return  boundSql;
    }
} 
  

1.4.8 Utils

GenericTokenParser解析Mybaitis中Sql语句的${}和#{}

/**
 *    Copyright 2009-2017 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package com.topxin.utils;
​
/**
 * @author Clinton Begin
 */
public class GenericTokenParser {
​
  private final String openToken; //开始标记
  private final String closeToken; //结束标记
  private final TokenHandler handler; //标记处理器
​
  public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
    this.openToken = openToken;
    this.closeToken = closeToken;
    this.handler = handler;
  }
​
  /**
   * 解析${}和#{}
   * @param text
   * @return
   * 该方法主要实现了配置文件、脚本等片段中占位符的解析、处理工作,并返回最终需要的数据。
   * 其中,解析工作由该方法完成,处理工作是由处理器handler的handleToken()方法来实现
   */
  public String parse(String text) {
    // 验证参数问题,如果是null,就返回空字符串。
    if (text == null || text.isEmpty()) {
      return "";
    }
​
    // 下面继续验证是否包含开始标签,如果不包含,默认不是占位符,直接原样返回即可,否则继续执行。
    int start = text.indexOf(openToken, 0);
    if (start == -1) {
      return text;
    }
​
   // 把text转成字符数组src,并且定义默认偏移量offset=0、存储最终需要返回字符串的变量builder,
    // text变量中占位符对应的变量名expression。判断start是否大于-1(即text中是否存在openToken),如果存在就执行下面代码
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;
    while (start > -1) {
     // 判断如果开始标记前如果有转义字符,就不作为openToken进行处理,否则继续处理
      if (start > 0 && src[start - 1] == '\\') {
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      } else {
        //重置expression变量,避免空指针或者老数据干扰。
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          expression.setLength(0);
        }
        builder.append(src, offset, start - offset);
        offset = start + openToken.length();
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {存在结束标记时
          if (end > offset && src[end - 1] == '\\') {//如果结束标记前面有转义字符时
            // this close token is escaped. remove the backslash and continue.
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
          } else {//不存在转义字符,即需要作为参数进行处理
            expression.append(src, offset, end - offset);
            offset = end + closeToken.length();
            break;
          }
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          //首先根据参数的key(即expression)进行参数处理,返回?作为占位符
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      start = text.indexOf(openToken, offset);
    }
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }
}

ParameterMapping分装Mybaitis中Sql语句的${}和#{}的内容

package com.topxin.utils;
​
public class ParameterMapping {
​
    private String content;
​
    public ParameterMapping(String content) {
        this.content = content;
    }
​
    public String getContent() {
        return content;
    }
​
    public void setContent(String content) {
        this.content = content;
    }
}
​

TokenHandler接口

/**
 *    Copyright 2009-2015 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package com.topxin.utils;
​
/**
 * @author Clinton Begin
 */
public interface TokenHandler {
  String handleToken(String content);
}

ParameterMappingTokenHandler类

package com.topxin.utils;
​
import java.util.ArrayList;
import java.util.List;
​
public class ParameterMappingTokenHandler implements TokenHandler {
    private List parameterMappings = new ArrayList();
​
    // context是参数名称 #{id} #{username}
​
    public String handleToken(String content) {
        parameterMappings.add(buildParameterMapping(content));
        return "?";
    }
​
    private ParameterMapping buildParameterMapping(String content) {
        ParameterMapping parameterMapping = new ParameterMapping(content);
        return parameterMapping;
    }
​
    public List getParameterMappings() {
        return parameterMappings;
    }
​
    public void setParameterMappings(List parameterMappings) {
        this.parameterMappings = parameterMappings;
    }
}

1.4.9 Test测试

package com.topxin.test;
​
import com.topxin.dao.IUserDao;
import com.topxin.io.Resource;
import com.topxin.pojo.User;
import com.topxin.sqlSession.SqlSession;
import com.topxin.sqlSession.SqlSessionFactory;
import com.topxin.sqlSession.SqlSessionFactoryBuilder;
import org.junit.Test;
​
import java.io.InputStream;
import java.util.List;
​
public class IPersistenceTest {
​
    @Test
    public void test() throws Exception {
        InputStream resourceAsSteam = Resource.getResourceAsSteam("sqlMapperConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsSteam);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        //调用
        User user = new User();
        user.setId(1);
        user.setUsername("lucy");
       /* User u = sqlSession.selectOne("user.selectOne", user);
        System.out.println(u);*/
        /*List users = sqlSession.selectList("user.selectList");
        for (User u : users) {
            System.out.println(u);
        }*/
        //返回的userDao是代理对象proxy
        IUserDao userDao = sqlSession.getMapper(IUserDao.class);
        /*User byCondition = userDao.findByCondition(user);
        System.out.println(byCondition);*/
        List all = userDao.findAll();
        for (User user1 : all) {
            System.out.println(user1);
        }
    }
}

1.6 自定义框架优化

通过上述我们的自定义框架,我们解决了JDBC操作数据库带来的一些问题:例如频繁创建释放数据库连接,硬编码,手动封装返回结果集等问题,但是现在我们继续来分析刚刚完成的自定义框架代码,有没有什么问题?

问题如下:

  • dao的实现类中存在重复的代码,整个操作的过程模板重复(创建sqlsession,调用sqlsession方法,关闭 sqlsession)

  • dao的实现类中存在硬编码,调用sqlsession的方法时,参数statement的id硬编码

解决:使用代理模式来创建接口的代理对象

@Test 
public void test2() throws Exception { 
    InputStream resourceAsSteam = Resources.getResourceAsSteam(path: "sqlMapConfig.xml")            SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsSteam); 
    SqlSession  sqlSession = build.openSession(); 
    User user = new User(); 
    user.setld(l); 
    user.setUsername("tom"); //代理对象 
    UserMapper userMapper = sqlSession.getMappper(UserMapper.class); 
    User userl = userMapper.selectOne(user); 
    System・out.println(userl);
}

在sqlSession中添加方法

public interface SqlSession { public  T getMappper(Class mapperClass);}

实现类

@Override 
public  T getMappper(Class mapperClass) { 
    T o = (T) Proxy.newProxyInstance(mapperClass.getClassLoader(), new Class[] {mapperClass}, new InvocationHandler() { 
        @Override 
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // selectOne 
            String methodName = method.getName(); 
            // className:namespace 
            String className = method.getDeclaringClass().getName(); 
            //statementid 
            String key = className+"."+methodName; 
            MappedStatement mappedStatement = configuration.getMappedStatementMap().get(key);
            Type genericReturnType = method.getGenericReturnType(); 
            ArrayList arrayList = new ArrayList<> (); 
            //判断是否实现泛型类型参数化 
            if(genericReturnType instanceof ParameterizedType){ 
                return selectList(key,args); 
                return selectOne(key,args); 
            } 
    }); 
    return o;
}

第二部分 Mybatis相关概念

2.1 对象/关系数据映射ORM

Object Relation Mapping

既可以利用面向对象程序设计语言的简单易用性,又可以利用关系数据库的技术优势

2.2 Mybatis简介

ORM、半自动、轻量级框架

支持定制化sql,存储过程,高级映射

xml或者注解配置和映射原生类

2.3 Mybaits历史

原是apache的一个开源项目iBatis, 2010年6月这个项目由apache software foundation 迁移到了

google code,随着开发团队转投Google Code旗下,ibatis3.x正式更名为Mybatis ,代码于2013年11

月迁移到Github。

iBATIS一词来源于“internet”和“abatis”的组合,是一个基于Java的持久层框架。iBATIS提供的持久层框

架包括SQL Maps和Data Access Objects(DAO)

2.4 Mybaits优势

半自动的持久层框架(轻量级),核心Sql可以自己优化

sql和Java代码分离(功能边界清晰,一个专注业务,一个专注数据)

第三部分 Mybatis基础应用

3.1 快速入门

3.1.1 导入GAV坐标


        
            junit
            junit
            4.12
        
        
            org.mybatis
            mybatis
            3.4.5
        
        
            mysql
            mysql-connector-java
            5.1.46
        
        
            log4j
            log4j
            1.2.17
        
        
            junit
            junit
            4.12
            test
        

3.1.2 创建数据库表

user表,创建语句见1.1

3.1.3 创建对应实体

User

3.1.4 编写映射文件

UserMapper.xml

3.1.5 编写核心配置文件

SqlMapConfig.xml




    
    
        
            
            
                
                
                
                
            
        
    
​
    
        
        
    

3.1.6 编写测试方法

@Test
public void testMybatis() throws IOException {
        //加载核心配置文件,返回流
        InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        //获取sqlSession工厂
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        //获取sqlSession对象
        SqlSession sqlSession = sqlSessionFactory.openSession();
        List list = sqlSession.selectList("userMapper.queryUserList");
        if(list != null && list.size() > 0) {
            for (int i = 0; i < list.size(); i++) {
                User user =  list.get(i);
                System.out.println(user);
            }
        }
        //关闭资源
        sqlSession.close();
}

3.2 MyBatis常用配置解析

3.2.1 environments标签

 

其中,事务管理器(transactionManager)类型有两种:

  • JDBC:这个配置就是直接使用了JDBC 的提交和回滚设置,它依赖于从数据源得到的连接来管理事务作用域。

  • MANAGED:这个配置几乎没做什么。它从来不提交或回滚一个连接,而是让容器来管理事务的整个生命周期(比如 JEE 应用服务器的上下文)。 默认情况下它会关闭连接,然而一些容器并不希望这样,因此需要将 closeConnection 属性设置为 false 来阻止它默认的关闭行为。

其中,数据源(dataSource)类型有三种:

  • UNPOOLED:这个数据源的实现只是每次被请求时打开和关闭连接。

  • POOLED:这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来。

  • JNDI:这个数据源的实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的引用。

3.2.2 mapper标签

该标签的作用是加载映射的,加载方式有如下几种:

  • 使用相对于类路径的资源引用,例如:

  • 使用完全限定资源定位符(URL),例如:

  • 使用映射器接口实现类的完全限定类名,例如:

  • 将包内的映射器接口实现全部注册为映射器,例如:

3.3 Mybatis相关API介绍

SqlSession工厂构建器SqlSessionFactoryBuilder

常用API:SqlSessionFactory build(InputStream inputStream)

通过加载mybatis的核心文件的输入流的形式构建一个SqlSessionFactory对象

String resource = "org/mybatis/builder/mybatis-config.xml"; 
InputStream inputStream = Resources.getResourceAsStream(resource); 
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); 
SqlSessionFactory factory = builder.build(inputStream);

其中, Resources 工具类,这个类在 org.apache.ibatis.io 包中。Resources 类帮助你从类路径下、文

件系统或一个 web URL 中加载资源文件。

SqlSession工厂对象SqlSessionFactory

SqlSessionFactory 有多个个方法创建SqlSession 实例。常用的有如下两个:

方法 解释
openSession()会默认 会默认开启一个事务,但事物不会自动提交,也就意味需要手动提交改事物,更新操作数据库才会吃计划到数据库中
openSession(boolean autoCommit) 参数是否自动提交,如果设置未true,那么不需要手动提交事务

执行语句的方法主要有:

 T selectOne(String statement, Object parameter) 
 List selectList(String statement, Object parameter) 
int insert(String statement, Object parameter) 
int update(String statement, Object parameter) 
int delete(String statement, Object parameter)

操作事务的方法主要有:

void commit() 
void rollback()

3.4 Mybaits的Dao层实现

3.4.1 传统开发方式

3.4.2 代理开发方式

采用 Mybatis 的代理开发方式实现 DAO 层的开发,这种方式是我们后面进入企业的主流。

Mapper 接口开发方法只需要程序员编写Mapper 接口(相当于Dao 接口),由Mybatis 框架根据接口

定义创建接口的动态代理对象,代理对象的方法体同上边Dao接口实现类方法。

Mapper 接口开发需要遵循以下规范:

1) Mapper.xml文件中的namespace与mapper接口的全限定名相同

2) Mapper接口方法名和Mapper.xml中定义的每个statement的id*相同

3) Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql的parameterType的类型相同

4) Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同

 

第四部分 配置文件深入

4.1 SqlMapConfiig.xml

4.1.1 配置

 

4.1.2 environments环境配置

事务管理器(transactionManager)

在 MyBatis 中有两种类型的事务管理器(也就是 type="[JDBC|MANAGED]"):

  • JDBC – 这个配置直接使用了 JDBC 的提交和回滚设施,它依赖从数据源获得的连接来管理事务作用域。

  • MANAGED – 这个配置几乎没做什么。它从不提交或回滚一个连接,而是让容器来管理事务的整个生命周期(比如 JEE 应用服务器的上下文)。 默认情况下它会关闭连接。然而一些容器并不希望连接被关闭,因此需要将 closeConnection 属性设置为 false 来阻止默认的关闭行为。例如:

数据源(dataSource)

dataSource 元素使用标准的 JDBC 数据源接口来配置 JDBC 连接对象的资源。有三种内建的数据源类型(也就是 type="[UNPOOLED|POOLED|JNDI]"):

UNPOOLED– 这个数据源的实现会每次请求时打开和关闭连接。

POOLED(推荐)– 这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。

JNDI – 这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用。

4.1.3 mapper映射器

使用相对于类路径的资源引用

使用完全限定资源定位符(URL)

使用映射器接口实现类的完全限定类名

 

    

4.1.4 properties属性


        
        
        
        

从 MyBatis 3.4.2 开始,你可以为占位符指定一个默认值。例如:


  
   

4)typeAliases类型别名标签



    
    


  
// 扫描包方式自定义别名
@Alias("user")
public class user {...}
别名 映射的类型
_byte byte
_long long
_short short
_int int
_integer int
_double double
_float float
_boolean boolean
string String
byte Byte
long Long
short Short
int Integer
integer Integer
double Double
float Float
boolean Boolean
date Date
decimal BigDecimal
bigdecimal BigDecimal
object Object
map Map
hashmap HashMap
list List
arraylist ArrayList
collection Collection
iterator Iterator

4.2 Mapper.xml

4.2.1 动态sql

  • if

  • choose (when, otherwise)

  • trim (where, set)

  • foreach

  • sql

第五部分 Mybatis复杂映射

5.1 一对一

一个用户对应一个订单

SELECT * FROM orders o INNER JOIN USER u ON o.uid = u.id

resultMap:手动配置

association:一对一




    
    
        
        
        
        
        
            
            
            
            
        
    
    
    

订单表初始化

-- 创建订单表并插入记录
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
​
-- ----------------------------
-- Table structure for orders
-- ----------------------------
DROP TABLE IF EXISTS `orders`;
CREATE TABLE `orders`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `ordertime` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `total` double NULL DEFAULT NULL,
  `uid` int(11) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `uid`(`uid`) USING BTREE,
  CONSTRAINT `orders_ibfk_1` FOREIGN KEY (`uid`) REFERENCES `user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
​
-- ----------------------------
-- Records of orders
-- ----------------------------
INSERT INTO `orders` VALUES (1, '2019-12-12', 3000, 1);
INSERT INTO `orders` VALUES (2, '2019-12-12', 4000, 1);
INSERT INTO `orders` VALUES (3, '2019-12-12', 5000, 2);
​
SET FOREIGN_KEY_CHECKS = 1;

5.2 一对多

一个用户对应多个订单

SELECT * FROM USER u LEFT JOIN orders o ON u.id = o.uid

collection:一对多




    
        
        
        
        
        
        
            
            
            
        
    
    
    

5.3 多对多

多个用户对应多个角色

-- 查询数据sql,对于多对多关系,都是通过中间表关联
SELECT * FROM USER u LEFT JOIN sys_user_role ur ON u.id = ur.userid
INNER JOIN sys_role r ON ur.roleid = r.id;



    
        
        
        
        
        
            
            
            
        
    
    

初始话用户角色关系表和角色表

-- 创建角色表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
​
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `rolename` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `roleDesc` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
​
-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, 'CTO', 'CTO');
INSERT INTO `sys_role` VALUES (2, 'CEO', 'CEO');
​
SET FOREIGN_KEY_CHECKS = 1;
​
-- -------------------------------------------------
​
-- 创建用户角色中间表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
​
-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role`  (
  `userid` int(11) NOT NULL,
  `roleid` int(11) NOT NULL,
  PRIMARY KEY (`userid`, `roleid`) USING BTREE,
  INDEX `roleid`(`roleid`) USING BTREE,
  CONSTRAINT `sys_user_role_ibfk_1` FOREIGN KEY (`userid`) REFERENCES `sys_role` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT `sys_user_role_ibfk_2` FOREIGN KEY (`roleid`) REFERENCES `user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
​
-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES (1, 1);
INSERT INTO `sys_user_role` VALUES (2, 1);
INSERT INTO `sys_user_role` VALUES (1, 2);
INSERT INTO `sys_user_role` VALUES (2, 2);
​
SET FOREIGN_KEY_CHECKS = 1;

5.4 知识小结

MyBatis多表配置方式:

一对一配置:使用做配置

一对多配置:使用+做配置

多对多配置:使用+做配置

第六部分 Mybatis注解开发

注意:多表复杂操作更适合xml

6.1 常用注解

注解 用途  
@Insert 新增  
@Update 更新  
@Delete 删除  
@Select 查询 select
@Result 封装结果集 id和result
@Results 与@Result配合,封装多个结果集 resultMap
@One 实现一对一结果集封装 association
@Many 实现一对多结果集封装 collection

 

6.2 一对一

一个订单对应一个用户uid作为参数

// 用户查询
@Select(value = "select * from user where id = #{id}")
User selectById(Integer id);
//一对一查询
@Results({
@Result(property = "id", column = "id"),
@Result(property = "ordertime", column = "ordertime"),
@Result(property = "total", column = "total"),
@Result(
    javaType = User.class,
    property = "user",
    column = "uid",
    one = @One(select = "com.topxin.mapper.UserMapper.selectById"))
})
@Select(value = "select * from orders")
List findAllOrdersAndUser();

6.3 一对多

一个用户对应多个订单

// select * from user u left join orders o on u.id = o.uid;
@Results({
    @Result(property = "id", column = "id"),
    @Result(property = "username", column = "username"),
    @Result(property = "password", column = "password"),
    @Result(property = "birthday", column = "birthday"),
    @Result(
        property = "orderList",
        column = "id",
        javaType = List.class,
        many = @Many(select = "com.topxin.mapper.OrdersMapper.selectByUId")
    )
})
@Select(value = "select * from user")
List findAllUserAndOrder();
// 根据用户id返回查询结果
@Select(value = "select * from orders where uid = #{uid}")
List selectByUId(Integer uid);

6.4 多对多

多个用户对应多个角色

//select * from user u left join sys_user_role ur on u.id = ur.userid left join sys_role r on ur.roleid = r.id;
@Results({
    @Result(property = "id", column = "id"),
    @Result(property = "username", column = "username"),
    @Result(property = "password", column = "password"),
    @Result(property = "birthday", column = "birthday"),
    @Result(
        property = "roleList",
        column = "id",
        javaType = List.class,
        many = @Many(select = "com.topxin.mapper.RoleMapper.selectByUserId")
    )
})
@Select(value = "select * from user")
List findAllUserAndRole();
@Select(value = "select r.* from sys_role r inner join sys_user_role ur on r.id = ur.roleid where userid = #{uid}")
List selectByUserId(Integer uid);

6.5 注解动态sql

@Update({""})
void updateUserValues(User user);

第七部分 Mybaits缓冲

mybatis使用了俩种缓冲,本地缓冲(local cache)和二级缓冲(second level cache)

 

7.1 一级缓冲

sqlSession(HashMap)

cachekey:statementId、params、boundSql、rowBounds组成

1)第一次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,如果没有,从 数据

库查询用户信息。得到用户信息,将用户信息存储到一级缓存中。

2)如果中间sqlSession去执行commit操作(执行插入、更新、删除),则会清空SqlSession中的 一

级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。

3)第二次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,缓存中有,直 接从

缓存中获取用户信息

4)手动清除一级缓冲sqlSession.claerCache();

@Test
public void localCacheTest(){
    // 使用sqlSession执行第一次查询
    List u1 = userMapper.findUserAndOrder();
    // 使用sqlSession执行第二次查询
    List u2 = userMapper.findUserAndOrder();
    System.out.println(u1 == u2);   //true
    sqlSession.close();
}

一级缓存原理探究与源码分析

一级缓存到底是什么?

一级缓存什么时候被创建?

一级缓存的工作流程是怎样的?

相信你现在应该会有这几个疑问,那么我们本节就来研究一下一级缓存的本质大家可以这样想,上面我们一直提到一级缓存,那么提到一级缓存就绕不开SqlSession,所以索性我们就直接从SqlSession,看看有没有创建缓存或者与缓存有关的属性或者方法

调研了一圈,发现上述所有方法中,好像只有clearCache()和缓存沾点关系,那么就直接从这个方法入手吧,分析源码时,我们要看它(此类)是谁,它的父类和子类分别又是谁,对如上关系了解了,你才会对这个类有更深的认识,分析了一圈,你可能会得到如下这个流程图

 

再深入分析,流程走到Perpetualcache中的clear()方法之后,会调用其cache.clear()方法,那么这个cache是什么东西呢?点进去发现,cache其实就是private Map cache = new HashMap();也就是一个Map,所以说cache.clear()其实就是map.clear(),也就是说,缓存其实就是本地存放的一个map对象,每一个SqISession都会存放一个map对象的引用,那么这个cache是何时创建的呢?

你觉得最有可能创建缓存的地方是哪里呢?我觉得是Executor,为什么这么认为?因为Executor是 执行器,用来执行SQL请求,而且清除缓存的方法也在Executor中执行,所以很可能缓存的创建也很 有可能在Executor中,看了一圈发现Executor中有一个createCacheKey方法,这个方法很像是创 建缓存的方法啊,跟进去看看,你发现createCacheKey方法是由BaseExecutor执行的,代码如下

CacheKey cacheKey = new CacheKey(); 
//MappedStatement 的 id 
// id就是Sql语句的所在位置包名+类名+ SQL名称 cacheKey.update(ms.getId()); 
// offset 就是 0 
cacheKey.update(rowBounds.getOffset()); 
// limit 就是 
Integer.MAXVALUE cacheKey.update(rowBounds.getLimit()); 
//具体的SQL语句 
cacheKey.update(boundSql.getSql()); 
//后面是update 了 sql中带的参数 
cacheKey.update(value); ... 
if (configuration.getEnvironment() != null) { 
// issue #176 cacheKey.update(configuration.getEnvironment().getId()); 

创建缓存key会经过一系列的update方法,udate方法由一个CacheKey这个对象来执行的,这个update方法最终由updateList的list来把五个值存进去,对照上面的代码和下面的图示,你应该能 理解这五个值都是什么了

 

这里需要注意一下最后一个值,configuration.getEnvironment().getId()这是什么,这其实就是 定义在mybatis-config.xml中的标签,见如下。

 
  
   
    
               
                   
  

那么我们回归正题,那么创建完缓存之后该用在何处呢?总不会凭空创建一个缓存不使用吧?绝对不会的,经过我们对一级缓存的探究之后,我们发现一级缓存更多是用于查询操作,毕竟一级缓存也叫做查询缓存吧,为什么叫查询缓存我们一会儿说。我们先来看一下这个缓存到底用在哪了,我们跟踪到query方法如下:

//此方法在SimpleExecutor的父类BaseExecutor中实现
@Override
public  List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    //根据传入的参数动态获得SQL语句,最后返回用BoundSql对象表示
    BoundSql boundSql = ms.getBoundSql(parameter);
    //为本次查询创建缓存的Key
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    // 查询
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
​
 List list;
 try {
     // queryStack + 1
     queryStack++;
     // 从一级缓存中,获取查询结果
     list = resultHandler == null ? (List) localCache.getObject(key) : null;
     // 获取到,则进行处理
     if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
     // 获得不到,则从数据库中查询
     } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
     }
 } finally {
    queryStack--;
 }
 
 // 从数据库中读取操作
private  List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List list;
    // 在缓存中,添加占位对象。此处的占位符,和延迟加载有关,可见 `DeferredLoad#canLoad()` 方法
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
        // 执行读操作
        list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
   } finally {
        // 从缓存中,移除占位对象
         localCache.removeObject(key);
   }
   // 添加到缓存中
   localCache.putObject(key, list);
   // 暂时忽略,存储过程相关
   if (ms.getStatementType() == StatementType.CALLABLE) {
        localOutputParameterCache.putObject(key, parameter);
    }
    return list;
}

如果查不到的话,就从数据库查,在queryFromDatabase中,会对localcache进行写入。 localcache对象的put方法最终交给Map进行存放

private Map cache = new HashMap(); 
@Override 
public void putObject(Object key, Object value) { 
    cache.put(key, value); 
}

7.2 二级缓存

 

开启二级缓冲

和一级缓存默认开启不一样,二级缓存需要我们手动开启

首先在全局配置文件sqlMapConfig.xml文件中加入如下代码:



    

其次在UserMapper.xml文件中开启缓存

 

我们可以看到mapper.xml文件中就这么一个空标签,其实这里可以配置,PerpetualCache这个类是mybatis默认实现缓存功能的类。我们不写type就使用mybatis默认的缓存,也可以去实现Cache接口来自定义缓存。

 

public class PerpetualCache implements Cache { 
    private final String id; 
    private MapcObject, Object> cache = new HashMapC);
    public PerpetualCache(St ring id) {this.id = id;}
}

我们可以看到二级缓存底层还是HashMap结构

public class User implements Serializable( 
    //用户ID private int id; 
    //用户姓名 private String username; 
    //用户性别 private String sex; 
}

开启了二级缓存后,还需要将要缓存的pojo实现Serializable接口,为了将缓存数据取出执行反序列化操 作,因为二级缓存数据存储介质多种多样,不一定只存在内存中,有可能存在硬盘中,如果我们要再取 这个缓存的话,就需要反序列化了。所以mybatis中的pojo都去实现Serializable接口

测试

测试二级缓存和sqlSession无关

@Test
public void secondLevelCacheTest(){
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    SqlSession sqlSession3 = sqlSessionFactory.openSession();
    UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
    UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
    UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class);
    List u1 = mapper1.findUserAndOrder();
    sqlSession1.close();
    List u2 = mapper2.findUserAndOrder();
    System.out.println(u1 == u2);//false不是存储的地址
}

useCache和flushCache

mybatis中还可以配置userCache和flushCache等配置项,userCache是用来设置是否禁用二级缓 存的,在statement中设置useCache=false可以禁用当前select语句的二级缓存,即每次查询都会发出 sql去查询,默认情况是true,即该sql使用二级缓存

这种情况是针对每次查询都需要最新的数据sql,要设置成useCache=false,禁用二级缓存,直接从数 据库中获取。在mapper的同一个namespace中,如果有其它insert、update, delete操作数据后需要刷新缓 存,如果不执行刷新缓存会出现脏读。设置statement配置中的flushCache="true”属性,默认情况下为true,即刷新缓存,如果改成false则 不会刷新。使用缓存时如果手动修改数据库表中的查询数据会出现脏读。

一般下执行完commit操作都需要刷新缓存,flushCache=true表示刷新缓存,这样可以避免数据库脏读。所以我们不用设置,默认即可

7.3 二级缓存整合redis

mybaits本身二级缓冲不支持分布式。

7.3.1 pom


    org.mybatis.caches
    mybatis-redis
    1.0.0-beta2

7.3.2 redis.properties

redis.host=localhost
redis.port=6379
redis.connectionTimeout=5000
redis.password=
redis.database=0

7.3.3 UserMapper.xml


7.3.4 Test

@Test
public void secondLevelCacheTest(){
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    SqlSession sqlSession3 = sqlSessionFactory.openSession();
​
    UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
    UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
    UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class);
    sqlSession.close();     //关闭sqlSession1测试Redis缓冲是否成功
​
    List u1 = mapper1.findUserAndOrder();
    sqlSession1.close();    //调用tmc中的commit()方法将map中的事务放入2级缓冲对象中
    List u2 = mapper2.findUserAndOrder();
​
    System.out.println(u1 == u2);   //false不是存储的地址
}

7.3.5 源码分析

RedisCache和大家普遍实现Mybatis的缓存方案大同小异,无非是实现Cache接口,并使用jedis操作缓存;不过该项目在设计细节上有一些区别;

public final class RedisCache implements Cache {
    private final ReadWriteLock readWriteLock = new DummyReadWriteLock();
    private String id;
    private static JedisPool pool;
    public RedisCache(String id) {
        if (id == null) {
            throw new IllegalArgumentException("Cache instances require an ID");
        } else {
            this.id = id;
            RedisConfig redisConfig = RedisConfigurationBuilder.getInstance().parseConfiguration();
            pool = new JedisPool(redisConfig, 
            redisConfig.getHost(), 
            redisConfig.getPort(),        
            redisConfig.getConnectionTimeout(), 
            redisConfig.getSoTimeout(), 
            redisConfig.getPassword(), 
            redisConfig.getDatabase(), 
            redisConfig.getClientName());
        }
    }
}

RedisCache在mybatis启动的时候,由MyBatis的CacheBuilder创建,创建的方式很简单,就是调用RedisCache的带有String参数的构造方法,即RedisCache(String id);而在RedisCache的构造方法中,调用了 RedisConfigu rationBuilder 来创建 RedisConfig 对象,并使用 RedisConfig 来创建JedisPool。

RedisConfig类继承了 JedisPoolConfig,并提供了 host,port等属性的包装,简单看一下RedisConfig的属性:

public class RedisConfig extends JedisPoolConfig {
    private String host = "localhost";
    private int port = 6379;
    private int connectionTimeout = 2000;
    private int soTimeout = 2000;
    private String password;
    private int database = 0;
    private String clientName;
}

RedisConfig对象是由RedisConfigurationBuilder创建的,简单看下这个类的主要方法:

 public RedisConfig parseConfiguration(ClassLoader classLoader) {
        Properties config = new Properties();
        InputStream input = classLoader.getResourceAsStream(this.redisPropertiesFilename);
        if (input != null) {
            try {
                config.load(input);
            } catch (IOException var12) {
                throw new RuntimeException("An error occurred while reading classpath property '" + this.redisPropertiesFilename + "', see nested exceptions", var12);
            } finally {
                try {
                    input.close();
                } catch (IOException var11) {
                }
            }
        }
        RedisConfig jedisConfig = new RedisConfig();
        this.setConfigProperties(config, jedisConfig);
        return jedisConfig;
    }

核心的方法就是parseConfiguration方法,该方法从classpath中读取一个redis.properties文件:

redis.host=localhost
redis.port=6379
redis.connectionTimeout=5000
redis.password=
redis.database=0

并将该配置文件中的内容设置到RedisConfig对象中,并返回;接下来,就是RedisCache使用RedisConfig类创建完成edisPool;在RedisCache中实现了一个简单的模板方法,用来操作Redis:

private Object execute(RedisCallback callback) {
        Jedis jedis = pool.getResource();
        Object var3;
        try {
            var3 = callback.doWithRedis(jedis);
        } finally {
            jedis.close();
        }
        return var3;
}

模板接口为RedisCallback,这个接口中就只需要实现了一个doWithRedis方法而已:

public interface RedisCallback {
    Object doWithRedis(Jedis jedis); 
}

接下来看看Cache中最重要的两个方法:putObject和getObject,通过这两个方法来查看mybatis-redis储存数据的格式

public void putObject(final Object key, final Object value) {
        this.execute(new RedisCallback() {
            public Object doWithRedis(Jedis jedis) {
                jedis.hset(RedisCache.this.id.toString().getBytes(), key.toString().getBytes(), SerializeUtil.serialize(value));
                return null;
            }
        });
    }
​
public Object getObject(final Object key) {
        return this.execute(new RedisCallback() {
            public Object doWithRedis(Jedis jedis) {
                return SerializeUtil.unserialize(jedis.hget(RedisCache.this.id.toString().getBytes(), key.toString().getBytes()));
            }
        });
    }

可以很清楚的看到,mybatis-redis在存储数据的时候,是使用的hash结构,把cache的id作为这个hash 的key (cache的id在mybatis中就是mapper的namespace);这个mapper中的查询缓存数据作为hash的field,需要缓存的内容直接使用SerializeUtil存储,SerializeUtil和其他的序列化类差不多,负责对象的序列化和反序列化;

第八部分 Mybatis插件

8.1 插件原理

一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展。这样的好处是显而易见的,一是增加了框架的灵活性。二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作。以MyBatis为例,我们可基于MyBati s插件机制实现分页、分表,监控等功能。由于插件和业务 无关,业务也无法感知插件的存在。因此可以无感植入插件,在无形中增强功能

8.2 Mybatis插件介绍

Mybati s作为一个应用广泛的优秀的ORM开源框架,这个框架具有强大的灵活性,在四大组件(Executor、StatementHandler、ParameterHandler、ResultSetHandler)处提供了简单易用的插 件扩展机制。Mybatis对持久层的操作就是借助于四大核心对象。MyBatis支持用插件对四大核心对象进 行拦截,对mybatis来说插件就是拦截器,用来增强核心对象的功能,增强功能本质上是借助于底层的 动态代理实现的,换句话说,MyBatis中的四大对象都是代理对象

 

MyBatis所允许拦截的方法如下

  • 执行器Executor (update、query、commit、rollback等方法);

  • SQL语法构建器StatementHandler (prepare、parameterize、batch、updates query等方 法);

  • 参数处理器ParameterHandler (getParameterObject、setParameters方法);

  • 结果集处理器ResultSetHandler (handleResultSets、handleOutputParameters等方法);

8.3 插件原理

在四大对象创建的时候

  • 每个创建出来的对象不是直接返回的,而是interceptorChain.pluginAll(parameterHandler);

  • 获取到所有的Interceptor (拦截器)(插件需要实现的接口);调用 interceptor.plugin(target);返

    回 target 包装后的对象

  • 插件机制,我们可以使用插件为目标对象创建一个代理对象;AOP (面向切面)我们的插件可 以

    为四大对象创建出代理对象,代理对象就可以拦截到四大对象的每一个执行;

拦截

插件具体是如何拦截并附加额外的功能的呢?以ParameterHandler来说

8.3 自定义插件

8.4 插件源码分析

8.5 pageHelper分页插件

8.5.1 坐标


    com.github.pagehelper
    pagehelper
    3.7.5


    com.github.jsqlparser
    jsqlparser
    0.9.1

配置

8.6 通用mapper

坐标

配置

映射

第九部分 架构原理

9.1 架构设计

 

9.3 工作流程

 

第十部门 源码剖析

10.1 传统方式源码剖析

10.1.1 源码剖析初始化

Test类方法读取配置文件,加载文件流

//1.读取字节文件,转换蔚输入流,注意现在还没有解析
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
​
//2.解析配置文件,封装Configuration对象,创建DefaultSqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

进入SqlSessionFactoryBuilder类调用build()的重载方法

// 2.调用的重载方法
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
        // 创建 XMLConfigBuilder, XMLConfigBuilder是专门解析mybatis的配置文件的类
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        // 执行 XML 解析
        // 创建 DefaultSqlSessionFactory 对象
        return build(parser.parse());
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
        ErrorContext.instance().reset();
        try {
            inputStream.close();
        } catch (IOException e) {
            // Intentionally ignore. Prefer previous error.
        }
    }
}

进入XMLConfigBuilder的parse()方法解析xml并封装对象Configuration,Configuration对象属性与Xml配置是对应的

注意:在Myabtis初始化中会将Myabatis配置文件中的信息全部加载到内存中,使用

org.apache.ibatis.session.Configuration 实例来维护

/**
 * 解析 XML 成 Configuration 对象。
 *
 * @return Configuration 对象
 */
public Configuration parse() {
    // 若已解析,抛出 BuilderException 异常
    if (parsed) {
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    // 标记已解析
    parsed = true;
    ///parser是XPathParser解析器对象,读取节点内数据,是MyBatis配置文件中的顶层标签
    // 解析 XML configuration 节点
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
}
​
 /**
     * 解析 XML
     *
     * 具体 MyBatis 有哪些 XML 标签,参见 《XML 映射配置文件》http://www.mybatis.org/mybatis-3/zh/configuration.html
     *
     * @param root 根节点
     */
    private void parseConfiguration(XNode root) {
        try {
            //issue #117 read properties first
            // 解析  标签
            propertiesElement(root.evalNode("properties"));
            // 解析  标签
            Properties settings = settingsAsProperties(root.evalNode("settings"));
            // 加载自定义的 VFS 实现类
            loadCustomVfs(settings);
            // 解析  标签
            typeAliasesElement(root.evalNode("typeAliases"));
            // 解析  标签
            pluginElement(root.evalNode("plugins"));
            // 解析  标签
            objectFactoryElement(root.evalNode("objectFactory"));
            // 解析  标签
            objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
            // 解析  标签
            reflectorFactoryElement(root.evalNode("reflectorFactory"));
            // 赋值  到 Configuration 属性
            settingsElement(settings);
            // read it after objectFactory and objectWrapperFactory issue #631
            // 解析  标签
            environmentsElement(root.evalNode("environments"));
            // 解析  标签
            databaseIdProviderElement(root.evalNode("databaseIdProvider"));
            // 解析  标签
            typeHandlerElement(root.evalNode("typeHandlers"));
            // 解析  标签
            mapperElement(root.evalNode("mappers"));
        } catch (Exception e) {
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
    }

MappedStatement对象

与Mapper配置文件中的一个select/update/insert/delete节点相对应。

初始化Myabtis将MappedStatement对象封装在Configuration对象属性Map中

存储的key = 全限类名 + 方法名,通过key来找到对应的数据

/**
 * MappedStatement 映射
 *
 * KEY:`${namespace}.${id}`
 */
protected final Map mappedStatements = new StrictMap<>("Mapped Statements collection");

Test方法步骤2中调用build()重载方法

/**
 * 创建 DefaultSqlSessionFactory 对象
 *
 * @param config Configuration 对象
 * @return DefaultSqlSessionFactory 对象
 */
public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config); //构建者设计模式
}

10.1.2 源码剖析执行SQL流程

SqlSession它有两个实现类:DefaultSqlSession (默认)和SqlSessionManager (弃用,不做介绍)

SqlSession是Myabatis中用于和数据库交互的顶层类,通常将它和ThreadLocal绑定,一个会话使用一个sqlSeesion,使用完毕后调用close()方法关闭资源

public class DefaultSqlSession implements SqlSession { 
    private final Configuration configuration;  //配置信息封装的对象
    private final Executor executor;    //执行器
}

10.1.3 源码剖析executor

10.1.4 源码剖析StatementHandler

10.2 Mapper代理方式

10.2.1 源码剖析getmapper()

10.2.2 源码剖析invoke()

10.3 二级缓冲源码剖析

10.3.1 启动二级缓冲

开启全局二级缓冲配置

 
     

在需要二级缓冲的Mapper中配置标签

在具体的CURD标签上配置useCache=true

10.3.2 标签cache解析

(1)根据之前的mybatis源码剖析,xml的解析工作主要交给XMLConfigBuilder.parse()方法来实现

(2)先来看看是如何构建Cache对象的MapperBuilderAssistant.useNewCache()

我们看到一个Mapper.xml只会解析一次标签,也就是只创建一次Cache对象,放进configuration中,

(3)并将cache赋值给MapperBuilderAssistant.currentCache()

buildStatementFromContext(context.evalNodes("select|insert|update|delete"));

将Cache包装到MappedStatement

10.3.3 总结

在二级缓冲设计上,maybatis大量运用了装饰者模式,入CachingExecutor,以及各种Cache的装饰器。

二级缓冲实现了sqlsession之间的缓冲数据共享,属于namespace级别

二级缓冲具有丰富的缓冲策略

二级缓冲由多个装饰器,与基础缓冲组合而成

二级缓冲工作由一个缓冲装饰器CacheingExexutor和一个事务预警缓冲TransactionlCache完成

如果开启二级缓冲

查询方法先查看2级别缓冲中是否由数据,然后去以及缓冲中查找,都没有的话查询数据库

二级缓冲在调用tcm事务类中的commit方法才会生效,最初二级缓冲存放在一个map中,没有这个事务管理会产生桩读。

10.4 延迟加载源码剖析

10.4.1 什么是延迟加载

就是在需要用到的数据才进行加载,不需要用到数据的时候就不加载,延迟加载就是懒加载

注意:延迟加载是基于嵌套查询来实现的

一对多,多对多可以采用延迟加载

一对一(多对一)建议使用立即加载

Mybatis默认使用立即加载

10.4.2 局部延迟加载


fetchType=“lazy”    //延迟加载  //eager立即加载

10.4.3 全局延迟加载

 
     
     

10.4.4 延迟加载原理实现

它的原理是,使用 CGLIB 或 Javassist( 默认 ) 创建目标对象的代理对象。

当调用代理对象的延迟加载属性的 getting 方法时,进入拦截器方法invoke()。

总结:延迟加载主要是通过动态代理的形式实现,通过代理拦截到指定方法,执行数据加载。

10.4.5 延迟加载invoke()

判断是否延迟包含延迟加载属性

10.5 设计模式

10.5.1 Builder构建着模型

相对于工厂模型会产出一个完整的产品;Builder应用于更加复杂的对象构建,甚至只会构建产品的一部分

使用多个简单对象一步一步构建一个发杂的对象

创建一个简单对象

package com.topxin.constructor;
​
public class Computer {
    private String disPlayer;
    private String mainUnit;
    private String mouse;
    private String keyboard;
    ...get/set/toString
}

创建ComputerBuilder复杂类和getComputer()方法

package com.topxin.constructor;
​
public class ComputerBuilder {
​
    private Computer computer = new Computer();
​
    public void installDisPlayer(String disPlayer){
        computer.setDisPlayer(disPlayer);
    }
​
    public void installMainUnit(String mainUnit){
        computer.setMainUnit(mainUnit);
    }
​
    public void installKeyboard(String keyboard){
        computer.setKeyboard(keyboard);
    }
​
    public void installMouse(String mouse){
        computer.setMouse(mouse);
    }
​
    public Computer getComputer(){
        return computer;
    }
}

测试

package com.topxin.constructor;
​
public class ConstructorTest {
    public static void main(String[]args){
        ComputerBuilder computerBuilder = new ComputerBuilder();
        computerBuilder.installDisPlayer("显万器");
        computerBuilder.installMainUnit("主机");
        computerBuilder.installKeyboard("键盘");
        computerBuilder.installMouse("鼠标");
        Computer computer = computerBuilder.getComputer();
        System.out.println(computer);
    }
}

10.5.2 工厂模式

简单工厂类:可以根据参数的不同返回不同类的实例

构架抽象类

package com.topxin.simpleFactory;
​
public abstract class Computer {
    public abstract void start();
}

新建惠普类

package com.topxin.simpleFactory;
​
public class HpComputer extends  Computer {
    @Override
    public void start() {
        System.out.println("hp");
    }
}

理想类

package com.topxin.simpleFactory;
​
public class LenovoComputer extends Computer {
    @Override
    public void start() {
        System.out.println("lenovo");
    }
}

工厂类

package com.topxin.simpleFactory;
​
public class ComputerFactory {
    public static Computer createComputer(String type){
        Computer computer = null;
        switch (type){
            case "lenovo":
                computer = new LenovoComputer();
            case "hp":
                computer = new HpComputer();
        }
        return  computer;
    }
}
​

测试

通过工厂类createComputer可以创建不同类型的类

package com.topxin.simpleFactory;
​
public class SimpleFactoryTest {
    public static void main(String[] args) {
        Computer hp = ComputerFactory.createComputer("hp");
        hp.start();
    }
}

10.5.3 代理模式

Mybatis核心使用

接口

package com.topxin.dynamicprocy;
​
public interface Person {
    void doSomething();
}
​

具体实现类

package com.topxin.dynamicprocy;
​
public class Bob implements Person{
    @Override
    public void doSomething() {
        System.out.println("Bob doSomeThing");
    }
}

根据JDC动态代理

package com.topxin.dynamicprocy;
​
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
​
public class JDKDynamicProxy implements InvocationHandler {
​
    //声明被代理的对象
    private Person person;
​
    public JDKDynamicProxy(Person person) {
        this.person = person;
    }
​
    //获取代理对象
    public Object getTarget(){
        Object proxyInstance = Proxy.newProxyInstance(person.getClass().getClassLoader(), person.getClass().getInterfaces(), this);
        return proxyInstance;
    }
​
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("前置增强");
        Object invoke = method.invoke(person, args);
        System.out.println("后置增强");
        return invoke;
    }
}

测试

获取目标最终都会调用invoke()方法

package com.topxin.dynamicprocy;
​
public class ProxyTest {
    public static void main(String[] args) {
        Person person = new Bob();
        person.doSomething();
        Person proxy = (Person) new JDKDynamicProxy(new Bob()).getTarget();
        proxy.doSomething();
    }
}
​

10.6 异常日志输出

使用ThreadLocal来管理ErrorContext

10.7 动态sql源码剖析

第十一部分 Mybatis-Plus

11.1 什么是Mybatis-Plus

MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简

化开发、提高效率而生。

注意@TableName指定数据库表个实体类映射关系

11.2 mybatis+mp

11.3 mybatis+mp+spring

11.4 mybatis+mp+springboot

11.5

@TableName()

@TableName("数据库表名称")    //指定表映射不同

@TableId()

@TableId(type = IdType.AUTO) //指定id类型为自增长等6中策略

@TableFiled()字段

@TableFiled(select = false)     //不反悔,不查询改字段
@TableFiled(value = "数据库对应的字段") //字段映射名称不同
@TableFiled(exist = false)      //数据库中不存在

分页拦截器

package com.topxin; 
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; 
import org.mybatis.spring.annotation.MapperScan; 
import org.springframework.context.annotation.Bean; 
import org.springframework.context.annotation.Configuration;
@Configuration 
@MapperScan("com.lagou.mp.mapper") //设置mapper接口的扫描包 
public class MybatisPlusConfig { 
    /*** 分页插件 */
    @Bean 
    public PaginationInterceptor paginationInterceptor() {
        return new PaginationInterceptor(); 
    } 
}

第十二部分 Mybatis简答题

12.1

你可能感兴趣的:(源码,java,mybatis)