Mybatis学习笔记(二)-自定义一个简单版本的mybatis

Scroll Down

Mybatis学习笔记

JDBC问题分析

传统JDBC代码;

    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        
        Class.forName("com.mysql.jdbc.Driver");
        String url = "jdbc:mysql://localhost:3306/learn_mybatis?characterEncoding=utf-8";
        String userName = "root";
        String password = "mysql";
        Connection connection = DriverManager.getConnection(url, userName, password);
        String sql = "select * from user where id = ?";
        PreparedStatement pstmt = connection.prepareStatement(sql);
        pstmt.setString(1, "1");
        ResultSet resultSet = pstmt.executeQuery();
        List<User> userList = new ArrayList<User> (10);  
        while (resultSet.next()) {
            int id = resultSet.getInt("id");
            String name = resultSet.getString("username");
            User user = new User();
            user.setId(id);
            user.setUsername(name);
            userList.add(user);
        }
        System.out.println(userList);
    }

使用JDBC存在的问题分析:

  • 数据库连接创建,释放频繁造成系统资源浪费,影响系统性能,连接数据库存在重复代码,导致重复编码
  • sql语句在代码中存在硬编码,导致代码不易维护,实际应用中sql变化的可能性较大,sql改动需要改动java代码
  • 使用PreparedStatement也存在硬编码的问题,导致代码不易维护
  • 对结果集的解析也存在硬编码的问题,sql变化也会导致解析代码变化,系统不容易维护,如果每次返回成pojo对象解析比较方便

JDBC问题解决方案

解决方案:

  • 使用连接池进行连接数据库,并把连接数据库的硬编码配置写入配置文件
  • sql语句也使用配置文件进行配置
  • 使用反射或者内省自动对结果集进行返回

自定义Mybatis框架设计

使用端:

  • 提供核心配置sqlMapConfig.xml文件,配置数据源信息以及引入sql配置文件
  • 提供所有的Sql配置文件 mapper.xml文件

框架端:

image-20200426112403919

自定义Mybaits框架代码实战

使用端

配置文件:

使用端首先需要提供一个核心的配置文件,并引入相应的配置文件sqlMapConfig.xml

<configuration>
    <dataSource>
        <property name = "driver">com.mysql.cj.jdbc.Driver</property>
        <property name = "url">jdbc:mysql://localhost:3306/learn_mybatis</property>
        <property name = "user">root</property>
        <property name = "password">mysql</property>
    </dataSource>
    <mapper>userMapper.xml</mapper>
</configuration>

相应的userMapper.xml文件内容,需要根据namespace的值进行与接口绑定

<mapper namespace="com.lagou.mapper.IUserMapper">
    
    <select id="selectList" resultType = "com.lagou.pojo.User">
        select id,username,password,birthday from user
    </select>

    <select id="selectOne" resultType = "com.lagou.pojo.User" paramType = "com.lagou.pojo.User">
        select id,username from user where id = #{id} and username = #{username}
    </select>
    
    <insert id = "insertUser" resultType = "int" paramType = "com.lagou.pojo.User">
        insert into user(id,username,password,birthday) values(#{id},#{username},#{password},#{birthday})
    </insert>

    <update id = "updateUser" resultType = "int" paramType = "com.lagou.pojo.User">
        update user set username = #{username},password = #{password},birthday = #{birthday}
        where id = #{id}
    </update>
    
    <delete id = "deleteUser" resultType = "int" paramType = "com.lagou.pojo.User">
        delete from user where id = #{id}
    </delete>
</mapper>

测试类:

public class TestMybatis {
    private IUserMapper userMapper;
    
    @Before
    public void before() throws Exception {
        InputStream resourceAsSteam = Resource.getInputStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactroyBuilder().build(resourceAsSteam);
        Sqlsession sqlsession = sqlSessionFactory.openSession();
        userMapper = sqlsession.getMapper(IUserMapper.class);
    }

    /**
     * 测试查询
     */
    @Test
    public void queryAll() {
        List<User> userList = userMapper.selectList();
        System.out.println(userList);
    }

    /**
     * 测试插入
     */
    @Test
    public void insert() {
        User user = new User();
        user.setId(4);
        user.setUsername("测试插入");
        user.setPassword("123456");
        user.setBirthday("2020-04-20");
        System.out.println("插入记录前!");
        int num = userMapper.insertUser(user);
        System.out.println("成功插入"+num+"条记录");
    }
    @Test
    public void update(){
        User user = new User();
        user.setId(4);
        user.setUsername("修改测试");
        user.setPassword("654321");
        user.setBirthday("2020-04-27");
        System.out.println("修改记录前!");
        int num = userMapper.updateUser(user);
        System.out.println("成功修改"+num+"条记录");
    }
    @Test
    public void delete() {
        User user = new User();
        user.setId(4);
        System.out.println("删除记录前!");
        int num = userMapper.deleteUser(user);
        System.out.println("成功删除"+num+"条记录");
    }
    @Test
    public void testAllMethod() {
        System.out.println("-------------------------第一次查询所有数据!");
        queryAll();
        insert();
        System.out.println("-------------------------插入一条数据后查询所有数据!");
        queryAll();
        update();
        System.out.println("-------------------------修改一条数据后查询所有数据!");
        queryAll();
        delete();
        System.out.println("-------------------------删除一条数据后查询所有数据!");
        queryAll();
    }
   
}

框架端

解析配置文件:

Configuration类:在mybatis中的配置是ALL IN ONE,所有的信息都在Configuration类中,所以这里我们也使用此种方式实现,声明一个Configuration类,存储相应的信息

public class Configuration {

    /**
     * 数据源配置
     */
    private DataSource source;

    /**
     * 将所有的Sql配置都缓存在该map中
     */
    private Map<String, MapperStatement> mapperStatementMap = new HashMap<>();
}

MapperStatement:存储配置文件中sql的详细信息

public class MapperStatement {
    /**
     * 相应SQL的id,id=nameSpace+Sql的id
     */
    private String id;

    /**
     * 返回类型类名称
     */
    private String resultType;

    /**
     * 参数类名称
     */
    private String paramType;

    /**
     * 未转换前JDBC的sql
     */
    private String sql;

    /**
     * 该SQL的类型,是否是增删改查
     */
    private SqlCommandType sqlCommandType;
}

SqlSessionFactroyBuilder类:解析相应的XML文件到Configuration类中

public class SqlSessionFactroyBuilder {
    private Configuration configuration;

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


    public  SqlSessionFactory build(InputStream inputStream) throws DocumentException, PropertyVetoException {
        Document document = new SAXReader().read(inputStream);
        Element rootElement = document.getRootElement();
        Element dataSource = rootElement.element("dataSource");
        List<Element> datasourceList = dataSource.selectNodes("//property");
        Properties properties = new Properties();
        datasourceList.forEach(item ->{
            properties.setProperty(item.attributeValue("name"), item.getTextTrim());
        });
        ComboPooledDataSource comboPooledDataSource = new
                ComboPooledDataSource();
        comboPooledDataSource.setDriverClass(properties.getProperty("driver"));
        comboPooledDataSource.setJdbcUrl(properties.getProperty("url"));
        comboPooledDataSource.setUser(properties.getProperty("user"));
        comboPooledDataSource.setPassword(properties.getProperty("password"));
        configuration.setSource(comboPooledDataSource);
        List<Element> mapperList = rootElement.selectNodes("//mapper");
        for (Element element : mapperList) {
            String textTrim = element.getTextTrim();
            InputStream mapperInputStream = Resource.getInputStream(textTrim);
            Document doc = new SAXReader().read(mapperInputStream);
            Element mapperRootElement = doc.getRootElement();
            String namespace = mapperRootElement.attributeValue("namespace");
            List<Element> selectList = mapperRootElement.selectNodes("select|insert|update|delete");
            for (Element selectNode : selectList) {
                MapperStatement mapperStatement = new MapperStatement();
                String id = selectNode.attributeValue("id");
                String paramType = selectNode.attributeValue("paramType");
                String resultType = selectNode.attributeValue("resultType");
                String sql = selectNode.getTextTrim();
                String key = namespace + "." + id;
                mapperStatement.setResultType(resultType);
                mapperStatement.setId(id);
                mapperStatement.setSql(sql);
                mapperStatement.setParamType(paramType);
                String nodeTypeName = selectNode.getName();             
 //设置sql的类型,"select|insert|update|delete"           
                mapperStatement.setSqlCommandType(SqlCommandType.valueOf(nodeTypeName.toUpperCase(Locale.ENGLISH)));
                configuration.getMapperStatementMap().put(key, mapperStatement);
            }
            
            
        }
        return new DefaultSqlSessionFactory(configuration);
    }

定义SqlSession类与Executor类:

SqlSession类中需要定义相应的增删改查的方法,而Executor则是对增删改查完成具体JDBC的封装,SqlSession调用Executor类完成最终功能实现

SqlSession类:SqlSession类中需要定义相应的增删改查的方法

public interface Sqlsession {

    /**
     * 查询批量的数据
     * @param statementId
     * @param params
     * @param <E>
     * @return
     * @throws Exception
     */
    <E> List<E> selectList(String statementId, Object... params) throws Exception;

    /**
     * 查询单个数据
     * @param statementId
     * @param params
     * @param <E>
     * @return
     * @throws Exception
     */
    <E> E selectOne(String statementId, Object... params) throws Exception;

    /**
     * 使用动态代理获得最终代理后的mapper对象
     * @param mapperClass
     * @param <T>
     * @return
     */
    <T> T getMapper(Class<?> mapperClass);

    /**
     * 插入方法
     * @param statement
     * @param params
     * @return
     * @throws Exception
     */
    int insert(String statement, Object... params) throws Exception;

    /**
     * 修改方法
     * @param statement
     * @param params
     * @return
     * @throws Exception
     */
    int update(String statement, Object... params) throws Exception;

    /**
     * 删除方法
     * @param statement
     * @param params
     * @return
     * @throws Exception
     */
    int delete(String statement, Object... params) throws Exception;

    /**
     * 关闭数据库连接
     * @throws SQLException
     */
    void close() throws SQLException;
    
}

SqlSession实现类:

public class DefaultSqlSession implements Sqlsession {
    /**
     * 获取配置文件内容
     */
    private Configuration configuration;

    /**
     * 配置执行器
     */
    private Executor executor = new SimpleExecutor();
    public DefaultSqlSession(Configuration configuration) {
        this.configuration = configuration;
    }

    @Override
    public <E> List<E> selectList(String statementId, Object... params) throws Exception {
        
        List<Object> query = executor.query(configuration, configuration.getMapperStatementMap().get(statementId),
                params);
        return (List<E>) query;
    }

    @Override
    public <E> E selectOne(String statementId, Object... params) throws Exception {
        List<Object> objects = selectList(statementId, params);
        if (objects != null && objects.size() == 1) {
            return (E) objects.get(0);
        }
        throw new RuntimeException("查询的数据为空或者多条!");
    }

    @Override
    public int insert(String statementId, Object... params) throws Exception {
       
        return executor.insert(configuration, configuration.getMapperStatementMap().get(statementId),
                params);
    }
    @Override
    public int update(String statementId, Object... params) throws Exception {
       
        
        return executor.doUpdate(configuration, configuration.getMapperStatementMap().get(statementId),
                params);
    }
    @Override
    public int delete(String statementId, Object... params) throws Exception {
        
        return  executor.delete(configuration, configuration.getMapperStatementMap().get(statementId),
                params);
    }

    @Override
    public void close() throws SQLException {
        executor.close();
    }
    
    @Override
    public <T> T getMapper(Class<?> mapperClass) {
        // 使用JDK动态代理来为Dao接口生成代理对象,并返回

        Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, (proxy, method, args) -> {
            // 底层都还是去执行JDBC代码 //根据不同情况,来调用selctList或者selectOne
            // 准备参数 1:statmentid :sql语句的唯一标识:namespace.id= 接口全限定名.方法名
            String methodName = method.getName();
            String className = method.getDeclaringClass().getName();

            String statementId = className+"."+methodName;
            MapperStatement mapperStatement = configuration.getMapperStatementMap().get(statementId);
            if (mapperStatement == null) {
                throw new RuntimeException("找不到对应的mapperStatement!请检查配置!");
            }
            // 准备参数2:params:args
            // 获取被调用方法的返回值类型
            Type genericReturnType = method.getGenericReturnType();
            
            SqlCommandType sqlCommandType = mapperStatement.getSqlCommandType();
            Object obj = null;
            switch (sqlCommandType) {
                case SELECT:
                    // 判断是否进行了 泛型类型参数化
                    if(genericReturnType instanceof ParameterizedType){
                        List<Object> objects = selectList(statementId, args);
                        obj =  objects;
                     }
                    else{
                        obj =  selectOne(statementId,args);
                    }
                    break;
                case UPDATE:
                    obj = update(statementId, args);
                    break;
                case DELETE:
                    obj = delete(statementId, args);
                    break;
                case INSERT:
                    obj = insert(statementId, args);
                    break;
                //此处不实现    
                case FLUSH:
                    break;
                case UNKNOWN:
                    break;
                default:
                    throw new IllegalStateException("Unexpected value: " + sqlCommandType);
            }
           return obj;

        });

        return (T) proxyInstance;
    }
   
}

Executor类:封装JDBC类

public interface Executor {
    /**
     * 底层封装jdbc查询方法
     * @param configuration
     * @param statement
     * @param param
     * @param <E>
     * @return
     * @throws Exception
     */
    <E> List<E> query(Configuration configuration, MapperStatement statement, Object... param) throws Exception;

    /**
     * 底层封装insert方法
     * @param configuration
     * @param ms
     * @param param
     * @return
     * @throws Exception
     */
    int insert(Configuration configuration,MapperStatement ms,  Object... param) throws  Exception;

    /**
     * 底层封装update方法
     * @param configuration
     * @param ms
     * @param param
     * @return
     * @throws Exception
     */
    int doUpdate(Configuration configuration,MapperStatement ms,  Object... param) throws  Exception;

    /**
     * 底层封装delete方法
     * @param configuration
     * @param ms
     * @param param
     * @return
     * @throws Exception
     */
    int delete(Configuration configuration,MapperStatement ms,  Object... param) throws  Exception;

    /**
     * 关闭对应数据库连接
     * @throws SQLException
     */
    void close() throws SQLException;
}

Executor实现类:查询需要针对返回值类型进行反射封装,增删改统一走doUpdate方法就可以了,不需要要额外写其他内容

public class SimpleExecutor implements Executor {

    private Connection connection;

    @Override
    public <E> List<E> query(Configuration configuration, MapperStatement statement, Object... param) throws Exception {
        List<E> result = new ArrayList<E>();
        try {
            connection = configuration.getSource().getConnection();
            String sql = statement.getSql();
            //将XML中的sql转换为JDBC执行的sql
            BoundSql boundSql = getBoundSql(sql);
            String paramType = statement.getParamType();
            Class<?> aClass = null;
            if (paramType != null) {
                //获取参数类型
                aClass = Class.forName(paramType);
            }

            String sqlText = boundSql.getSqlText();
            List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
            PreparedStatement pstmt = connection.prepareStatement(sqlText);
            for (int i = 0; parameterMappingList != null && i < parameterMappingList.size(); i++) {
                ParameterMapping parameterMapping = parameterMappingList.get(i);
                Field field = aClass.getDeclaredField(parameterMapping.getContent());
                field.setAccessible(true);
                Object value = field.get(param[0]);
                //使用JDBC设置值
                pstmt.setObject(i + 1, value);
            }
            ResultSet resultSet = pstmt.executeQuery();
            //返回类型
            Class<?> resultClass = Class.forName(statement.getResultType());
            
            while (resultSet.next()) {

                Object object = resultClass.newInstance();
                //使用反射的方式为对象设置值并返回
                ResultSetMetaData metaData = resultSet.getMetaData();
                for (int i = 0; i < metaData.getColumnCount(); i++) {
                    String columnName = metaData.getColumnName(i + 1);
                    Object value = resultSet.getObject(columnName);
                    PropertyDescriptor property = new PropertyDescriptor(columnName, resultClass);
                    Method writeMethod = property.getWriteMethod();
                    writeMethod.invoke(object, value);
                }
                result.add((E) object);
            }
        }finally {
            close();
        }
        return result;
    }

    @Override
    public int insert(Configuration configuration, MapperStatement statement, Object... param) throws Exception {
        return doUpdate(configuration, statement, param);
    }

    @Override
    public int doUpdate(Configuration configuration, MapperStatement statement, Object... param) throws Exception {
        int num = 0;
        try {
            connection = configuration.getSource().getConnection();
            String sql = statement.getSql();
            //将XML中的sql转换为JDBC执行的sql
            BoundSql boundSql = getBoundSql(sql);
            //获取参数类型
            String paramType = statement.getParamType();
            Class<?> aClass = Class.forName(paramType);
            String sqlText = boundSql.getSqlText();
            List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
            PreparedStatement pstmt = connection.prepareStatement(sqlText);
            for (int i = 0; i < parameterMappingList.size(); i++) {
                ParameterMapping parameterMapping = parameterMappingList.get(i);
                Field field = aClass.getDeclaredField(parameterMapping.getContent());
                field.setAccessible(true);
                Object value = field.get(param[0]);
                pstmt.setObject(i + 1, value);
            }
            num = pstmt.executeUpdate();
        } finally {
            close();
        }
        return num;
    }

    @Override
    public int delete(Configuration configuration, MapperStatement statement, Object... param) throws Exception {
        return doUpdate(configuration, statement, param);
    }

    @Override
    public void close() throws SQLException {
        connection.close();
    }

    /**
     * 将sql中#{}转换为?号,并按照顺序设置到List<ParameterMapping>中,这样后面可以按照顺序从参数实体类中get出对应的值
     * @param sql
     * @return
     */
    private BoundSql getBoundSql(String sql) {
        ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
        GenericTokenParser genericTokenParser =
                new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
        String parse = genericTokenParser.parse(sql);
        List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
        return new BoundSql(parse, parameterMappings);
    }
}