手写ORM框架(简易版Mybatis)

手写Mybatis,封装JDBC

此篇的主要目的是为了学习框架底层的原理和思想,将一些日常使用的较多的概念和程序进行梳理,希望能帮助到大家

接下来默认理解为你是掌握了JDBC操作数据库的流程和掌握了MVC三层架构的基本思想

这里没有用到controller,所以创建了一个TestMain来测试所有代码,前期还需要准备service层,dao层:

TestMain:

package com.xzc.myORM.test;

import com.xzc.myORM.domain.User;
import com.xzc.myORM.service.UserService;

public class TestMain {
    public static void main(String[] args) {
        UserService userService = new UserService();
        //新增用户
        //userService.registNewUser(new User(3,"roke",789789));
        //删除用户
        //userService.deleteUser(1);
        //修改用户
        //userService.updateUser(new User(1,"xuezhaochang",789456));
        //根据id查询用户
        User user = userService.selectOne(1);
        System.out.println(user);

    }
}

service层:

package com.xzc.myORM.service;

import com.xzc.myORM.dao.UserDao;
import com.xzc.myORM.domain.User;

public class UserService {

    private UserDao userDao = new UserDao();

    public void registNewUser(User user) {
        userDao.insert(user);
    }

    public void deleteUser(int uid) {
        userDao.delete(uid);
    }

    public void updateUser(User user) {
        userDao.update(user);
    }

    public User selectOne(int uid) {
        return userDao.selectOne(uid);
    }
}

dao层

package com.xzc.myORM.dao;

import com.xzc.myORM.domain.User;

import java.sql.*;

public class UserDao {
    private String driver = "com.mysql.jdbc.Driver";
    private String url = "jdbc:mysql://localhost:3306/xuezhaochang?useSSL=false";
    private String username = "root";
    private String password = "root";

    //新增
    public void insert(User user) {
        String sql = "insert into users values(?,?,?)";

        try {
            Class.forName(driver);
            Connection connection = DriverManager.getConnection(url, username, password);
            PreparedStatement statement = connection.prepareStatement(sql);
            statement.setInt(1, user.getUid());
            statement.setString(2, user.getUname());
            statement.setInt(3, user.getUpass());
            statement.executeUpdate();
            statement.close();
            connection.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    //删除
    public void delete(int uid) {
        String sql = "delete from users where uid = ?";

        try {
            Class.forName(driver);
            Connection connection = DriverManager.getConnection(url, username, password);
            PreparedStatement statement = connection.prepareStatement(sql);
            statement.setInt(1, uid);
            statement.executeUpdate();
            statement.close();
            connection.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    //修改
    public void update(User user) {
        String sql = "update users set uname = ? , upass = ? where uid = ?";

        try {
            Class.forName(driver);
            Connection connection = DriverManager.getConnection(url, username, password);
            PreparedStatement statement = connection.prepareStatement(sql);
            statement.setString(1, user.getUname());
            statement.setInt(2, user.getUpass());
            statement.setInt(3, user.getUid());
            statement.executeUpdate();
            statement.close();
            connection.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    //根据id查询单条数据
    public User selectOne(int uid) {
        String sql = "select * from users where uid = ?";
        User user = null;
        try {
            Class.forName(driver);
            Connection connection = DriverManager.getConnection(url, username, password);
            PreparedStatement statement = connection.prepareStatement(sql);
            statement.setInt(1,uid);
            ResultSet rs = statement.executeQuery();
            if (rs.next()) {
                user = new User();
                user.setUid(rs.getInt("uid"));
                user.setUname(rs.getString("uname"));
                user.setUpass(rs.getInt("upass"));
            }
            rs.close();
            statement.close();
            connection.close();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
        return user;
    }
}

以上是使用原生的JDBC来对数据库的增删改查

可以看到,上面的很多问题,

1,大量的代码冗余,其实每个方法的不同点就sql语句和中间statement赋值的时候

2,每执行一个方法,就要连接一次mysql,连接以后再关闭,这是非常耗能的

3,我们的连接数据库参数(url,username,password)是写死的,与代码高度耦合

我们首先先解决代码冗余的问题

SqlSession

为了解决代码冗余问题,我们封装了一个SqlSession类,设计一个通用的方法,来专门帮助dao层来操作jdbc一切事物

SqlSession类:

package com.xzc.myORM.orm;

import com.xzc.myORM.domain.User;

import java.sql.*;

/**
 * 负责帮DAO来做之前所有读写的操作
 * 未来这个类只负责与JDBC打交道,其他的操作可以交给其他人来做
 * 可以理解为,这是DAO找的一个小弟,专门负责给dao大哥提供数据库操作服务
 * 当然,未来这个SqlSession类也可以找小弟帮它做一些除了JDBC之外的操作
 */
public class SqlSession {
    
    private String driver = "com.mysql.jdbc.Driver";
    private String url = "jdbc:mysql://localhost:3306/xuezhaochang?useSSL=false";
    private String username = "root";
    private String password = "root";

    /**
     * 一个update包含增删改
     * 增删改的代码基本长的一样,那么我们就可以只封装这一个方法,来满足dao层的insert(),delete(),update()
     * 参数sql:从dao层传递过来的sql语句字符
     * 参数objs: 可变参数,因为不确定到底要传递几个参数,不确定每个参数的顺序,比如update的顺序id就在最后,
     * insert的顺序id就在第一个
     */
    public void update(String sql, Object... objs) {
        try {
            Class.forName(driver);
            Connection connection = DriverManager.getConnection(url, username, password);
            PreparedStatement statement = connection.prepareStatement(sql);
            if (objs.length > 0) {
                //循环可变参数,可变参数其实就是一个数组
                for (int i = 0; i < objs.length; i++) {
                    statement.setObject(i + 1, objs[i]);
                }
            }
            statement.executeUpdate();
            //关闭资源,先开后关,后开先关
            statement.close();
            connection.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    /**
     * 内核调用的其实是update方法
     * @param sql
     * @param objs
     */
    public void insert(String sql, Object... objs) {
        this.update(sql,objs);
    }
    /**
     * 内核调用的其实是update方法
     * @param sql
     * @param objs
     */
    public void delete(String sql, Object... objs) {
        this.update(sql,objs);
    }
}

这样,我们的dao层的增删改只要2行代码就可以解决了,完全屏蔽了JDBC操作数据库的底层

package com.xzc.myORM.dao;

import com.xzc.myORM.domain.User;
import com.xzc.myORM.orm.SqlSession;

import java.sql.*;

public class UserDao {
	//将SqlSession作为UserDao的依赖引入(作为UserDao的小弟)
    private SqlSession sqlSession = new SqlSession();
    //新增
    public void insert(User user) {
        String sql = "insert into users values(?,?,?)";
        sqlSession.insert(sql,user.getUid(),user.getUname(),user.getUpass());
    }
    //删除
    public void delete(int uid) {
        String sql = "delete from users where uid = ?";
        sqlSession.delete(sql,uid);
    }
    //修改
    public void update(User user) {
        String sql = "update users set uname = ? , upass = ? where uid = ?";
        sqlSession.update(sql,user.getUname(),user.getUpass(),user.getUid());
    }
}

接下来是查询一条数据和查询多条数据的封装

每个查询不同的地方有三处:

​ 一处是sql语句不同

​ 二处是将值传给PreparedStatement不同,也就是问号值

​ 三处是填充不同类型的返回值处不同,也就是将查询结果装入一个新的容器中返回

那么我们可以在SqlSession中设计一个通用的方法,负责支持所有dao层中的任意单条查询操作

⭐⭐在这里需要注意一个点就是三处,三处有一些特殊的是:因为SqlSession中的方法是dao层调用的,三处是需要将返回值返回给dao层的

但是三处所在的方法是不知道这个要返回的是什么类型的,现在我们返回的是User对象,那比如返回Student对象呢,Teacher对象呢?

放眼望去,其实一处和二处都是固定的,和增删改一样,增删改也有这两处不同,但唯独返回值所在的三处不一样,那么可以这样说:

所有的单条查询的方法其他地方都相同,但唯独有一处不同,需要根据不同的需求实现不同的返回类型,那么就可以使用二十三中设计模式中的⭐策略模式⭐

要设计一个策略模式,首先我们要有一个接口,让不同的实现都遵循这个规则,在真正调用的时候返回各自规定的策略

设计一个RowMapper接口,定义一个mapperRow方法,此方法接收一个ResultSet(ResultSet是通过statement.executeQuery得来的数据库中表的其中一行记录,对应着java中实体类的一个对象) ,返回一个T类型的返回值,你策略传给我什么类型的返回值,我就返回给你什么类型

RowMapper接口

package com.xzc.myORM.orm;

import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * 此类是为了在做查询操作时,解决返回不同类型返回值而存在
 * 策略模式,在调用处传入指定类型的返回值类型和具体的操作策略
 */
public interface RowMapper {
	//返回任意类型,具体返回什么类型,由实现类决定
    <T> T mapperRow(ResultSet resultSet) throws SQLException;
}

SqlSession中的selectOne方法和selectList方法:


/**
 * 方式一: 此方法使用到了策略模式
 * @param sql
 * @param rm 策略接口,此接口执行真正实现它的实现类中的策略规则,rm作为参数传入
 * @param values
 * @param <T>
 * @return
 */
public <T> List<T> selectList(String sql, RowMapper rm, Object... values) throws SQLException{
    List<T> list = new ArrayList<>();
    try {
        Class.forName(driver);
        Connection connection = DriverManager.getConnection(url, username, password);
        PreparedStatement statement = connection.prepareStatement(sql);
        if (values.length > 0) {
            for (int i = 0; i < values.length; i++) {
                statement.setObject(i + 1, values[i]);
            }
        }
        ResultSet rs = statement.executeQuery();
        while (rs.next()) {
            //由于sqlsession对象在构建domain的时候不确定到底构建什么类型
            //所以需要一个条件,让不同的dao层对象告诉我,该如何处理对象
            T obj = rm.mapperRow(rs);//此方法是等待dao层调用的时候传递的策略进来才能执行
            list.add(obj);
        }
        rs.close();
        statement.close();
        connection.close();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } catch (SQLException throwables) {
        throwables.printStackTrace();
    }
    return list;
}


/**
 * 方式一: 因为查询单条的这个方法其实就是查询多条的第一条,如果在循环中只循环了一次,那么就是集合中的0下标的数据
 * @param sql
 * @param rm 策略接口,此接口执行真正实现它的实现类中的策略规则
 * @param values
 * @param <T>
 * @return
 */
public <T> T selectOne(String sql,RowMapper rm, Object... values) throws SQLException{
    return (T) selectList(sql, rm, values).get(0);
}

dao层(方式一)

public User selectOne(int uid) throws SQLException {
    String sql = "select * from users where uid = ?";
    /*
    	这种写法是匿名实现类,这也是具体的策略
    	在dao层中定义好具体的策略后,传递给小弟sqlSession的第二个参数
    	当sqlSession中的selectOne方法执行到:rm.mapperRow(rs)时会回来执行此段代码
    */
    RowMapper rowMapper = new RowMapper(){
        @Override
        public <T> T mapperRow(ResultSet resultSet) throws SQLException {
            User user = new User();
            user.setUid(resultSet.getInt("uid"));
            user.setUname(resultSet.getString("uname"));
            user.setUpass(resultSet.getInt("upass"));
            return (T) user;
        }
    };
    //当sqlSession中的selectOne方法执行到:rm.mapperRow(rs)时会回来执行此段代码
    return sqlSession.selectOne(sql,rowMapper,uid);
}

⭐以上的增删改查通过封装一个小弟SqlSession,将冗余代码进行优化,用策略模式解决了通用的查询数据并返回的效果,不过仅此优化还是不够的,现在的增删改看上去没什么大问题了,虽然策略模式帮dao层优化了JDBC的底层细节,也将具体操作隐藏了起来,但是这个策略模式的代码放在这里总觉得怪怪的,别的都是2行代码就搞定了,唯独查询需要额外写一个策略,而且还是需要用户(dao层)自己去规定策略手写代码,并且现在User类中只有3个属性,未来如果一个类中有10个属性,30个属性呢?每个都要这样写,很麻烦,其实也是冗余代码,能不能dao层只传一个类型,然后小弟SqlSession根据这个类型来将数据库返回的数据帮dao层封装好以后并返回呢,这样在dao层看来,就统一了,更加简化了dao层的工作量,可不可以呢?

当然可以,我们可以利用反射,将dao层传递过来的类型,在小弟SqlSession中进行操作,通过反射获取到该类型的所有的属性,然后和数据库中的字段名做匹配,如果匹配成功,那么就将对应的数据帮你填入

⭐⭐⭐我们将策略模式的实现称为方式一⭐⭐⭐

⭐⭐⭐我们将反射机制的实现称为方式二⭐⭐⭐

⭐我们将SqlSession中的selectList方法中本该调用策略接口的地方改为根据Class类型来进行赋值返回操作,这里会用到反射等其他操作,可以给SqlSession这个类再找一个小弟,让这个小弟帮助SqlSession干活,我们定义它为Handler类

Handler类:

package com.xzc.myORM.orm;

import java.lang.reflect.Field;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 重新设计的一个类
 * 可以理解为是SqlSession的小弟
 * 可以帮助SqlSession做很多其他的事情,比如反射分析类型等等问题
 */
public class Handler {

    //如果是domain类型的返回值,目前只支持数据表中的列名和类中的属性名对应
    Object handlerResult(ResultSet rs,Class resultType) throws SQLException {
        Object result = null;
        //这里需要提前判断一下返回值类型,如果是普通类型或者包装类型,那么可以理解为返回值只需要
        //一个确切的数,比如求的是总数,平均值,一个字符串等只有单列单行的结果,如果这样,直接赋值返回即可
        if (resultType == int.class || resultType == Integer.class) {
            result = rs.getInt(1);
        }else if (resultType == float.class || resultType == Float.class) {
            result = rs.getFloat(1);
        }else if (resultType == double.class || resultType == Double.class) {
            result = rs.getDouble(1);
        }else if (resultType == String.class) {
            result = rs.getString(1);
        }else {
            if (resultType == Map.class) {
                //需要解析rs结果集的列名和列值, 列名为key,列值为value, map.put(列名,列值)
                result = getMap(rs);
            }else {//不是list,就是domain实体类
                result = getObject(rs,resultType);
            }
        }
        return result;
    }


    //设计一个小弟方法,为handlerResult方法解析并返回一个map
    //它分析rs结果集,获取列名和列值,存入map中返回
    private Map getMap(ResultSet rs) throws SQLException {
        //创建一个map用来存储返回值类型为map的信息
        Map<String, Object> map = new HashMap<>();
        //获取rs结果集中的所有列名和列值
        ResultSetMetaData metaData = rs.getMetaData();
        //遍历这个metaData,目的是为了获得全部的列,列名
        for (int i = 1; i <= metaData.getColumnCount(); i++) {
            //获取到所有的列名
            String columnName = metaData.getColumnName(i);
            //获取所有的列值
            Object columnValue = rs.getObject(columnName);
            //存入map
            map.put(columnName,columnValue);
        }
        return map;
    }

    //这里用到反射了
    //设计一个小弟方法,为handlerResult方法解析并返回一个domain类型
    //它分析rs结果集,获取列名和列值,存入domain中返回
    private Object getObject(ResultSet rs,Class resultType) throws SQLException {
        Object result = null;
        try {
            result = resultType.newInstance();
            //获取rs结果集中的所有列名和列值
            ResultSetMetaData metaData = rs.getMetaData();
            //遍历这个metaData,目的是为了获得全部的列,列名
            for (int i = 1; i <= metaData.getColumnCount(); i++) {
                //获取到所有的列名
                String columnName = metaData.getColumnName(i);
                //根据rs结果集中的列名,然后通过反射找到对应的属性名
                Field field = resultType.getDeclaredField(columnName);
                //jdk版本高(11,17等)的话需要先通过属性 -->set方法---> invoke()方法   先找到field.getName找到属性名
                //然后将uid的U变大写,拼接一个set,变成setUid,然后执行invoke方法传入实例和列值

                //jdk1.8通过直接操作私有属性的方式
                field.setAccessible(true);
                //直接给属性赋值
                Object columnValue = rs.getObject(columnName);
                field.set(result,columnValue);
            }
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
        return result;
    }
}

然后在SqlSession类中的selectOne方法中调用handler.handlerResult(rs,resultType):

注意这里的参数,之前中间的参数传的是策略接口,现在是Class对象,也就是说dao层给一个返回值类型,我们根据这个类型去分析,反射,将数据装入这个类型内返回

 /**
     * 方式二: 此方法使用反射将查询的结果装入Class类中,也就是resultType中
     *
     * @param sql        dao层传递的sql语句
     * @param resultType dao层提供的一个Class类,用来让小弟SqlSession中的
     *                   selectList方法通过反射的方式将数据装入resultType的实例中
     * @param values     dao层传递过来的拼接sql语句问号的参数
     * @param <T>
     * @return
     */
    public <T> T selectOne(String sql, Class resultType, Object... values) throws SQLException {
        return (T) selectList(sql,resultType,values).get(0);
    }

    /**
     * 方式二: 此方法使用反射将查询的结果装入Class类中,也就是resultType中
     *
     * @param sql        dao层传递的sql语句
     * @param resultType dao层提供的一个Class类,用来让小弟SqlSession中的
     *                   selectList方法通过反射的方式将数据装入resultType的实例中
     * @param values     dao层传递过来的拼接sql语句问号的参数
     * @param <T>
     * @return
     */
    public <T> List<T> selectList(String sql, Class resultType, Object... values) throws SQLException {
        List<T> list = new ArrayList<>();
        try {
            Class.forName(driver);
            Connection connection = DriverManager.getConnection(url, username, password);
            PreparedStatement statement = connection.prepareStatement(sql);
            if (values.length > 0) {
                for (int i = 0; i < values.length; i++) {
                    statement.setObject(i + 1, values[i]);
                }
            }
            ResultSet rs = statement.executeQuery();
            while (rs.next()) {
                //这个过程是让rs里装的数据转换成对象的过程,现在是使用反射的技术,不想写策略模式了
                //找小弟干,之前就说了,SqlSession只负责与JDBC相关的操作,其他别的可以找它的小弟帮忙实现
                T obj = (T) handler.handlerResult(rs, resultType);
                list.add(obj);
            }
            rs.close();
            statement.close();
            connection.close();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
        return list;
    }

dao层:

package com.xzc.myORM.dao;

import com.xzc.myORM.domain.User;
import com.xzc.myORM.orm.RowMapper;
import com.xzc.myORM.orm.SqlSession;

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class UserDao {


    private SqlSession sqlSession = new SqlSession();

    //新增
    public void insert(User user) {
        String sql = "insert into users values(?,?,?)";
        sqlSession.insert(sql, user.getUid(), user.getUname(), user.getUpass());
    }
    //删除
    public void delete(int uid) {
        String sql = "delete from users where uid = ?";
        sqlSession.delete(sql, uid);
    }
    //修改
    public void update(User user) {
        String sql = "update users set uname = ? , upass = ? where uid = ?";
        sqlSession.update(sql, user.getUname(), user.getUpass(), user.getUid());
    }
    //查询单条数据
    public User selectOne(int uid) throws SQLException {
        String sql = "select * from users where uid = ?";
        return sqlSession.selectOne(sql, User.class, uid);
    }
    //查询多条数据
    public List<User> selectList() throws SQLException {
        String sql = "select * from users";
        return sqlSession.selectList(sql,User.class);
    }
}

看,现在的dao层多么的清新,每一个方法都是2行代码,它的小弟SqlSession帮它把所有与JDBC相关的操作都隐藏了起来,只提供了对应的方法,dao层的方法只需要按照小弟的意思,把小弟需要的参数传入即可

虽然目前已经封装的算是很好了,但是细心的朋友可能想到了: 现在是3个属性,那我现在一个表中有30个字段,实体类中有30个属性呢?

难道我要写30个问号和30个属性值吗,就算只剩下两行,但是这两行很长啊,别扭的很... 是的,这是一个问题,而且我们可以在此基础上再次进行优化,我们已经用反射实现了返回值的类型,也能读取它的属性,那么为什么不可以让dao层只传一个Class类型我们就可以帮它做到呢?

如何做到呢? 我们可以根据在sql语句的 ? 问号处做一些工作:

修改某条数据 : String sql = "update users set uname = ? , upass = ? where uid = ?";

新增一条数据 : String sql = "insert into users values(?,?,?)";

我们来看这两条sql,insert的时候我们传入参数的值是按照顺序传的: uid , uname , upass , update的时候参数顺序是: uname,upass,uid

很明显,如果我们只传一个Class类型进去倒是可以通过反射赋值,但是怎么确保sql语句每个问号所对应的值是这个 ? 问号想要的呢,也就是说如何保证属性值的顺序呢

⭐⭐⭐OK, SqlSession小弟说了, 大哥(dao层),我定义一个规则,你将sql语句 传给我的时候把 ? 问号先用#{属性名}的方式代替,这样我就可以根据你传给我的返回值类型反射出来所有的属性名跟你传给我的#{属性名}匹配,这样,就可以保证顺序也可以将数据装入返回值类型中给你返回了

⭐⭐⭐这就是#{属性名}的由来,在底层将#{}解析完以后再转换成sql语句需要的 ? 问号并传给prepareStatement()方法⭐⭐⭐

那么现在,我们之前的增删改方法,就可以退下舞台了,接下来重新写一个方法,来帮助dao层更加便捷有效的工作

首先顶一个类:SQLAndKey,这个类专门负责装载原生的sql和有序的属性名参数集合

SQLAndKey类:

package com.xzc.myORM.orm;

import java.util.ArrayList;
import java.util.List;

/**
 * 设计这个类的目的是为了在解析带有#{属性名}的sql,存储返回结果用的
 * 这里目前返回的有两个属性:
 *          String          sql             原生带有 ? 问号的sql
 *          ArrayList    有序属性名集合        存着所有属性名的顺序
 */
public class SQLAndKey {

    //用来拼接原生带 ? 问号的sql
    private StringBuilder sql = new StringBuilder();
    //用来存储解析#{属性名}过后的有序属性名集合
    private List<String> list = new ArrayList<>();

    public SQLAndKey(StringBuilder sql, List<String> list) {
        this.sql = sql;
        this.list = list;
    }
    
    public String getSql() {
        return sql.toString();
    }

    public List<String> getList() {
        return list;
    }

    public void setSql(StringBuilder sql) {
        this.sql = sql;
    }

    public void setList(List<String> list) {
        this.list = list;
    }
}

在Handler中新定义的两个方法,这些方法专门来对#{}和传进来的参数类型进行解析,并给Statement赋值

//==========================处理参数值的问题,给PreparedStatement赋值==========================

void handlerParameter(PreparedStatement statement, Object obj, List<String> KeyList) throws SQLException {
    Class<?> clazz = obj.getClass();
    /**
     * 1,判断clazz是什么类型
     *   可能是基本类型或者说一个变量(int/float/double/string)
     *   map类型(多个问号值,可能还不是domain中的属性值,不过map中的key要和#{key}中的key对应)
     *   domain类型(多个问号值,刚好都是domain中自带的属性---属性名字与#{key}对应)
     */
    //先判断基本类型或者说单个变量的
    if (clazz == int.class || clazz == Integer.class) {
        statement.setInt(1, (Integer) obj);
    } else if (clazz == float.class || clazz == Float.class) {
        statement.setFloat(1, (Float) obj);
    } else if (clazz == double.class || clazz == Double.class) {
        statement.setDouble(1, (Double) obj);
    } else if (clazz == String.class) {
        statement.setString(1, (String) obj);
    } else {
        //不是map就是domain
        if (obj instanceof Map) {
            //因为这个很麻烦,所以我找小弟来帮忙处理Map类型的参数
            this.setMap(statement, obj, KeyList);
        } else {
            //因为这个更麻烦,所以我找小弟来帮忙处理domain类型的参数,这里用到了反射
            this.setObject(statement, obj, KeyList);
        }
    }
}

/**
 * 设计的小弟方法,负责处理map类型的参数
 *
 * @param statement
 * @param obj
 * @param KeyList
 */
private void setMap(PreparedStatement statement, Object obj, List<String> KeyList) throws SQLException {
    Map map = (Map) obj;
    //遍历keyList 通过key去map中找值,让statement赋值
    for (int i = 0; i < KeyList.size(); i++) {
        String key = KeyList.get(i);
        Object value = map.get(key);
        statement.setObject(i + 1, value);
    }
}

/**
 * 设计的小弟方法,负责处理domain类型的参数
 * 这里用到了反射技术
 *
 * @param statement
 * @param obj
 * @param KeyList
 */
private void setObject(PreparedStatement statement, Object obj, List<String> KeyList) throws SQLException {
    try {
        //获取Class
        Class<?> clazz = obj.getClass();
        for (int i = 0; i < KeyList.size(); i++) {
            String key = KeyList.get(i);
            //通过key反射找到obj对象对应的那个属性
            Field field = clazz.getDeclaredField(key);
            field.setAccessible(true);
            //获取到每一个属性对应的值
            Object value = field.get(obj);
            statement.setObject(i + 1,value);
        }
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
}


//============================解析sql并返回原生带?号sql和有序属性名=============================

SQLAndKey parseSQL(String sql) {
    StringBuilder newSQL = new StringBuilder();//用来存储最终带 ? 问号的sql
    List<String> list = new ArrayList<>();//用来存储和?问号对应的有序的属性名
    //解析,按照规则,先解析#{}
    while (true) {
        int left = sql.indexOf("#{");
        int right = sql.indexOf("}");
        //判断大括号是否是一对儿
        //update users set uname = #{uname} , upass = #{upass} where uid = #{uid}
        if (left != -1 && right != -1 && left < right) {
            newSQL.append(sql.substring(0, left));
            newSQL.append("?");
            list.add(sql.substring(left + 2, right));
        } else {
            newSQL.append(sql);
            break;
        }
        sql = sql.substring(right + 1);
    }
    return new SQLAndKey(newSQL, list);
}

SqlSession中的update方法中调用Handler中的handlerParameter()方法和parseSQL()方法:

/**
 * 为了实现sql语句带#{}的功能
 *
 * @param sql = "update users set uname = #{uname} , upass = #{upass} where uid = #{uid}";
 * @param obj Object类型是为了兼容任何类型
 */
public void update(String sql, Object obj) {
    if (sql == null || sql == "") return;
    /*
        解析这条特殊的sql: "update users set uname = #{uname} , upass = #{upass} where uid = #{uid}";
        我们要将#{属性名}中的属性名取出来放入到一个有序集合中,这是顺序的保证,还要将这条sql中的#{}再替换成 ? 问号
        找Handler小弟,让它帮我解析这条sql,然后帮我返回一个有序的集合,和带?问号的sql
        那么这里需要返回两个不同类型的数据,我们可以单独创建一个类: SQLAndKey,就只是来存放这两个返回值
    * */
    //这里调用Handler类中的parseSQL(sql)方法
    SQLAndKey sqlAndKey = handler.parseSQL(sql);
    try {
        Class.forName(driver);
        Connection connection = DriverManager.getConnection(url, username, password);
        PreparedStatement statement = connection.prepareStatement(sqlAndKey.getSql());
        /**
         * 让statement对象,将带有?问号的sql和obj对象中存储的值,进行完整的拼接
         * 在handler中定义一个小弟方法,让它帮忙完成这个功能
         */
        if (obj != null)
            //调用Handler类中的handlerParameter()方法
            handler.handlerParameter(statement,obj,sqlAndKey.getList());
        //上部执行完以后就可以执行啦
        statement.executeUpdate();
        statement.close();
        connection.close();
    } catch (SQLException throwables) {
        throwables.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

dao层现在的样子:

package com.xzc.myORM.dao;

import com.xzc.myORM.domain.User;
import com.xzc.myORM.orm.RowMapper;
import com.xzc.myORM.orm.SqlSession;

import java.sql.*;
import java.util.List;

public class UserDao {
    
    private SqlSession sqlSession = new SqlSession();

    //新增
    public void insert(User user) {
        String sql = "insert into users values(#{uid},#{uname},#{upass})";
        sqlSession.insert(sql, user);
    }
    //删除
    public void delete(int uid) {
        String sql = "delete from users where uid = #{uid}";
        sqlSession.delete(sql, uid);
    }
    //修改
    public void update(User user) {
        String sql = "update users set uname = #{uname} , upass = #{upass} where uid = #{uid}";
        sqlSession.update(sql,user);
    }
    //查询单条数据
    public User selectOne(int uid) throws SQLException {
        String sql = "select * from users where uid = ?";
        return sqlSession.selectOne(sql, User.class, uid);
    }
    //查询多条数据
    public List<User> selectList() throws SQLException {
        String sql = "select * from users";
        return sqlSession.selectList(sql,User.class);
    }
}

接着是service层

package com.xzc.myORM.service;

import com.xzc.myORM.dao.UserDao;
import com.xzc.myORM.domain.User;

import java.sql.SQLException;
import java.util.List;

public class UserService {

    private UserDao userDao = new UserDao();

    public void registNewUser(User user) {
        userDao.insert(user);
    }

    public void deleteUser(int uid) {
        userDao.delete(uid);
    }

    public void updateUser(User user) {
        userDao.update(user);
    }

    public User selectOne(int uid) throws SQLException {
        return userDao.selectOne(uid);
    }
    public List<User> selectList() throws SQLException {
        return userDao.selectList();
    }
}

最后测试:

package com.xzc.myORM.test;

import com.xzc.myORM.domain.User;
import com.xzc.myORM.service.UserService;

import java.sql.SQLException;
import java.util.List;

public class TestMain {
    public static void main(String[] args) throws SQLException {
        UserService userService = new UserService();
//        //新增用户
//        userService.registNewUser(new User(4,"zhangyasen",666666));
        //删除用户
//        userService.deleteUser(4);
        //修改用户
        userService.updateUser(new User(3,"tiantian",9999));
        //根据id查询用户
//       User user = userService.selectOne(1);
//        System.out.println(user);
        //查询所有
//        List<User> users = userService.selectList();
//        System.out.println(users);

    }
}

以上做完如果完成了数据库的增删改查所有功能,说明上述代码和思想已领悟

然后接着往下看!!!

⭐现在dao层的每个方法的代码都是两行,现在想更变态一点,能不能连这2行代码都省掉,sql不就是一个字符串嘛,给它提出来,放在外边,然后让sqlsession去读,比如说写在文件中(xml文件),比如写在注解里

如果sql语句放在了外边,那么代码就剩下一行sqlsession调用方法了,这一行怎么办呢? 其实可以使用代理对象来调用sqlsession,谁的代理对象呢?当然是dao层的代理对象,如果真这样的实现了的话,那dao层中的方法就没代码了,就只剩下一个空壳大括号了,那dao层就可以改成一个接口,然后创建dao层接口的代理实现类来帮助dao层调用sqlsession的方法,然后我们可以创建自定义注解,将sql语句写在方法上,底层通过反射得到这个sql,并传给JDBC的PreparedStatement去执行相应的操作

接下来以注解形式来实现上述要求

定义自定义注解:

一共定义了4个注解:

​ @interface Insert

​ @interface Update

​ @interface Delete

​ @interface Query

下面就只展示一个吧,因为这四个除了名字不一样,其他全部一样

@interface Insert:

package com.xzc.myORM.orm.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Insert {
    //可以理解为注解对象是个搬运工
    //帮忙做一件事,帮助DAO将一条sql搬运给解析的人(sqlsession)

    String value();
    
}

dao层

package com.xzc.myORM.dao;

import com.xzc.myORM.domain.User;
import com.xzc.myORM.orm.annotation.Delete;
import com.xzc.myORM.orm.annotation.Insert;
import com.xzc.myORM.orm.annotation.Query;
import com.xzc.myORM.orm.annotation.Update;
import java.sql.*;
import java.util.List;

public interface UserDao {
    //新增
    @Insert("insert into users values(#{uid},#{uname},#{upass})")
    public void insert(User user);
    //删除
    @Delete("delete from users where uid = #{uid}")
    public void delete(int uid);
    //修改
    @Update("update users set uname = #{uname} , upass = #{upass} where uid = #{uid}")
    public void update(User user);
    //查询单条数据
    @Query("select * from users where uid = ?")
    public User selectOne(int uid) throws SQLException;
    //查询多条数据
    @Query("select * from users")
    public List<User> selectList() throws SQLException;
}

SqlSession中定义了一个getMapper()方法,用来创建UserDao的代理实现类,在这里动态的判断UserDao接口方法上的注解类型,并进行调用,传值,拼接sql,返回在查询后想要的数据

getMapper():

//=================设计一个方法,可以在sqlsession类中产生一个代理对象,代替原来的DAO做事=================
// 分析 需要返参数吗?  需要,我们生产的是代理对象,你要告诉我代理的是谁
//     需要返回值吗?   需要,返回一个代理对象,这个代理对象其实就是DAOC层接口的子类实现
public <T> T getMapper(Class clazz) {

    //创建代理对象,newProxyInstance()有三个参数:
    //1,ClassLoader
    //2,加载的类Class[] 通常来说,数组里就装一个Class,但考虑到可能会一次性创建多个代理对象
    //3,具体该如何做事:InvocationHandler
    //ClassLoader
    ClassLoader classLoader = clazz.getClassLoader();
    //要加载的类Class[]
    Class[] classes = new Class[]{clazz};
    //具体的操作
    InvocationHandler invocation = new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            /**
             * 参数讲解:
             *  proxy:就是代理对象,代替原来的DAO(本例中是userDao)
             *  method:表示的是DAO中的方法
             *  args:DAO中方法的参数
             */
            //这个invoke方法就是代替原来dao做的事情,之前dao层的UserDao会调用增删改查方法
            //在这里,我们根据反射,拿到dao层接口方法上的注解,如果是Insert注解就调用insert方法,以此类推
            //1,解析dao层方法上的注解对象(获取注解的类型以判别调用哪个方法,获取注解中的值,也就是sql语句,还有调用方法时的传参,参数)
            Annotation an = method.getAnnotations()[0];
            //2,获取注解对象的Class类型
            Class<? extends Annotation> annotationType = an.annotationType();
            //3,获取注解中的值(sql语句) 通过注解对象的Class类型来获得
            Method valueMethod = annotationType.getMethod("value");
            String sql = (String) valueMethod.invoke(an);
            Object obj = args == null ? null : args[0];
            if (annotationType == Insert.class)
                SqlSession.this.update(sql, obj);
            else if (annotationType == Update.class)
                SqlSession.this.update(sql, obj);
            else if (annotationType == Delete.class)
                SqlSession.this.update(sql, obj);
            else if (annotationType == Select.class) {
                //查询比较复杂一些,光根据Select注解是无法确定要调用是单条查询还是多条查询
                //可以根据方法的返回值来确定: 是对象,调单条,是集合,调多条
                Class<?> returnType = method.getReturnType();
                if (returnType == List.class) {
                    //如果是集合,我们就要取出里面的泛型
                    Type type = method.getGenericReturnType();
                    //将这个type还原成可以操作泛型的类
                    ParameterizedType realReturnType = (ParameterizedType) type;
                    //获取ParameterizedType中的泛型类
                    Type[] patterns = realReturnType.getActualTypeArguments();
                    //获取patterns数组的第一个
                    Type patternType = patterns[0];
                    //将Type类型的再转换成Class类型
                    Class resultType = (Class) patternType;
                    return SqlSession.this.selectList(sql,obj,resultType);
                }else {
                    return SqlSession.this.selectOne(sql,obj,returnType);
                }
            }else {
                System.out.println("这个类型框架没有,出现异常,这个类型框架处理不了");
            }
            return null;
        }
    };
    Object obj = Proxy.newProxyInstance(classLoader, classes, invocation);
    //将代理对象返回出去
    return (T) obj;
}

dao层之所以如此简洁,全归功与sqlsession底层的大力封装,框架真给力

还有一个小问题,现在都是SqlSession在做事,所有的与JDBC交互的事情都交给了sqlSession,我们的数据库连接配置也是在sqlsession中写死的,未来这个sqlsession就是一个框架,人家不能只连我们的数据库连接的配置信息,一定是通用的,未来交给任何人都可以连接mysql进行操作,我们可以将这些数据库配置写在一个文件中,然后sqlsession读取这个文件来完成数据库的连接

定义一个configuration.properties文件,来存放配置信息

configuration.properties:

driver=com.mysql.jdbc.Driver  
url=jdbc:mysql://localhost:3306/xuezhaochang?useSSL=false
username=root
password=root

然后再专门定义一个类,目的就是去读取配置文件

ConfigurationReader:

package com.xzc.myORM.orm;

import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

//这个类专门来加载JDBC连接的配置文件
//当SqlSession加载的时候就会读取此类加载配置文件
public final class ConfigurationReader {

    //当作一个缓存,将properties读出来的数据装入
    private static Map<String,String> map = new HashMap<>();

    //在当前类加载的时候就去读取配置文件,并且是只读一次的
    static {
        Properties prop = new Properties();
        InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("configuration.properties");
        try {
            prop.load(inputStream);
            Enumeration<?> enumeration = prop.propertyNames();
            while (enumeration.hasMoreElements()) {
                String key = (String) enumeration.nextElement();
                String value = prop.getProperty(key);
                map.put(key,value);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    public Map<String,String> getMap() {
        return map;
    }

    public static String getValue(String key) {
        return map.get(key);
    }
}

到这里,我们手写的简易版Mybatis就告一段落了,其实还有数据库连接池的功能没有做,现在是每开启一次操作就会连接一次数据库然后再次关闭,这样是非常消耗资源的,而且效率也不高,未来有机会会再回来弥补

⭐⭐⭐接下来展示所有的代码(最终版)⭐⭐⭐

TestMain:

package com.xzc.myORM.test;

import com.xzc.myORM.domain.User;
import com.xzc.myORM.service.UserService;

import java.sql.SQLException;
import java.util.List;

public class TestMain {
    public static void main(String[] args) throws SQLException {
        UserService userService = new UserService();
//        //新增用户
//        userService.registNewUser(new User(5,"xiaojiba",111111));
        //删除用户
//        userService.deleteUser(5);
        //修改用户
//        userService.updateUser(new User(3,"hello",7788));
        //根据id查询用户
//       User user = userService.selectOne(1);
//        System.out.println(user);
        //查询所有
//        List<User> users = userService.selectList();
//        System.out.println(users);

    }
}

UserService

package com.xzc.myORM.service;

import com.xzc.myORM.dao.UserDao;
import com.xzc.myORM.domain.User;
import com.xzc.myORM.orm.SqlSession;

import java.sql.SQLException;
import java.util.List;

public class UserService {

    //在这里,我们在service层中通过SqlSession的getMapper方法,将UserDao接口的Class类型传入得到UserDao的代理实现类
    private UserDao userDao = new SqlSession().getMapper(UserDao.class);

    public void registNewUser(User user) {
        userDao.insert(user);
    }

    public void deleteUser(int uid) {
        userDao.delete(uid);
    }

    public void updateUser(User user) {
        userDao.update(user);
    }

    public User selectOne(int uid) throws SQLException {
        return userDao.selectOne(uid);
    }
    public List<User> selectList() throws SQLException {
        return userDao.selectList();
    }
}

UserDao:

package com.xzc.myORM.dao;

import com.xzc.myORM.domain.User;
import com.xzc.myORM.orm.annotation.Delete;
import com.xzc.myORM.orm.annotation.Insert;
import com.xzc.myORM.orm.annotation.Select;
import com.xzc.myORM.orm.annotation.Update;
import java.sql.*;
import java.util.List;

public interface UserDao {

    //新增
    @Insert("insert into users values(#{uid},#{uname},#{upass})")
    public void insert(User user);
    //删除
    @Delete("delete from users where uid = #{uid}")
    public void delete(int uid);
    //修改
    @Update("update users set uname = #{uname} , upass = #{upass} where uid = #{uid}")
    public void update(User user);
    //查询单条数据
    @Select("select * from users where uid = ?")
    public User selectOne(int uid) throws SQLException;
    //查询多条数据
    @Select("select * from users")
    public List<User> selectList() throws SQLException;
}

❗❗❗注意,接下来的所有类,接口,注解都是在orm包中的,也就是未来框架所做的事情,这些并不是我们在日常开发中写的,写这篇文章的目的就是为了学习以下所有的代码的逻辑思维,了解框架的底层思想!!!

SqlSession:

package com.xzc.myORM.orm;

import com.xzc.myORM.domain.User;
import com.xzc.myORM.orm.annotation.Delete;
import com.xzc.myORM.orm.annotation.Insert;
import com.xzc.myORM.orm.annotation.Select;
import com.xzc.myORM.orm.annotation.Update;

import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

/**
 * 负责帮DAO来做之前所有读写的操作
 * 未来这个类只负责与JDBC打交道,其他的操作可以交给其他人来做
 * 可以理解为,这是DAO找的一个小弟,专门负责给dao大哥提供数据库操作服务
 * 当然,未来这个SqlSession类也可以找小弟帮它做一些除了JDBC之外的操作
 */
public class SqlSession {

    //SqlSession的小弟
    private Handler handler = new Handler();
	//通过ConfigurationReader读取properties文件,根据Key到value
    private String driver = ConfigurationReader.getValue("driver");
    private String url = ConfigurationReader.getValue("url");
    private String username = ConfigurationReader.getValue("username");
    private String password = ConfigurationReader.getValue("password");

    //=================设计一个方法,可以在sqlsession类中产生一个代理对象,代替原来的DAO做事=================
    // 分析 需要返参数吗?  需要,我们生产的是代理对象,你要告诉我代理的是谁
    //     需要返回值吗?   需要,返回一个代理对象,这个代理对象其实就是DAOC层接口的子类实现
    public <T> T getMapper(Class clazz) {

        //创建代理对象,newProxyInstance()有三个参数:
        //1,ClassLoader
        //2,加载的类Class[] 通常来说,数组里就装一个Class,但考虑到可能会一次性创建多个代理对象
        //3,具体该如何做事:InvocationHandler
        //ClassLoader
        ClassLoader classLoader = clazz.getClassLoader();
        //要加载的类Class[]
        Class[] classes = new Class[]{clazz};
        //具体的操作
        InvocationHandler invocation = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                /**
                 * 参数讲解:
                 *  proxy:就是代理对象,代替原来的DAO(本例中是userDao)
                 *  method:表示的是DAO中的方法
                 *  args:DAO中方法的参数
                 */
                //这个invoke方法就是代替原来dao做的事情,之前dao层的UserDao会调用增删改查方法
                //在这里,我们根据反射,拿到dao层接口方法上的注解,如果是Insert注解就调用insert方法,以此类推
                //1,解析dao层方法上的注解对象(获取注解的类型以判别调用哪个方法,获取注解中的值,也就是sql语句,还有调用方法时的传参,参数)
                Annotation an = method.getAnnotations()[0];
                //2,获取注解对象的Class类型
                Class<? extends Annotation> annotationType = an.annotationType();
                //3,获取注解中的值(sql语句) 通过注解对象的Class类型来获得
                Method valueMethod = annotationType.getMethod("value");
                String sql = (String) valueMethod.invoke(an);
                Object obj = args == null ? null : args[0];
                if (annotationType == Insert.class)
                    SqlSession.this.update(sql, obj);
                else if (annotationType == Update.class)
                    SqlSession.this.update(sql, obj);
                else if (annotationType == Delete.class)
                    SqlSession.this.update(sql, obj);
                else if (annotationType == Select.class) {
                    //查询比较复杂一些,光根据Select注解是无法确定要调用是单条查询还是多条查询
                    //可以根据方法的返回值来确定: 是对象,调单条,是集合,调多条
                    Class<?> returnType = method.getReturnType();
                    if (returnType == List.class) {
                        //如果是集合,我们就要取出里面的泛型
                        Type type = method.getGenericReturnType();
                        //将这个type还原成可以操作泛型的类
                        ParameterizedType realReturnType = (ParameterizedType) type;
                        //获取ParameterizedType中的泛型类
                        Type[] patterns = realReturnType.getActualTypeArguments();
                        //获取patterns数组的第一个
                        Type patternType = patterns[0];
                        //将Type类型的再转换成Class类型
                        Class resultType = (Class) patternType;
                        return SqlSession.this.selectList(sql,obj,resultType);
                    }else {
                        return SqlSession.this.selectOne(sql,obj,returnType);
                    }
                }else {
                    System.out.println("这个类型框架没有,出现异常,这个类型框架处理不了");
                }
                return null;
            }
        };
        Object obj = Proxy.newProxyInstance(classLoader, classes, invocation);
        //将代理对象返回出去
        return (T) obj;
    }

    /**
     * ⭐方式二(最终选择的方式): 此方法使用反射将查询的结果装入Class类中,也就是resultType中  ⭐方式一在最下边,已注释(可用)
     *
     * @param sql        dao层传递的sql语句
     * @param resultType dao层提供的一个Class类,用来让小弟SqlSession中的
     *                   selectList方法通过反射的方式将数据装入resultType的实例中
     * @param obj        dao层传递过来的拼接sql语句问号的参数
     * @param <T>
     * @return
     */
    public <T> T selectOne(String sql, Object obj, Class resultType) throws SQLException {
        return (T) selectList(sql, obj, resultType).get(0);
    }

    /**
     * ⭐方式二(最终选择的方式): 此方法使用反射将查询的结果装入Class类中,也就是resultType中	⭐方式一在最下边,已注释(可用)
     *
     * @param sql        dao层传递的sql语句
     * @param resultType dao层提供的一个Class类,用来让小弟SqlSession中的
     *                   selectList方法通过反射的方式将数据装入resultType的实例中
     * @param obj        dao层传递过来的拼接sql语句问号的参数
     * @param <T>
     * @return
     */
    public <T> List<T> selectList(String sql, Object obj, Class resultType) throws SQLException {
        List<T> list = new ArrayList<>();
        try {
            Class.forName(driver);
            SQLAndKey sqlAndKey = handler.parseSQL(sql);
            Connection connection = DriverManager.getConnection(url, username, password);
            PreparedStatement statement = connection.prepareStatement(sqlAndKey.getSql());
            if (obj != null) {
                handler.handlerParameter(statement, obj, sqlAndKey.getList());
            }
            ResultSet rs = statement.executeQuery();
            while (rs.next()) {
                //这个过程是让rs里装的数据转换成对象的过程,现在是使用反射的技术,不想写策略模式了
                //找小弟干,之前就说了,SqlSession只负责与JDBC相关的操作,其他别的可以找它的小弟帮忙实现
                T result = (T) handler.handlerResult(rs, resultType);
                list.add(result);
            }
            rs.close();
            statement.close();
            connection.close();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
        return list;
    }

//===================方式二: #{}方式实现:这样参数只传sql和类型即可=======================

    /**
     * 为了实现sql语句带#{}的功能
     *
     * @param sql = "update users set uname = #{uname} , upass = #{upass} where uid = #{uid}";
     * @param obj Object类型是为了兼容任何类型
     */
    public void update(String sql, Object obj) {
        if (sql == null || sql == "") return;
        /*
            解析这条特殊的sql: "update users set uname = #{uname} , upass = #{upass} where uid = #{uid}";
            我们要将#{属性名}中的属性名取出来放入到一个有序集合中,这是顺序的保证,还要将这条sql中的#{}再替换成 ? 问号
            找Handler小弟,让它帮我解析这条sql,然后帮我返回一个有序的集合,和带?问号的sql
            那么这里需要返回两个不同类型的数据,我们可以单独创建一个类: SQLAndKey,就只是来存放这两个返回值
        * */
        SQLAndKey sqlAndKey = handler.parseSQL(sql);
        try {
            Class.forName(driver);
            Connection connection = DriverManager.getConnection(url, username, password);
            PreparedStatement statement = connection.prepareStatement(sqlAndKey.getSql());
            /**
             * 让statement对象,将带有?问号的sql和obj对象中存储的值,进行完整的拼接
             * 在handler中定义一个小弟方法,让它帮忙完成这个功能
             */
            if (obj != null)
                handler.handlerParameter(statement, obj, sqlAndKey.getList());
            //上部执行完以后就可以执行啦
            statement.executeUpdate();
            statement.close();
            connection.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }


    /**
     * 为了实现sql语句带#{}的功能
     *
     * @param sql = "update users set uname = #{uname} , upass = #{upass} where uid = #{uid}";
     * @param obj Object类型是为了兼容任何类型
     */
//    public void insert(String sql, Object obj) {
//        update(sql, obj);
//    }

    /**
     * 为了实现sql语句带#{}的功能
     *
     * @param sql = "update users set uname = #{uname} , upass = #{upass} where uid = #{uid}";
     * @param obj Object类型是为了兼容任何类型
     */
//    public void delete(String sql, Object obj) {
//        update(sql, obj);
//    }

//============================方式一: 以下是#{}方式出现之前的方式==============================

    /**
     * 一个update包含增删改
     * 增删改的代码基本长的一样,那么我们就可以只封装这一个方法,来满足dao层的insert(),delete(),update()
     * 参数sql:从dao层传递过来的sql语句字符
     * 参数objs: 可变参数,因为不确定到底要传递几个参数,不确定顺序,
     */
//    public void update(String sql, Object... objs) {
//        try {
//            Class.forName(driver);
//            Connection connection = DriverManager.getConnection(url, username, password);
//            PreparedStatement statement = connection.prepareStatement(sql);
//            if (objs.length > 0) {
//                for (int i = 0; i < objs.length; i++) {
//                    statement.setObject(i + 1, objs[i]);
//                }
//            }
//            statement.executeUpdate();
//            statement.close();
//            connection.close();
//        } catch (SQLException throwables) {
//            throwables.printStackTrace();
//        } catch (ClassNotFoundException e) {
//            e.printStackTrace();
//        }
//    }

    /**
     * 内核调用的其实是update方法
     *
     * @param sql
     * @param objs
     */
//    public void insert(String sql, Object... objs) {
//        this.update(sql, objs);
//    }

    /**
     * 内核调用的其实是update方法
     *
     * @param sql
     * @param objs
     */
//    public void delete(String sql, Object... objs) {
//        this.update(sql, objs);
//    }
    
    //=======================================================================================

//    /**
//     * 方式一: 此方法使用到了策略模式
//     * @param sql
//     * @param rm 策略接口,此接口执行真正实现它的实现类中的策略规则
//     * @param values
//     * @param <T>
//     * @return
//     */
//    public <T> T selectOne(String sql,RowMapper rm, Object... values) throws SQLException{
//        return (T) selectList(sql, rm, values).get(0);
//    }
//
//    /**
//     * 方式一: 此方法使用到了策略模式
//     * @param sql
//     * @param rm 策略接口,此接口执行真正实现它的实现类中的策略规则
//     * @param values
//     * @param <T>
//     * @return
//     */
//    public <T> List<T> selectList(String sql, RowMapper rm, Object... values) throws SQLException{
//        List<T> list = new ArrayList<>();
//        try {
//            Class.forName(driver);
//            Connection connection = DriverManager.getConnection(url, username, password);
//            PreparedStatement statement = connection.prepareStatement(sql);
//            if (values.length > 0) {
//                for (int i = 0; i < values.length; i++) {
//                    statement.setObject(i + 1, values[i]);
//                }
//            }
//            ResultSet rs = statement.executeQuery();
//            while (rs.next()) {
//                //由于sqlsession对象在构建domain的时候不确定到底构建什么类型
//                //所以需要一个条件,让不同的dao层对象告诉我,该如何处理对象
//                T obj = rm.mapperRow(rs);//此方法是等待dao层调用的时候传递的策略进来才能执行
//                list.add(obj);
//            }
//            rs.close();
//            statement.close();
//            connection.close();
//        } catch (ClassNotFoundException e) {
//            e.printStackTrace();
//        } catch (SQLException throwables) {
//            throwables.printStackTrace();
//        }
//        return list;
//    }
}

Handler:

package com.xzc.myORM.orm;

import java.lang.reflect.Field;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.*;

/**
 * 重新设计的一个类
 * 可以理解为是SqlSession的小弟
 * 可以帮助SqlSession做很多其他的事情,比如反射分析类型等等问题
 */
public class Handler {

    //==========================处理参数值的问题,给PreparedStatement赋值==========================

    void handlerParameter(PreparedStatement statement, Object obj, List<String> KeyList) throws SQLException {
        Class<?> clazz = obj.getClass();
        /**
         * 1,判断clazz是什么类型
         *   可能是基本类型或者说一个变量(int/float/double/string)
         *   map类型(多个问号值,可能还不是domain中的属性值,不过map中的key要和#{key}中的key对应)
         *   domain类型(多个问号值,刚好都是domain中自带的属性---属性名字与#{key}对应)
         */
        //先判断基本类型或者说单个变量的
        if (clazz == int.class || clazz == Integer.class) {
            statement.setInt(1, (Integer) obj);
        } else if (clazz == float.class || clazz == Float.class) {
            statement.setFloat(1, (Float) obj);
        } else if (clazz == double.class || clazz == Double.class) {
            statement.setDouble(1, (Double) obj);
        } else if (clazz == String.class) {
            statement.setString(1, (String) obj);
        } else {
            //不是map就是domain
            if (obj instanceof Map) {
                //因为这个很麻烦,所以我找小弟来帮忙处理Map类型的参数
                this.setMap(statement, obj, KeyList);
            } else {
                //因为这个更麻烦,所以我找小弟来帮忙处理domain类型的参数,这里用到了反射
                this.setObject(statement, obj, KeyList);
            }
        }
    }

    /**
     * 设计的小弟方法,负责处理map类型的参数
     *
     * @param statement
     * @param obj
     * @param KeyList
     */
    private void setMap(PreparedStatement statement, Object obj, List<String> KeyList) throws SQLException {
        Map map = (Map) obj;
        //遍历keyList 通过key去map中找值,让statement赋值
        for (int i = 0; i < KeyList.size(); i++) {
            String key = KeyList.get(i);
            Object value = map.get(key);
            statement.setObject(i + 1, value);
        }
    }

    /**
     * 设计的小弟方法,负责处理domain类型的参数
     * 这里用到了反射技术
     *
     * @param statement
     * @param obj
     * @param KeyList
     */
    private void setObject(PreparedStatement statement, Object obj, List<String> KeyList) throws SQLException {
        try {
            //获取Class
            Class<?> clazz = obj.getClass();
            for (int i = 0; i < KeyList.size(); i++) {
                String key = KeyList.get(i);
                //通过key反射找到obj对象对应的那个属性
                Field field = clazz.getDeclaredField(key);
                field.setAccessible(true);
                //获取到每一个属性对应的值
                Object value = field.get(obj);
                statement.setObject(i + 1,value);
            }
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }


    //============================解析sql并返回原生带?号sql和有序属性名=============================

    SQLAndKey parseSQL(String sql) {
        StringBuilder newSQL = new StringBuilder();//用来存储最终带 ? 问号的sql
        List<String> list = new ArrayList<>();//用来存储和?问号对应的有序的属性名
        //解析,按照规则,先解析#{}
        while (true) {
            int left = sql.indexOf("#{");
            int right = sql.indexOf("}");
            //判断大括号是否是一对儿
            //update users set uname = #{uname} , upass = #{upass} where uid = #{uid}
            if (left != -1 && right != -1 && left < right) {
                newSQL.append(sql.substring(0, left));
                newSQL.append("?");
                list.add(sql.substring(left + 2, right));
            } else {
                newSQL.append(sql);
                break;
            }
            sql = sql.substring(right + 1);
        }
        return new SQLAndKey(newSQL, list);
    }


    //===========================================================================

    //如果是domain类型的返回值,目前只支持数据表中的列名和类中的属性名对应
    Object handlerResult(ResultSet rs, Class resultType) throws SQLException {
        Object result = null;
        //这里需要提前判断一下返回值类型,如果是普通类型或者包装类型,那么可以理解为返回值只需要
        //一个确切的数,比如求的是总数,平均值,一个字符串等只有单列单行的结果,如果这样,直接赋值返回即可
        if (resultType == int.class || resultType == Integer.class) {
            result = rs.getInt(1);
        } else if (resultType == float.class || resultType == Float.class) {
            result = rs.getFloat(1);
        } else if (resultType == double.class || resultType == Double.class) {
            result = rs.getDouble(1);
        } else if (resultType == String.class) {
            result = rs.getString(1);
        } else {
            //这里只判断一个是否是list集合或者是domain类型,map可以自行判断处理
            if (resultType == Map.class) {
                //需要解析rs结果集的列名和列值, 列名为key,列值为value, map.put(列名,列值)
                result = getMap(rs);
            } else {//不是list,就是domain实体类
                result = getObject(rs, resultType);
            }
        }
        return result;
    }

    //设计一个小弟方法,为handlerResult方法解析并返回一个map
    //它分析rs结果集,获取列名和列值,存入map中返回
    private Map getMap(ResultSet rs) throws SQLException {
        //创建一个map用来存储返回值类型为map的信息
        Map<String, Object> map = new HashMap<>();
        //获取rs结果集中的所有列名和列值
        ResultSetMetaData metaData = rs.getMetaData();
        //遍历这个metaData,目的是为了获得全部的列,列名
        for (int i = 1; i <= metaData.getColumnCount(); i++) {
            //获取到所有的列名
            String columnName = metaData.getColumnName(i);
            //获取所有的列值
            Object columnValue = rs.getObject(columnName);
            //存入map
            map.put(columnName, columnValue);
        }
        return map;
    }

    //这里用到反射了
    //设计一个小弟方法,为handlerResult方法解析并返回一个domain类型
    //它分析rs结果集,获取列名和列值,存入domain中返回
    private Object getObject(ResultSet rs, Class resultType) throws SQLException {
        Object result = null;
        try {
            result = resultType.newInstance();
            //获取rs结果集中的所有列名和列值
            ResultSetMetaData metaData = rs.getMetaData();
            //遍历这个metaData,目的是为了获得全部的列,列名
            for (int i = 1; i <= metaData.getColumnCount(); i++) {
                //获取到所有的列名
                String columnName = metaData.getColumnName(i);
                //根据rs结果集中的列名,然后通过反射找到对应的属性名
                Field field = resultType.getDeclaredField(columnName);
                //jdk版本高(11,17等)的话需要先通过属性 -->set方法---> invoke()方法   先找到field.getName找到属性名
                //然后将uid的U变大写,拼接一个set,变成setUid,然后执行invoke方法传入实例和列值

                //jdk1.8通过直接操作私有属性的方式
                field.setAccessible(true);
                //直接给属性赋值
                Object columnValue = rs.getObject(columnName);
                field.set(result, columnValue);
            }
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
        return result;
    }
}

SQLAndKey:

package com.xzc.myORM.orm;

import java.util.ArrayList;
import java.util.List;

/**
 * 设计这个类的目的是为了在解析带有#{属性名}的sql,存储返回结果用的
 * 这里目前返回的有两个属性:
 *          String          sql             原生带有 ? 问号的sql
 *          ArrayList    有序属性名集合        存着所有属性名的顺序
 */
public class SQLAndKey {

    //用来拼接原生带 ? 问号的sql
    private StringBuilder sql = new StringBuilder();
    //用来存储解析#{属性名}过后的有序属性名集合
    private List<String> list = new ArrayList<>();

    public SQLAndKey(StringBuilder sql, List<String> list) {
        this.sql = sql;
        this.list = list;
    }

    public String getSql() {
        return sql.toString();
    }

    public List<String> getList() {
        return list;
    }

    public void setSql(StringBuilder sql) {
        this.sql = sql;
    }

    public void setList(List<String> list) {
        this.list = list;
    }
}

@Insert @Update @Delete @Select:

package com.xzc.myORM.orm.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Insert {
    //可以理解为注解对象是个搬运工
    //帮忙做一件事,帮助DAO将一条sql搬运给解析的人(sqlsession)

    String value();
}
package com.xzc.myORM.orm.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Update {
    //可以理解为注解对象是个搬运工
    //帮忙做一件事,帮助DAO将一条sql搬运给解析的人(sqlsession)
    String value();
}
package com.xzc.myORM.orm.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Delete {
    //可以理解为注解对象是个搬运工
    //帮忙做一件事,帮助DAO将一条sql搬运给解析的人(sqlsession)
    String value();
}
package com.xzc.myORM.orm.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Select {
    //可以理解为注解对象是个搬运工
    //帮忙做一件事,帮助DAO将一条sql搬运给解析的人(sqlsession)
    String value();
}

ConfigurationReader:

package com.xzc.myORM.orm;

import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

//这个类专门来加载JDBC连接的配置文件
//当SqlSession加载的时候就会读取此类加载配置文件
public final class ConfigurationReader {

    //当作一个缓存,将properties读出来的数据装入
    private static Map<String,String> map = new HashMap<>();

    //在当前类加载的时候就去读取配置文件,并且是只读一次的
    static {
        Properties prop = new Properties();
        InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("configuration.properties");
        try {
            prop.load(inputStream);
            Enumeration<?> enumeration = prop.propertyNames();
            while (enumeration.hasMoreElements()) {
                String key = (String) enumeration.nextElement();
                String value = prop.getProperty(key);
                map.put(key,value);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    public Map<String,String> getMap() {
        return map;
    }

    public static String getValue(String key) {
        return map.get(key);
    }
}

RowMapper放在了最后,因为这种方式就是一种过度,由策略模式引出的反射

RowMapper:

package com.xzc.myORM.orm;

import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * 此类是为了在做查询操作时,解决返回不同类型返回值而存在
 * 策略模式,在调用处传入指定类型的返回值类型和具体的操作策略
 */
public interface RowMapper {

    <T> T mapperRow(ResultSet resultSet) throws SQLException;
}

到此完毕,如果有人耐心做完得到了效果,那么可以将Mybatis依赖导入,方法名和注解名都不用该,只需要更改创建UserDao的代理实现类那一块和创建SqlSession的地方,就能和我们自己写的orm包下的功能实现一样的效果,不过就是自己写的是一个简易版的,所以需要学习的地方还由很多,路还长~~~

细细品味这整个更新换代的过程吧

posted @ 2023-03-01 17:26  XUEZHAOCHANG  阅读(176)  评论(0编辑  收藏  举报