2021年4月1日星期四

小练手:手写Mybatis实现动态增删改查

一、Mybatis 流程简介

最近在看 Mybatis 的源码,大致了解整个框架流程后便手写了一个特别简单的SimpMybatis的小Demo,来巩固这整个框架的学习。下图是我所画的框架大致执行流程:

mybatis流程图

💊对上图分析后得出结论:

  1. Mybatis 的配置文件分为两种,且这两个配置文件会被封装到 Configuration 中
    • 主配置文件(MybatisConfig.
    • 映射文件(xxxMapper.
  2. 通过 Mybatis 配置文件得到 SqlSessionFactory ;
  3. 通过 SqlSessionFactory 得到 SqlSession,它就相当于 Request 请求;
  4. SqlSession 调用底层的 Executor 执行器来操作数据库,同时执行器有两类实现
    • 基本实现
    • 带有缓存功能的实现
  5. 解析传入的参数,对其进行封装,执行并返回结果;

以上就是我梳理的 Mybatis 大致流程,看似简单,却很精妙。

二、手写简化版 Mybatis 设计思路

2.1 简化后的思路

简化流程图

2.3 创建SqlSession,搭建 Configuration 和 Executor 之间的桥梁

从流程图中的箭头可以看出,MySqlSession 的成员变量中必须得有 MyExecutorImpl 和 MyConfig 去集中做调配。一个Session仅拥有一个对应的数据库连接。类似于一个前段请求Request,它负责直接调用对应 execute(sql) 来做 CRUD 操作。

2.4 创建 MyExecutor,封装 JDBC 操作数据库

MyExecutor 是一个执行器,负责SQL语句的生成和查询缓存的维护,也就是 Jdbc 的代码将在这里完成,不过本文只实现了单表,查询缓存并未实现。

2.5 创建 MySqlSessionProxy,使用动态代理生成 Mapper 对象

只是希望对指定的接口生成一个对象,使得执行它的时候能运行一句 sql,而接口无法直接调用方法,所以这里使用动态代理生成对象,在执行时还是回到 MySqlSession 中调用查询,最终由 MyExecutorImpl 做 JDBC查询。这样设计是为了单一职责,可扩展性更强。

三、实现自己的Mybatis

这次会将其打成 Jar 包,并将其导入项目实现,做一个 Mybatis 的还原。

工程文件及目录:

image-20201125151214685

/** * @author Kenelm * @version 1.0 * @date 2020/11/22 12:17 */public class MyConfig { /** * 启动应用程序类加载器 */ private static final ClassLoader loader = ClassLoader.getSystemClassLoader(); /** * 数据库建立连接 * @return 返回数据库连接对象 * */ public Connection build() { // Mybatis主配置文件名 String resource = "mybatis-config.

⚡由 MyConfig类 代码可以得知:

  1. Mybatis 主配置类名称必须为:mybatis-config.;
  2. mybatis-config.<database></database>
  3. Mapper.namespace
  4. Sql 是否有返回值都应包括:resultType(个人偷懒,没做判断);
  5. ... ...

3.3 MySqlSession 代理

MySqlSession 肯定不会自己去执行,因为不能写死所以使用动态代理来使代理类去实现具体方法。

/** * @author Kenelm * @version 1.0 * @date 2020/11/22 12:52 */public class MySqlSession { private final MyExcutor excutor= new MyExcutorImpl(); private final MyConfig config = new MyConfig(); public <T> T selectValue(Mapping statement, List<Object> parameter){  return excutor.queryValue(statement, parameter); } public <T> T selectNull(Mapping statement){  return excutor.queryNull(statement); } public int deleteValue(Mapping statement, List<Object> parameter) {  return excutor.deleteValue(statement, parameter); } public int updateValue(Mapping statement, List<Object> parameter) {  return excutor.updateValue(statement, parameter); } public int insertValue(Mapping mapping, List<Object> parameter) {  return excutor.insertValue(mapping, parameter); } @SuppressWarnings("unchecked") public <T> T getMapper(Class<T> clas){  //动态代理调用  return (T) Proxy.newProxyInstance(clas.getClassLoader(),new Class[]{clas},    new MySqlSessionProxy(config,this)); }}

编写代理类,把mapper映射文件解析进来

/** * @author Kenelm * @version 1.0 * @date 2020/11/22 12:55 */public class MySqlSessionProxy implements InvocationHandler { private MyConfig config; private MySqlSession sqlSession; public MySqlSessionProxy(MyConfig config, MySqlSession sqlSession) {  this.config = config;  this.sqlSession = sqlSession; } @Override public Object invoke(Object proxy, Method method,Object[] args) {  String name = method.getDeclaringClass().getName();  String mapperName = name.substring(name.lastIndexOf(".")+1);  MappingBean bean=config.parseMapper(MyConfig.parse

⚡注意:通过上段代码可知,映射文件必须和接口名称保持一致。

a. 接口实体类

/** * @author Kenelm * @version 1.0 * @date 2020/11/22 12:38 */public class MappingBean { /**  * 接口名  */ private String interfaceName; /**  * 接口下所有方法  */ private List<Mapping> list; // setter、getter略}

b. 映射文件中 Sql 的实体类

/** * @author Kenelm * @version 1.0 * @date 2020/11/22 12:38 */public class Mapping { private String sqlType; private String funcName; private String sql; private Object resultType; private String parameterType;  // setter、getter略}

3.5 创建 MyExcutor 接口以及实现类

MyExcutor 接口

/** * @author Kenelm * @version 1.0 * @date 2020/11/22 12:42 */public interface MyExcutor { // 无参查询 <T> T queryNull(Mapping mapping);	// 有参查询 <T> T queryValue(Mapping mapping, List<Object> params);	// 删除 int deleteValue(Mapping mapping, List<Object> params);	// 更新 int updateValue(Mapping mapping, List<Object> params);	// 插入 int insertValue(Mapping mapping, List<Object> params);}

MyExcutorImpl 实现类

这里通过反射将结果转换成对象

/** * @author Kenelm * @version 1.0 * @date 2020/11/22 12:42 */public class MyExcutorImpl implements MyExcutor { private MyConfig config = new MyConfig(); @Override public <T> T queryNull(Mapping mapping) {  Connection conn = config.build();  PreparedStatement preparedStatement;  ResultSet resultSet;  Object obj;  List<Object> list = new ArrayList<>();  try {   preparedStatement=conn.prepareStatement(mapping.getSql());   if (mapping.getResultType() == null){    throw new RuntimeException("返回的映射结果不能为空!");   }   resultSet = preparedStatement.executeQuery();   int row = 0;   ResultSetMetaData rd = resultSet.getMetaData();   while (resultSet.next()){    obj=resultToObject(resultSet,mapping.getResultType());    row++;    list.add(obj);   }   System.out.println("记录行数:"+row);  } catch (SQLException e) {   e.printStackTrace();  }  return (T) list; } @Override public <T> T queryValue(Mapping mapping, List<Object> params) {  Connection conn = config.build();  PreparedStatement preparedStatement;  ResultSet resultSet;  Object obj;  List<Object> list = new ArrayList<>();  try {   preparedStatement=conn.prepareStatement(mapping.getSql());   for (int i=0; i<params.size(); i++) {    preparedStatement.setString(i+1, params.get(i).toString());   }   if (mapping.getResultType() == null){    throw new RuntimeException("返回的映射结果不能为空!");   }   resultSet = preparedStatement.executeQuery();   int row = 0;   ResultSetMetaData rd = resultSet.getMetaData();   while (resultSet.next()){    obj=resultToObject(resultSet,mapping.getResultType());    row++;    list.add(obj);   }   System.out.println("记录行数:"+row);  } catch (SQLException e) {   e.printStackTrace();  }  return (T) list; } @Override public int deleteValue(Mapping mapping, List<Object> params) {  Connection conn = config.build();  int rows = 0;  PreparedStatement preparedStatement=null;  try {   preparedStatement = conn.prepareStatement(mapping.getSql());   for (int i=0; i<params.size(); i++) {    preparedStatement.setString(i+1, params.get(i).toString());   }   rows = preparedStatement.executeUpdate();   if (rows != 0) {    System.out.println("删除成功,受影响行数:"+rows);   } else {    System.out.println("删除失败,数据库无相应数据...");   }  } catch (SQLException e) {   e.printStackTrace();  }  return rows; } @Override public int updateValue(Mapping mapping, List<Object> params) {  Connection conn = config.build();  int rows = 0;  PreparedStatement preparedStatement=null;  try {   preparedStatement = conn.prepareStatement(mapping.getSql());   for (int i=0; i<params.size(); i++) {    preparedStatement.setString(i+1, params.get(i).toString());   }   rows = preparedStatement.executeUpdate();   if (rows != 0) {    System.out.println("修改成功,受影响行数:"+rows);   } else {    System.out.println("修改失败,数据库无相应数据...");   }  } catch (SQLException e) {   e.printStackTrace();  }  return rows; } @Override public int insertValue(Mapping mapping, List<Object> params) {  Connection conn = config.build();  int rows = 0;  PreparedStatement preparedStatement=null;  try {   preparedStatement = conn.prepareStatement(mapping.getSql());   for (int i=0; i<params.size(); i++) {    preparedStatement.setString(i+1, params.get(i).toString());   }   try {    rows = preparedStatement.executeUpdate();    if (rows != 0) {     System.out.println("插入成功,受影响行数:"+rows);    } else {     System.out.println("插入失败...");    }   } catch (SQLException throwables) {    throw new RuntimeException("插入重复 \"Key\" 值数据");   }  } catch (SQLException e) {   e.printStackTrace();  }  return rows; } private <T> T resultToObject(ResultSet rs, Object object) {  Object obj=null;  try {   Class<?> cls = object.getClass();  /*   这里为什么要通过class再new一个对象?   因为如果不new一个新的对象,每次返回的都是形参上的object,   而这个object都是同一个,会导致list列表后面覆盖前面值。   */   obj=cls.newInstance();   //获取结果集元数据(获取此 ResultSet 对象的列的编号、类型和属性。)   ResultSetMetaData rd=rs.getMetaData();   for (int i = 0; i < rd.getColumnCount(); i++) {    //获取列名    String columnName=rd.getColumnLabel(i+1);    //组合方法名    String methodName="set"+columnName.substring(0, 1).toUpperCase()+columnName.substring(1);    //获取列类型    int columnType=rd.getColumnType(i+1);    Method method=null;    switch(columnType) {     case java.sql.Types.VARCHAR:     case java.sql.Types.CHAR:      method=cls.getMethod(methodName, String.class);      method.invoke(obj, rs.getString(columnName));      break;     case java.sql.Types.INTEGER:      method=cls.getMethod(methodName, Integer.class);      method.invoke(obj, rs.getInt(columnName));      break;     default:      break;    }   }  } catch (IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException | SQLException e) {   e.printStackTrace();  }  return (T) obj; }}

四、打包测试

4.1 将其打成 Jar 包

打包

4.2 创建一个Maven项目,因为需要导入对应的包

<!-- https://mvnrepository.com/artifact/org.dom4j/dom4j --><!-- 

⚡Maven导入本地Jar包方法自行百度,这里就不赘述。

4.3 创建数据库

这里提供一个我测试的,你们可以自行创建其他的

CREATE DATABASE IF NOT EXISTS `test`;USE `test`;CREATE TABLE `user` (	`id` INT ( 10 ) NOT NULL,	`sex` VARCHAR ( 2 ) NOT NULL,	`password` VARCHAR ( 255 ) DEFAULT NULL,	`username` VARCHAR ( 255 ) DEFAULT NULL,	PRIMARY KEY ( `id` ) ) ENGINE = INNODB AUTO_INCREMENT = 2 DEFAULT CHARSET = utf8;INSERT INTO `test`.`user` ( `id`, `sex`, `password`, `username` )VALUES	( 1, '男', '12344', '五六' ),	( 2, '女', '12643', '张三' ),	( 3, '男', '1245453', '李四' );

4.4 创建实体类

/** * @author Kenelm * @version 1.0 * @date 2020/11/22 16:17 */public class User { private Integer id; private String sex; private String password; private String username; // setter、getter略}

4.5 创建 UserMapper 接口

/** * @author Kenelm * @version 1.0 * @date 2020/11/22 16:17 */public interface UserMapper { List<User> getUsers(); List<User> getUserBySexAndName(String sex, String username); int deleteUserById(Integer id); int updateUserByName(String username, String password); int insertUser(int id, String sex, String password, String username);}

<?

4.8 创建启动类测试

/** * @author Kenelm * @version 1.0 * @date 2020/11/22 16:24 */public class app { public static void main(String[] args) {  MySqlSession sql = new MySqlSession();  UserMapper mapper = sql.getMapper(UserMapper.class);  List<User> users = mapper.getUsers();  users.forEach(System.out::println);  System.out.println("==========================");  List<User> users1 = mapper.getUserBySexAndName("女", "张三");  users1.forEach(System.out::println);  System.out.println("==========================");  mapper.deleteUserById(1);  System.out.println("==========================");  mapper.updateUserByName("五六", "女");  System.out.println("==========================");  mapper.insertUser(10, "男", "123123", "五七"); }}

4.9 测试结果

success

👌测试成功,这就是本人所手写的Mybatis,虽然比较简单,但还是学习到了很多东西。

💥项目放在 Gitee 上有需要自行下载,觉得可以还请点个Star









原文转载:http://www.shaoqun.com/a/656739.html

跨境电商:https://www.ikjzd.com/

aca:https://www.ikjzd.com/w/1371

笨鸟转运:https://www.ikjzd.com/w/1550


一、Mybatis流程简介最近在看Mybatis的源码,大致了解整个框架流程后便手写了一个特别简单的SimpMybatis的小Demo,来巩固这整个框架的学习。下图是我所画的框架大致执行流程:💊对上图分析后得出结论:Mybatis的配置文件分为两种,且这两个配置文件会被封装到Configuration中主配置文件(MybatisConfig.映射文件(xxxMapper.通过Mybatis配置文
patents:https://www.ikjzd.com/w/857
c88:https://www.ikjzd.com/w/1017.html
芒果店长:https://www.ikjzd.com/w/1533
小三问老公她的胸部手感好不好:http://lady.shaoqun.com/a/272445.html
老公深夜归来诸多细节暴露出轨:http://lady.shaoqun.com/a/270392.html
阿里国际站推买家"承诺达":23国5日达:https://www.ikjzd.com/home/106423

没有评论:

发表评论