Mybatis实践笔记-搭建一个简易Mybatis

2020-08-29  本文已影响0人  叶子的翅膀

文章内容输出来源:拉勾教育Java高薪训练营

说明

通过分析使用原生JDBC操作存在的问题,带着这些问题的解决思路,结合Mybatis框架主流程,一步一步搭建一个简易版本。

一、数据准备

  1. 创建MYSQL数据库
DROP DATABASE IF EXISTS db_test;

CREATE DATABASE db_test DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
  1. 创建表、初始化一些数据
CREATE TABLE user(
 `id` int(10) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
 `name` varchar(200) NOT NULL COMMENT '名称',
  PRIMARY KEY(`id`)
)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_bin


insert into user(name) values('钱大'),('何二'),('张三'),('李四'),('王五');

二、项目准备

  1. 创建自定义框架的Maven项目simple_mybatis
  1. 创建测试Maven项目simple_mybatis_test

测试项目作为使用端,引入simple_mybatis的框架,可以通过创建DAO接口,实现数据的操作

三、分析问题

  1. 原生JDBC的查询
  1. 总结原生JDBC查询使用上的问题以及大概的解决思路

四、项目设计

设计思路

根据上面分析出来的问题,对简易版本框架的设计。

  1. 测试驱动开发,首先看下下测试项目需要如何设计
    作为使用端,引入了自定义的框架,希望可以这样使用:
  1. 框架端就应该能提供以下的功能

框架设计

五、项目实现

基于simple_mybatis项目

1. 读取并解析配置文件

1.1 读取工具类

public class Resources {
    public static InputStream getResourceAsStream(String path) {
        InputStream resourceAsStream = Resources.class.getClassLoader().getResourceAsStream(path);
        return resourceAsStream;
    }
}

1.2 创建配置类

public class MappedStatement {
    //标识ID
    private String id;
    //参数类型
    private String parameterType;
    //结果集类型
    private String resultType;
    //SQL语句
    private String sql;
      //ignore getter/setter
}

public class Configuration {
    //数据源
    private DataSource dataSource;

    //key为statementId,由mapper脚本文件的namespace和节点标签的id组成
    private Map<String, MappedStatement> mappedStatementMap = new HashMap<>();
    
    //ignore getter/setter
}

1.3 解析配置文件

<!---XML解析工具-->
<dependency>
    <groupId>dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>1.6.1</version>
</dependency>

<!---dom4j解析使用到的XPath-->
<dependency>
    <groupId>jaxen</groupId>
    <artifactId>jaxen</artifactId>
    <version>1.1.6</version>
</dependency>
public class XMLConfigBuilder {
    private Configuration configuration;

    public XMLConfigBuilder() {
        this.configuration = new Configuration();
    }

    // 使用dom4j对配置文件进行解析,封装Configuration
    public Configuration parseConfig(InputStream inputStream) throws DocumentException, PropertyVetoException {

        Document document = new SAXReader().read(inputStream);
       
        Element rootElement = document.getRootElement();
        
        //获取所有的属性值
        List<Element> list = rootElement.selectNodes("//property");
        Properties properties = new Properties();
        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<Element> mapperList = rootElement.selectNodes("//mapper");

        for (Element element : mapperList) {
            String mapperPath = element.attributeValue("resource");
            InputStream resourceAsSteam = Resources.getResourceAsSteam(mapperPath);
            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
            xmlMapperBuilder.parse(resourceAsSteam);
        }

        return configuration;
    }
}
public class XmlMapperBuilder {
    private Configuration configuration;

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

    //解析Mapper文件
    public void parse(InputStream inputStream) throws Exception{
        Document document = new SAXReader().read(inputStream);

        Element rootElement = document.getRootElement();

        String namespace = rootElement.attributeValue("namespace");

        List<Element> list = rootElement.selectNodes("select|insert|update|delete");


        for (Element element : list) {
            String id = element.attributeValue("id");
            String resultType = element.attributeValue("resultType");
            String parameterType = element.attributeValue("parameterType");
            String sqlText = element.getTextTrim();

            MappedStatement mappedStatement = new MappedStatement();
            mappedStatement.setId(id);
            mappedStatement.setResultType(resultType);
            mappedStatement.setParameterType(parameterType);
            mappedStatement.setSql(sqlText);

                        //唯一key
            String key = namespace + "." + id;
            configuration.getMappedStatementMap().put(key, mappedStatement);

        }
    }
}

2. 创建执行器

执行器主要负责SQL语句的生成、执行、结果映射

2.1 创建执行层接口:com.yyh.core.executor.Executor
增加一个查询数据的接口,查询数据就要知道执行哪一个SQL,SQL如果还有参数得传递相应的参数。所以接口方法参数要传入MappedStatement和params,因为需要将结果集封装到实体中返回,返回值就使用了泛型

public interface Executor {
    //查询集合数据
    <E> List<E> selectList(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception;
}

2.2 创建执行层接口实现类:com.yyh.core.executor.SimpleExecutor

public class SimpleExecutor implements Executor{
    @Override
    public <E> List<E> selectList(Configuration configuration,
                                  MappedStatement mappedStatement, Object... params)
            throws Exception {
        //获取连接
        Connection connection = configuration.getDataSource().getConnection();
        //获取sql
        String sql = mappedStatement.getSql();
        //对sql语句进行解析
        BoundSql boundSql = getBoundSql(sql);
        //获取预编译对象
        PreparedStatement statement = connection.prepareStatement(boundSql.getSqlText());

        //获取参数类型
        String parameterType = mappedStatement.getParameterType();
        Class<?> paramterTypeClass = getClassType(parameterType);
        List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();

        for (int i = 0; i < parameterMappingList.size(); i++) {
            ParameterMapping parameterMapping = parameterMappingList.get(i);
            String content = parameterMapping.getContent();
            if(null != paramterTypeClass) {
                if(paramterTypeClass == Integer.class) {
                    statement.setObject(i + 1, params[0]);

                }else {
                    Field declaredField = paramterTypeClass.getDeclaredField(content);
                    declaredField.setAccessible(true);
                    Object o = declaredField.get(params[0]);
                    statement.setObject(i + 1, o);
                }
            }
        }

        //执行sql
        ResultSet resultSet = statement.executeQuery();
        String resultType = mappedStatement.getResultType();
        Class<?> resultTypeClass = getClassType(resultType);
        List<Object> list = new ArrayList<>();

        while (resultSet.next()) {
            Object o = resultTypeClass.newInstance();
            ResultSetMetaData metaData = resultSet.getMetaData();
            //从1开始
            for (int i = 1; i <= metaData.getColumnCount(); i++) {
                //属性名
                String columnName = metaData.getColumnName(i);
                //属性值
                Object value = resultSet.getObject(columnName);
                //创建属性描述器,为属性生成读写方法
                PropertyDescriptor descriptor = new PropertyDescriptor(columnName, resultTypeClass);
                //获取写方法
                Method writeMethod = descriptor.getWriteMethod();
                //向类中写入值
                writeMethod.invoke(o, value);
            }

            list.add(o);
        }
        return (List<E>) list;
    }
    
    //获取参数的类型
    private Class<?> getClassType(String className) {
        if(null != className) {
            try {
                Class<?> aClass = Class.forName(className);
                return aClass;
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
    
    //解析SQL
    private BoundSql getBoundSql(String sql) {
        ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler();
        GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
        String parseSql = parser.parse(sql);
        BoundSql boundSql = new BoundSql();
        boundSql.setSqlText(parseSql);
        boundSql.setParameterMappingList(handler.getParameterMappings());
        return boundSql;
    }
}

getBoundSql方法就是对SQL进行解析的方法
使用了Mybatis提供的GenericTokenParserParameterMappingTokenHandler工具类进行解析

2.3 上一步的执行器实现类,需要对SQL语句进行解析
使用到了BoundSql类即com.yyh.core.pojo.BoundSql。如下

public class BoundSql {
    //解析后的SQL语句
    private String sqlText;
    //解析后的参数
    private List<ParameterMapping> parameterMappingList = new ArrayList<>();
    //ignore getter/setter
}

3. 创建会话层

3.1 创建会话层接口:com.yyh.core.session.SqlSession
接口主要是提供通用的查询、添加、编辑等接口,如下即查询列表数据的接口,传入相应的statementId以及对应的参数

public interface SqlSession {
//查询数据
<E> List<E> selectList(String statementId, Object... params) throws Exception;
}

3.2 创建会话层实现类:com.yyh.core.session.DefaultSession
实现类中对查询数据方法进行了实现,主要是获取到对应的MappedStatement对象,调用执行器Executor的查询数据方法

public class DefaultSqlSession implements SqlSession {
    private Configuration configuration;
    private Executor executor;

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

    //查询数据
    @Override
    public <E> List<E> selectList(String statementId, Object... params) throws Exception {
        MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
        return executor.selectList(configuration, mappedStatement, params);
    }
}    

3.2 创建会话层工厂接口:com.yyh.core.session.SqlSessionFactory

public interface SqlSessionFactory {
    /**
     * 开启一个session
     * @return
     */
    SqlSession openSession();
}

3.3 创建会话层创建工厂实现类:com.yyh.core.session.DefaultSqlSessionFactory

public class DefaultSqlSessionFactory implements SqlSessionFactory {
    private Configuration configuration;

    public DefaultSqlSessionFactory(Configuration configuration) {
        this.configuration = configuration;
    }
    //开启一个session
    @Override
    public SqlSession openSession() {
        return new DefaultSqlSession(configuration);
    }
}

3.4 创建会话层工厂的构造器:com.yyh.core.session.SqlSessionFactoryBuilder
主要用于对配置文件信息进行解析,创建核心配置类,从而去创建一个session工厂

public class SqlSessionFactoryBuilder {

    //构造工厂
    public SqlSessionFactory build(InputStream inputStream) throws Exception {
        //解析配置信息
        XmlConfigBuilder xmlConfigBuilder = new XmlConfigBuilder();
        Configuration configuration = xmlConfigBuilder.parseConfig(inputStream);

        DefaultSqlSessionFactory factory = new DefaultSqlSessionFactory(configuration);
        return factory;
    }
}

项目测试

基于simple_mybatis_test项目

1. 创建核心配置文件

resources目录下创建mybatis-config.xml。主要配置下数据源,配置下Mapper数据脚本的路径

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <dataSource>
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/db_test?characterEncoding=utf-8"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
    </dataSource>

    <mapper resource="mapper/UserMapper.xml"/>
</configuration>

2. 创建用户DAO接口

创建类com.yyh.dao.UserDao.java

public interface UserDao {
    /**
     * 查询数据列表
     * @param user
     * @return
     */
    List<UserEntity> selectList(UserEntity user);
}

3. 创建用户Mapper数据脚本

resources/mapper目录下创建UserMapper.xml

<?xml version="1.0" encoding="utf-8" ?>
<mapper namespace="com.yyh.dao.UserDao">
    <select id="selectList" parameterType="com.yyh.entity.UserEntity" resultType="com.yyh.entity.UserEntity">
        select * from user where name=#{name}
    </select>
</mapper>

4. 创建用户单元测试类

创建类com.yyh.test.SqlSessionUserTest

public class SqlSessionUserTest {
    private SqlSession sqlSession;
    @Before
    public void before() throws Exception {
        //1.读取配置文件
        InputStream stream = Resources.getResourceAsStream("mybatis-config.xml");
        //2.创建SqlSession工厂
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(stream);
        //3.打开session
        sqlSession = sqlSessionFactory.openSession();
    }

    @After
    public void after() {
        //5.关闭session
        sqlSession.close();
    }

    //测试查询用户
    @Test
    public void testSelectUserList() throws Exception {
        UserEntity user = new UserEntity();
        user.setName("张三");
        //4.调用session的查询数据方法获取数据
        List<UserEntity> users = sqlSession.selectList("com.yyh.dao.UserDao.selectList", user);

        Assert.checkNonNull(users);
    }
} 

项目代码

上一篇 下一篇

猜你喜欢

热点阅读