【手写MyBatis】(05)- SqlSession执行流程

文章目录

  • 手写MyBatis框架
    • Code:SqlSession执行流程
      • SqlSession接口
      • SqlSessionFactory
      • SqlSessionFactoryBuilder
      • Excutor (CachingExecutor、BaseExecutor)
      • SimpleExecutor
      • 完善:SqlSession内的方法执行
      • 完善:getBoundSql()
    • 测试:手写MyBatis框架select全执行流程
      • 测试准备
      • 测试结果:

手写MyBatis框架

Code:SqlSession执行流程

SqlSession接口

SqlSession接口的作用就是提供给程序员更加便捷的调用代码(JDBC操作)。
我们模拟两个接口,单条查询selectOne()与多条查询selectList()

/**
 * 表示一个sql会话,就是一次CRUD操作
 */
public interface SqlSession {
     

	<T> T selectOne(String statementId, Object param);

	<T> List<T> selectList(String statementId, Object param);
}

有了接口,我们就需要解决两个问题,一个是实现类怎么搞,另外一个是sqlsession会被调用很多次,而且它需要Configuration对象(通过构造方法来获取)。但是调用sqlSession的人,不需要关心Configuration对象的创建,同样也不需要持有这个对象。这个时候就可以考虑使用工厂模式来屏蔽SqlSession的构造细节。

SqlSessionFactory


/**
 * SqlSession默认实现类
 */
public class DefaultSqlSession implements SqlSession {
     

    private Configuration configuration;

    public DefaultSqlSession(Configuration configuration) {
     
        this.configuration = configuration;
    }

    @Override
    public <T> T selectOne(String statementId, Object param) {
     
        return null;
    }

    @Override
    public <T> List<T> selectList(String statementId, Object param) {
     
        return null;
    }
}

public interface SqlSessionFactory {
     

    SqlSession openSqlSession();
}

public class DefaultSqlSessionFactory implements SqlSessionFactory {
     

	// 等待注入
	private Configuration configuration;
	
	public DefaultSqlSessionFactory(Configuration configuration) {
     
		super();
		this.configuration = configuration;
	}

	@Override
	public SqlSession openSqlSession() {
     
		return new DefaultSqlSession(configuration);
	}

}

此时即使我们已经能够通过SqlSessionFactory去创建SqlSession从而达到隐藏SqlSession构建的详细内容,但是对于SqlSessionFactory我们还是要去通过构造方法得到Configuration对象,其实没有完全达到我们的要求。因此,考虑采用构建者模式的方式去生成SqlSessionFactory.

程序开发人员可以得到MyBatis全局配置文件,所以能够将全局配置文件转换为InputStream或者Reader对象,我们的构造者类可以用InputStream或者Reader作为入参。

最后,构造者类中再提供直接使用Configuration创建SqlSessionFactory的方式,使得不同的build方法可以得到共同的结果。

SqlSessionFactoryBuilder


public class SqlSessionFactoryBuilder {
     

    public SqlSessionFactory build(InputStream inputStream) {
     
        // 获取Configuration对象
        Document document = DocumentUtils.readDocument(inputStream);
        XMLConfigParser configParser = new XMLConfigParser();
        Configuration configuration = configParser.parse(document.getRootElement());
        return build(configuration);
    }

    // 也可以通过另外的入参来创建SqlSessionFactory
    public SqlSessionFactory build(Reader reader) {
     
        return null;
    }

    private SqlSessionFactory build(Configuration configuration) {
     
        return new DefaultSqlSessionFactory(configuration);
    }
}

以上整体的类关系如图:

【手写MyBatis】(05)- SqlSession执行流程_第1张图片

到此,我们就完成了sqlSession的创建,接下来集中于selectOne方法的实现。

Excutor (CachingExecutor、BaseExecutor)

在selectOne方法中,我们需要做的事情是:

  • 根据statementId获取MappedStatement对象,进而获取要执行的sql语句
  • 执行Statement的操作,执行的方式有多种:一种是带有二级缓存的执行方式,一种是基本执行方式(只带有一级缓存的,基本执行方式又分为几种:基本执行器、批处理执行器等)

因此,我们需要一个执行器接口,使用它的多个实现类来分别实现statement的操作。(此处可以考虑放到MappedStatement对象中,该对象中可以根据是否配置了二级缓存来确定创建的是哪个Executor。我们本次手写框架暂时不实现此功能)


public interface Executor {
     
    
    /**
     * 查询方法
     * @param mappedStatement 获取sql语句和入参出参等信息
     * @param configuration 获取数据源对象
     * @param param 入参对象
     * @return
     */
    <T> List<T> query(MappedStatement mappedStatement, Configuration configuration, Object param);
}

随后我们还需要两个Executor的实现类,分别实现二级缓存和一级缓存:

CachingExecutor


/**
 * 处理二级缓存
 */
public class CachingExecutor implements Executor {
     

    // 基本执行器
    private Executor delegate;

    public CachingExecutor(Executor delegate) {
     
        super();
        this.delegate = delegate;
    }

    @Override
    public <T> List<T> query(MappedStatement mappedStatement, Configuration configuration, Object param) {
     
        // 从二级缓存中根据sql语句获取处理结果(由于已经被弃用,暂不实现)

        // 如果有,则直接返回,如果没有则继续委托给基本执行器去执行
        return delegate.query(mappedStatement, configuration, param);
    }

}

对于基本执行器,核心是处理一级缓存和执行真正的查询方法。其中:

  • 一级缓存的实现是sql语句级别的缓存,因此我们需要建立一个Map来做一级缓存
  • 对于基本执行器的执行方式又分为很多种:简单执行、批处理执行…所以考虑使用抽象类的抽象方法实现执行操作

BaseExecutor

这里我们暂时还没有实现getBoundSql()的核心逻辑,我们放在最后实现。


/**
 * 基本执行器,主要处理一级缓存
 */
public abstract class BaseExecutor implements Executor {
     

	private Map<String, List<Object>> oneLevelCache = new HashMap<String, List<Object>>();

	@Override
	public <T> List<T> query(MappedStatement mappedStatement, Configuration configuration, Object param) {
     
		// 根据方法入参,从mappedStatement中动态获取(动态处理#{})带有值的sql语句
		String sql = mappedStatement.getSqlSource().getBoundSql(param).getSql();
		// 从一级缓存去根据sql语句获取查询结果
		List<Object> result = oneLevelCache.get(sql);
		if (result != null) {
     
			return (List<T>) result;
		}
		// 如果没有结果,则调用相应的处理器去处理
		result = queryFromDataBase(mappedStatement, configuration, param);
		// 回种到一级缓存中
		oneLevelCache.put(sql, result);
		return (List<T>) result;
	}

    // 真正的抽象查询方法
	public abstract List<Object> queryFromDataBase(MappedStatement mappedStatement, Configuration configuration,
			Object param);

}

以上的整体类关系如图:

【手写MyBatis】(05)- SqlSession执行流程_第2张图片
最后,我们要进入真正的JDBC执行了,编写基本执行器的子类,来最终实现queryFromDataBase方法:

SimpleExecutor

JDBC的主要操作在之前的测试代码中讲到过,简化下来为:

  • 获取连接
  • 获取sql语句
  • 创建Statement
  • 设置参数
  • 执行Statement
  • 处理结果

我们编写主要的执行顺序代码:


/**
 * 普通执行JDBC程序
 * @author JeffOsmond
 */
public class SimpleExecutor extends BaseExecutor {
     

    @Override
    public List<Object> queryFromDataBase(MappedStatement mappedStatement, Configuration configuration, Object param) {
     
        List<Object> results = new ArrayList<Object>();

        try {
     
            // 获取连接
            Connection connection = getConnection(configuration);
            // 获取sql语句
            BoundSql boundSql = getBoundSql(mappedStatement.getSqlSource(), param);

            String statementType = mappedStatement.getStatementType();

            // 可以使用mybatis的四大组件来优化
            if ("prepared".equals(statementType)) {
     
                // 创建Statement
                PreparedStatement statement = createStatement(connection, boundSql.getSql());
                // 设置参数
                handleParameter(statement, boundSql, param);
                // 执行Statement
                ResultSet resultSet = statement.executeQuery();
                // 处理结果
                handleResult(resultSet, mappedStatement, results);
            }
        } catch (Exception e) {
     
            e.printStackTrace();
        }
        return results;
    }
}

接下来完善每一步的具体操作:

获取连接getConnection()

我们在配置文件读取的时候,将数据库连接信息已经封装到DataSource中,所以可以通过DataSource类的内置方法获取数据库连接:


public class SimpleExecutor extends BaseExecutor {
     
    
    ...
    
    private Connection getConnection(Configuration configuration) throws Exception {
     
		DataSource dataSource = configuration.getDataSource();
		Connection connection = dataSource.getConnection();
		return connection;
	}
}

获取sql语句 getBoundSql()

一个Mapper.xml对应一个MappedStatement,一个Mapper.xml中的标签对应一个SqlSource,因此,我们可以通过SqlSource类获取到真正要执行的SQL语句:

public class SimpleExecutor extends BaseExecutor {
     
    
    ...

    private BoundSql getBoundSql(SqlSource sqlSource, Object param) {
     
	    BoundSql boundSql = sqlSource.getBoundSql(param);
	    return boundSql;
    }
   
}

创建Statement createStatement()

public class SimpleExecutor extends BaseExecutor {
     
    
    ...
    
    private PreparedStatement createStatement(Connection connection, String sql) throws Exception {
     
        PreparedStatement prepareStatement = connection.prepareStatement(sql);
        return prepareStatement;
    }
}

处理参数 handleParameter()

这里面要操作的就是将已经仅包含?的sql进行处理,把?替换成方法入参中的具体参数:
select username from user where id = ? -> select username from user where id = 1

public class SimpleExecutor extends BaseExecutor {
     
    
    ...
    
    private void handleParameter(PreparedStatement statement, BoundSql boundSql, Object param) throws Exception {
     
        // 判断入参的类型,如果是简单类型,直接处理
        if (param instanceof Integer) {
     
            statement.setObject(1, Integer.parseInt(param.toString())); // 这里为了方便,直接采用setObject,而非setInt
        } else if (param instanceof String) {
     
            statement.setObject(1, param.toString());
        } else {
     
            // 如果是POJO类型,则根据参数信息里面的参数名称,去入参对象中获取对应的参数值
            // 获取参数集合信息(#{}处理之后得到的参数信息)
            List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
            for (int i = 0; i < parameterMappings.size(); i++) {
     
                Object valueToUser = null;
                ParameterMapping parameterMapping = parameterMappings.get(i);
                // #{}中的参数名称,也应该和POJO类型中的属性名称一直
                String name = parameterMapping.getName();
                // 使用反射获取指定name的值
                Class<?> clazz = param.getClass();
                // 获取指定名称的属性对象
                Field field = clazz.getDeclaredField(name);
                field.setAccessible(true);
                valueToUser = field.get(param);

                statement.setObject(i + 1, valueToUser);
            }
        }
    }
}

处理结果映射 handleResult()

我们通过JDBC执行sql语句,得到了ResultSet,接下来就需要讲ResultSet里面的数据映射成Mapper.xml文件中配置的resultTye类型。

public class SimpleExecutor extends BaseExecutor {
     
    
    ...
    
    private void handleResult(ResultSet rs, MappedStatement mappedStatement, List<Object> results) throws Exception {
     
        // 从结果集中一行一行的取数据
        // 每一行数据,再一列一列的取数据(包括列的名称和列的值)
        // 最终将获取到的每一列的值都映射到目标对象的指定属性中(列的名称和属性名称要一致)
        Class<?> resultTypeClass = mappedStatement.getResultTypeClass();
        while (rs.next()) {
     
            // 要映射的结果目标对象
            Object result = resultTypeClass.newInstance();
            // 获取结果集的元数据(目的是取列的信息)
            ResultSetMetaData metaData = rs.getMetaData();
            int columnCount = metaData.getColumnCount();
            for (int i = 0; i < columnCount; i++) {
     
                String columnName = metaData.getColumnName(i + 1);
                Field field = resultTypeClass.getDeclaredField(columnName);
                field.setAccessible(true);
                field.set(result, rs.getObject(columnName));
            }

            results.add(result);
        }
    }
}


完善:SqlSession内的方法执行

有了前面编写的Executor,我们就可以调用executor进行selectOne与selectList操作了:


public class DefaultSqlSession implements SqlSession {
     

    private Configuration configuration;

    public DefaultSqlSession(Configuration configuration) {
     
        this.configuration = configuration;
    }

    @Override
    public <T> T selectOne(String statementId, Object param) {
     
        List<Object> list = this.selectList(statementId, param);
        if (list == null || list.size() == 0) {
     
            return null;
        } else if (list.size() == 1) {
     
            return (T) list.get(0);
        } else {
     
            throw new RuntimeException("只能返回一个对象");
        }
    }

    @Override
    public <T> List<T> selectList(String statementId, Object param) {
     
        // 根据statementId获取MappedStatement对象
        MappedStatement mappedStatement = configuration.getMappedStatementById(statementId);
        // 调用二级缓存执行器执行查询 (二级缓存CachingExecutor -> 一级缓存BaseExecutor -> 基础执行器SimpleExecutor)
        Executor executor = new CachingExecutor(new SimpleExecutor());
        return executor.query(mappedStatement, configuration, param);
    }

}

到此,我们已经完成了全部的sql语句的配置文件加载以及执行器执行流程。

完善:getBoundSql()

getBoundSql()方法是SqlSource接口的方法。用于获取处理完的sql语句。SqlSource接口有两个主要的实现类:DynamicSqlSource、RawSqlSource.这两个类的处理逻辑都一样,只不过处理的时机不同。

对于整体的处理逻辑为:将DynamicSqlSource、RawSqlSource处理成StaticSqlSource.StaticSqlSource就是最终我们可以在jdbc程序中使用的sql+?参数映射。我们借助另外一个类来实现这种操作。

对于单独的处理逻辑为:

  • DynamicSqlSource:包含带有KaTeX parse error: Expected 'EOF', got '#' at position 22: …ql标签的sql语句,可能包含#̲{},因此需要先处理{}和动态标签,最后处理#{}
  • RawSqlSource: 只包含#{},只需要处理#{}就可以了。

处理过程示意图:

【手写MyBatis】(05)- SqlSession执行流程_第3张图片

SqlSourceParser

该类主要用于处理#{},处理手段与之前的${}类似


public class SqlSourceParser {
     

    public SqlSource parse(String sqlText) {
     
        ParameterMappingTokenHandler tokenHandler = new ParameterMappingTokenHandler();
        GenericTokenParser tokenParser = new GenericTokenParser("#{", "}", tokenHandler);
        // tokenParser.parse(sqlText)参数是未处理的,返回值是已处理的(没有${}和#{})
        String sql = tokenParser.parse(sqlText);
        return new StaticSqlSource(sql, tokenHandler.getParameterMappings());
    }
}

public class ParameterMappingTokenHandler implements TokenHandler {
     
	private List<ParameterMapping> parameterMappings = new ArrayList<>();

	// context是参数名称
	@Override
	public String handleToken(String content) {
     
		parameterMappings.add(buildParameterMapping(content));
		return "?";
	}

	private ParameterMapping buildParameterMapping(String content) {
     
		ParameterMapping parameterMapping = new ParameterMapping(content);
		return parameterMapping;
	}

	public List<ParameterMapping> getParameterMappings() {
     
		return parameterMappings;
	}

	public void setParameterMappings(List<ParameterMapping> parameterMappings) {
     
		this.parameterMappings = parameterMappings;
	}

}

RawSqlSource

注意:之前的步骤里,我们在RawSqlSource中定义了sqlNode,现在将其去掉,变为SqlSource.


public class RawSqlSource implements SqlSource {
     

	private SqlSource sqlSource;

	public RawSqlSource(SqlNode rootSqlNode) {
     
		DynamicContext context = new DynamicContext(null);
		rootSqlNode.apply(context);
		// 在这里要先对sql节点进行解析
		SqlSourceParser sqlSourceParser = new SqlSourceParser();
		sqlSource = sqlSourceParser.parse(context.getSql());
	}

	@Override
	public BoundSql getBoundSql(Object param) {
     
		// 从staticSqlSource中获取相应信息
		return sqlSource.getBoundSql(param);
	}
}

DynamicSqlSource


public class DynamicSqlSource implements SqlSource {
     

	private SqlNode rootSqlNode;

	public DynamicSqlSource(MixedSqlNode rootSqlNode) {
     
		this.rootSqlNode = rootSqlNode;
	}

	/**
	 * 在sqlsession执行的时候,才调用该方法
	 */
	@Override
	public BoundSql getBoundSql(Object param) {
     
		//首先先调用SqlNode的处理,将动态标签和${}处理一下
		DynamicContext context = new DynamicContext(param);
		rootSqlNode.apply(context);

		// 再调用SqlSourceParser来处理#{}
		SqlSourceParser sqlSourceParser = new SqlSourceParser();
		SqlSource sqlSource = sqlSourceParser.parse(context.getSql());
		return sqlSource.getBoundSql(param);
	}

}

测试:手写MyBatis框架select全执行流程

测试准备

在前面配置文件加载的测试项目中添加:

【手写MyBatis】(05)- SqlSession执行流程_第4张图片

po类:User

public class User {
     

    private Integer id;
    private String username;
    private Date birthday;
    private String sex;
    private String address;

    // 省略get/set方法

    @Override
    public String toString() {
     
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", birthday=" + birthday +
                ", sex='" + sex + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
}

dao接口:UserDao


public interface UserDao {
     

    /**
     * 根据用户Id查询用户信息
     * @param param
     * @return
     */
    User queryUserById(User param);
}

dao实现类:UserDaoImpl


public class UserDaoImpl implements UserDao {
     

    private SqlSessionFactory sqlSessionFactory;

    public UserDaoImpl(SqlSessionFactory sqlSessionFactory) {
     
        this.sqlSessionFactory = sqlSessionFactory;
    }

    public User queryUserById(User param) {
     
        SqlSession sqlSession = sqlSessionFactory.openSqlSession();
        return sqlSession.selectOne("test.findUserById",param);
    }
}

全局配置文件:SqlMapConfig.xml

<configuration>
    
    <environments default="dev">
        <environment id="dev">
            
            <dataSource type="DBCP">
                <property name="driver" value="com.mysql.jdbc.Driver" />
                <property name="url" value="jdbc:mysql://192.168.152.100:3306/ssm" />
                <property name="username" value="root" />
                <property name="password" value="root" />
            dataSource>
        environment>
    environments>
    
    <mappers>
        
        <mapper resource="mapper/UserMapper.xml" />
    mappers>
configuration>

UserMapper.xml

<mapper namespace="test">
	
	<select id="findUserById"
			parameterType="com.osmond.mybatis.po.User"
			resultType="com.osmond.mybatis.po.User"
			statementType="prepared">

		SELECT * FROM user WHERE id = #{id} AND username like '%${username}'
		<if test="username != null and username !='' ">
			AND username like '%${username}'
			<if test="username != null and username !=''">
				AND 1=1
			if>
		if>
	select>
mapper>

测试类:UserDaoTest


public class UserDaoTest {
     

    @Test
    public void testQueryUserById() {
     
        String resource = "SqlMapConfig.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        // SqlSessionFactory的创建可能有几种创建方式,但是我还是不想要知道SqlSessionFactory的构造细节
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        UserDao userDao = new UserDaoImpl(sqlSessionFactory);
        User param = new User();
        param.setId(1);
        User user = userDao.queryUserById(param);
        System.out.println(user);
    }
}

测试结果:

【手写MyBatis】(05)- SqlSession执行流程_第5张图片

你可能感兴趣的:(MyBatis,笔记,mybatis)