Mybatis实践笔记-写一个简易Mybatis

文章内容输出来源:拉勾教育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
  • pom.xml配置
    <project>
        <groupId>com.yyh</groupId>
        <artifactId>simple_mybatis</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
            <java.version>1.8</java.version>
            <maven.compiler.source>1.8</maven.compiler.source>
            <maven.compiler.target>1.8</maven.compiler.target>
        </properties>
    
         <dependencies>
            <!---MYSQL 驱动-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>5.1.47</version>
            </dependency>
        </dependencies>
    </project>
    
  1. 创建测试Maven项目simple_mybatis_test

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

  • com.yyh.entity包下创建UserEntity实体,对应于数据表user
    public class UserEntity {
        private Integer id;
        private String name;
    
        public UserEntity() {}
    
        public UserEntity(Integer id, String name) {
            this.id = id;
            this.name = name;
        }
        //ignore getter/setter/toString
    }
    
  • pom.xml配置
    <project>
        <groupId>com.yyh</groupId>
        <artifactId>simple_mybatis_test</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
            <java.version>1.8</java.version>
            <maven.compiler.source>1.8</maven.compiler.source>
            <maven.compiler.target>1.8</maven.compiler.target>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>com.yyh</groupId>
                <artifactId>simple_mybatis</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
    
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.12</version>
            </dependency>
        </dependencies>
    </project>
    

三、分析问题

  1. 原生JDBC的查询
  • 在测试项目中创建类com.yyh.test.JdbcDemo,对用户表进行查询,查询张三的用户信息

    • 加载驱动
    • 创建连接
    • 获取SQL查询语句的预编译statement,设置参数
    • 执行查询操作
    • 处理结果集
  • 代码如下

      Connection connection = null;
      PreparedStatement preparedStatement = null;
      ResultSet resultSet = null;
      List<UserEntity> list = new ArrayList<>();
    
      try {
          //加载数据库驱动
          Class.forName("com.mysql.jdbc.Driver");
          //获取数据库连接
          connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/db_test?characterEncoding=utf-8",
                  "root", "password");
    
          //获取预处理statement
          preparedStatement = connection.prepareStatement("select * from user where name=?");
          //设置参数,从1开始
          preparedStatement.setString(1, "张三");
    
          //执行查询操作
          resultSet = preparedStatement.executeQuery();
          //处理结果集
          while (resultSet.next()) {
              int id = resultSet.getInt("id");
              String username = resultSet.getString("name");
              UserEntity userEntity = new UserEntity();
              list.add(new UserEntity(id, username));
          }
    
    
      } catch (Exception e) {
          e.printStackTrace();
      }finally {
          //... 省略关闭资源操作
      }
    
      //遍历查询到的用户
      if(null != list && list.size() > 0) {
          for (UserEntity userEntity : list) {
              System.out.println(userEntity.toString());
          }
      }
    
  1. 总结原生JDBC查询使用上的问题以及大概的解决思路
  • 数据库连接频繁创建、关闭,消耗资源
    • 使用连接池
  • 代码繁琐,不易于复用(需要加载驱动、创建连接、生成statement)
    • 对底层细节进行封装
  • 数据库配置、数据脚本与代码紧耦合,如果脚本比较复杂,不易维护
    • 增加配置文件、脚本文件,与代码分离
  • prepareStatement的参数设置需要手动一一对照顺序设值,如果SQL条件多或者条件复杂,不易维护
    • 反射,解析参数实体
  • 需要手动解析结果集,对返回对象进行一一设值
    • 反射,将数据库记录封装成pojo返回

四、项目设计

设计思路

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

  1. 测试驱动开发,首先看下下测试项目需要如何设计
    作为使用端,引入了自定义的框架,希望可以这样使用:
  • 有一个配置文件可以对数据源进行维护
  • 将相关的查询、更新、添加等SQL代码放到数据脚本文件中进行维护,与代码分离
  • 编写接口方法,业务方可以直接调用接口方法就可以实现数据的操作
  1. 框架端就应该能提供以下的功能
  • 读取配置文件、数据脚本文件,然后进行解析
    (1)创建数据源
    (2)创建Sql脚本的对象,能将接口方法与对应的Sql脚本进行映射
  • 获取数据源,打开连接
  • 创建与JDBC的交互,暴露接口方便外部的调用
框架设计
  • 基础实体类

    • MappedStatement
    1. Sql映射对象。对mapper文件的每一个节点(insert/select/update/delete)的封装。
    2. 包括了标识ID(由mapper文件的namespace和节点的id组合而成)、参数类型、结果类型、SQL
    • 核心配置类:Configuration

    存储数据源、以及所有扫描解析到的Sql映射对象

    • BoundSql

    标识解析后生态生成的SQL以及参数信息

  • 读取配置文件

    • 资源读取:Resources

    将配置文件加载为字节输入流,存储到内存中

  • 解析配置文件

    • 使用dom4j对配置文件进行解析,分为两部分

    (1)对mybatis核心配置文件的解析:XmlConfigBuilder
    (2)对mapper文件的解析:XmlMapperBuilder

  • Session会话层

    • SqlSession接口,以及对应的实现类DefaultSqlSession

    提供SQL操作接口方法(调用执行器进行具体SQL执行操作),供外部调用

    • SqlSessionFactory接口,以及对应的实现类DefaultSqlSessionFactory

    通过工厂模式进行SqlSession的创建

    • SqlSessionFactoryBuilder构造器

    读取配置文件信息、进行解析、创建Session工厂

  • 执行器

    • Executor接口,以及对应的实现类SimpleExecutor

    获取连接,解析SQL参数、创建预编译对象,执行SQL请求、解析映射结果集

五、项目实现

基于simple_mybatis项目

1. 读取并解析配置文件

1.1 读取工具类

  • 创建com.yyh.core.io.Resources
    读取文件加载为字节流,存储到内存中
public class Resources {
    public static InputStream getResourceAsStream(String path) {
        InputStream resourceAsStream = Resources.class.getClassLoader().getResourceAsStream(path);
        return resourceAsStream;
    }
}

1.2 创建配置类

  • 创建com.yyh.pojo.MappedStatement
    此类用于记录mapper数据脚本中的每一个节点(select|update|delete|insert)的SQL信息
public class MappedStatement {
    //标识ID
    private String id;
    //参数类型
    private String parameterType;
    //结果集类型
    private String resultType;
  	//SQL语句
	private String sql;
	  //ignore getter/setter
}

  • 创建com.yyh.pojo.Configuration
    此类用于加载数据源以及所有的mapper数据脚本的SQL信息
public class Configuration {
    //数据源
    private DataSource dataSource;

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

1.3 解析配置文件

  • 引入dom4j依赖
<!---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>
  • 创建核心配置文件的解析类:com.yyh.core.xml.XMLConfigBuilder
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;
    }
}
  • 创建mapper数据文件的解析类:com.yyh.core.xml.XmlMapperBuilder
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);
    }
} 

项目代码

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值