源码分析系列教程(08) - 手写MyBatis(注解版)

简介: 源码分析系列教程(08) - 手写MyBatis(注解版)

代码已上传到GitHub,有兴趣的同学可以下载来看看:https://github.com/ylw-github/Java-CodeAnalysis-Demo

1. MyBatis相关核心知识

MyBatis和数据库的交互有两种方式:

  • 使用传统的MyBatis提供的API;
  • 使用Mapper接口;

1.1 使用Mapper接口

MyBatis 将配置文件中的每一个<mapper>节点抽象为一个 Mapper 接口:

这个接口中声明的方法和<mapper>节点中的<select|update|delete|insert>节点项对应,即<select|update|delete|insert>节点的id值为Mapper 接口中的方法名称,parameterType 值表示Mapper 对应方法的入参类型,而resultMap 值则对应了Mapper 接口表示的返回值类型或者返回结果集的元素类型。

根据MyBatis 的配置规范配置好后,通过SqlSession.getMapper(XXXMapper.class)方法,MyBatis 会根据相应的接口声明的方法信息,通过动态代理机制生成一个Mapper 实例,我们使用Mapper接口的某一个方法时,MyBatis会根据这个方法的方法名和参数类型,确定Statement Id,底层还是通过SqlSession.select("statementId",parameterObject);或者SqlSession.update("statementId",parameterObject); 等等来实现对数据库的操作,MyBatis引用Mapper 接口这种调用方式,纯粹是为了满足面向接口编程的需要。(其实还有一个原因是在于,面向接口的编程,使得用户在接口上可以使用注解来配置SQL语句,这样就可以脱离XML配置文件,实现“0配置”)。

1.2 数据处理层

数据处理层可以说是MyBatis的核心,从大的方面上讲,它要完成两个功能:

  • 通过传入参数构建动态SQL语句;
  • SQL语句的执行以及封装查询结果集成 List<E>

1.3 框架支撑层

1.3.1 事务管理机制

事务管理机制对于ORM框架而言是不可缺少的一部分,事务管理机制的质量也是考量一个ORM框架是否优秀的一个标准。

1.3.2 连接池管理机制

由于创建一个数据库连接所占用的资源比较大,对于数据吞吐量大和访问量非常大的应用而言,连接池的设计就显得非常重要。

1.3.3 缓存机制

为了提高数据利用率和减小服务器和数据库的压力,MyBatis 会对于一些查询提供会话级别的数据缓存,会将对某一次查询,放置到SqlSession 中,在允许的时间间隔内,对于完全相同的查询,MyBatis会直接将缓存结果返回给用户,而不用再到数据库中查找。

1.4 SQL语句的配置方式

传统的MyBatis 配置SQL语句方式就是使用XML文件进行配置的,但是这种方式不能很好地支持面向接口编程的理念,为了支持面向接口的编程,MyBatis 引入了Mapper接口的概念,面向接口的引入,对使用注解来配置SQL语句成为可能,用户只需要在接口上添加必要的注解即可,不用再去配置XML文件了。

但是,目前的MyBatis 只是对注解配置SQL语句提供了有限的支持,某些高级功能还是要依赖XML配置文件配置SQL 语句。

1.5 引导层

引导层是配置和启动MyBatis配置信息的方式。MyBatis 提供两种方式来引导MyBatis :

  • 基于XML配置文件的方式
  • 基于Java API 的方式。

1.6 主要构件及其相互关系

从MyBatis代码实现的角度来看,MyBatis的主要的核心部件有以下几个:

  • SqlSession:作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能;
  • Executor:MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护;
  • StatementHandler:封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合。
  • ParameterHandler:负责对用户传递的参数转换成JDBC Statement 所需要的参数;
  • ResultSetHandler:负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;
  • TypeHandler:负责java数据类型和jdbc数据类型之间的映射和转换;
  • MappedStatement:MappedStatement维护了一条<select|update|delete|insert>节点的封装;
  • SqlSource:负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装BoundSql对象中,并返回;
  • BoundSql:表示动态生成的SQL语句以及相应的参数信息;
  • Configuration:MyBatis所有的配置信息都维持在Configuration对象之中;

2. 手写MyBatis代码

1.定义JDBCUtils:

package com.ylw.mybatis.orm.utils;
import java.sql.*;
import java.util.List;
public final class JDBCUtils {
  private static String connect;
  private static String driverClassName;
  private static String URL;
  private static String username;
  private static String password;
  private static boolean autoCommit;
  /** 声明一个 Connection类型的静态属性,用来缓存一个已经存在的连接对象 */
  private static Connection conn;
  static {
    config();
  }
  /**
   * 开头配置自己的数据库信息
   */
  private static void config() {
    /*
     * 获取驱动
     */
    driverClassName = "com.mysql.jdbc.Driver";
    /*
     * 获取URL
     */
    URL = "jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8";
    /*
     * 获取用户名
     */
    username = "root";
    /*
     * 获取密码
     */
    password = "123456";
    /*
     * 设置是否自动提交,一般为false不用改
     */
    autoCommit = false;
  }
  /**
   * 载入数据库驱动类
   */
  private static boolean load() {
    try {
      Class.forName(driverClassName);
      return true;
    } catch (ClassNotFoundException e) {
      System.out.println("驱动类 " + driverClassName + " 加载失败");
    }
    return false;
  }
  /**
   * 专门检查缓存的连接是否不可以被使用 ,不可以被使用的话,就返回 true
   */
  private static boolean invalid() {
    if (conn != null) {
      try {
        if (conn.isClosed() || !conn.isValid(3)) {
          return true;
          /*
           * isValid方法是判断Connection是否有效,如果连接尚未关闭并且仍然有效,则返回 true
           */
        }
      } catch (SQLException e) {
        e.printStackTrace();
      }
      /*
       * conn 既不是 null 且也没有关闭 ,且 isValid 返回 true,说明是可以使用的 ( 返回 false )
       */
      return false;
    } else {
      return true;
    }
  }
  /**
   * 建立数据库连接
   */
  public static Connection connect() {
    if (invalid()) { /* invalid为true时,说明连接是失败的 */
      /* 加载驱动 */
      load();
      try {
        /* 建立连接 */
        conn = DriverManager.getConnection(URL, username, password);
      } catch (SQLException e) {
        System.out.println("建立 " + connect + " 数据库连接失败 , " + e.getMessage());
      }
    }
    return conn;
  }
  /**
   * 设置是否自动提交事务
   **/
  public static void transaction() {
    try {
      conn.setAutoCommit(autoCommit);
    } catch (SQLException e) {
      System.out.println("设置事务的提交方式为 : " + (autoCommit ? "自动提交" : "手动提交") + " 时失败: " + e.getMessage());
    }
  }
  /**
   * 创建 Statement 对象
   */
  public static Statement statement() {
    Statement st = null;
    connect();
    /* 如果连接是无效的就重新连接 */
    transaction();
    /* 设置事务的提交方式 */
    try {
      st = conn.createStatement();
    } catch (SQLException e) {
      System.out.println("创建 Statement 对象失败: " + e.getMessage());
    }
    return st;
  }
  /**
   * 根据给定的带参数占位符的SQL语句,创建 PreparedStatement 对象
   * 
   * @param SQL
   *            带参数占位符的SQL语句
   * @return 返回相应的 PreparedStatement 对象
   */
  private static PreparedStatement prepare(String SQL, boolean autoGeneratedKeys) {
    PreparedStatement ps = null;
    connect();
    /* 如果连接是无效的就重新连接 */
    transaction();
    /* 设置事务的提交方式 */
    try {
      if (autoGeneratedKeys) {
        ps = conn.prepareStatement(SQL, Statement.RETURN_GENERATED_KEYS);
      } else {
        ps = conn.prepareStatement(SQL);
      }
    } catch (SQLException e) {
      System.out.println("创建 PreparedStatement 对象失败: " + e.getMessage());
    }
    return ps;
  }
  public static ResultSet query(String SQL, List<Object> params) {
    if (SQL == null || SQL.trim().isEmpty() || !SQL.trim().toLowerCase().startsWith("select")) {
      throw new RuntimeException("你的SQL语句为空或不是查询语句");
    }
    ResultSet rs = null;
    if (params.size() > 0) {
      /* 说明 有参数 传入,就需要处理参数 */
      PreparedStatement ps = prepare(SQL, false);
      try {
        for (int i = 0; i < params.size(); i++) {
          ps.setObject(i + 1, params.get(i));
        }
        rs = ps.executeQuery();
      } catch (SQLException e) {
        System.out.println("执行SQL失败: " + e.getMessage());
      }
    } else {
      /* 说明没有传入任何参数 */
      Statement st = statement();
      try {
        rs = st.executeQuery(SQL); // 直接执行不带参数的 SQL 语句
      } catch (SQLException e) {
        System.out.println("执行SQL失败: " + e.getMessage());
      }
    }
    return rs;
  }
  private static Object typeof(Object o) {
    Object r = o;
    if (o instanceof java.sql.Timestamp) {
      return r;
    }
    // 将 java.util.Date 转成 java.sql.Date
    if (o instanceof java.util.Date) {
      java.util.Date d = (java.util.Date) o;
      r = new java.sql.Date(d.getTime());
      return r;
    }
    // 将 Character 或 char 变成 String
    if (o instanceof Character || o.getClass() == char.class) {
      r = String.valueOf(o);
      return r;
    }
    return r;
  }
  public static boolean execute(String SQL, Object... params) {
    if (SQL == null || SQL.trim().isEmpty() || SQL.trim().toLowerCase().startsWith("select")) {
      throw new RuntimeException("你的SQL语句为空或有错");
    }
    boolean r = false;
    /* 表示 执行 DDL 或 DML 操作是否成功的一个标识变量 */
    /* 获得 被执行的 SQL 语句的 前缀 */
    SQL = SQL.trim();
    SQL = SQL.toLowerCase();
    String prefix = SQL.substring(0, SQL.indexOf(" "));
    String operation = ""; // 用来保存操作类型的 变量
    // 根据前缀 确定操作
    switch (prefix) {
    case "create":
      operation = "create table";
      break;
    case "alter":
      operation = "update table";
      break;
    case "drop":
      operation = "drop table";
      break;
    case "truncate":
      operation = "truncate table";
      break;
    case "insert":
      operation = "insert :";
      break;
    case "update":
      operation = "update :";
      break;
    case "delete":
      operation = "delete :";
      break;
    }
    if (params.length > 0) { // 说明有参数
      PreparedStatement ps = prepare(SQL, false);
      Connection c = null;
      try {
        c = ps.getConnection();
      } catch (SQLException e) {
        e.printStackTrace();
      }
      try {
        for (int i = 0; i < params.length; i++) {
          Object p = params[i];
          p = typeof(p);
          ps.setObject(i + 1, p);
        }
        ps.executeUpdate();
        commit(c);
        r = true;
      } catch (SQLException e) {
        System.out.println(operation + " 失败: " + e.getMessage());
        rollback(c);
      }
    } else { // 说明没有参数
      Statement st = statement();
      Connection c = null;
      try {
        c = st.getConnection();
      } catch (SQLException e) {
        e.printStackTrace();
      }
      // 执行 DDL 或 DML 语句,并返回执行结果
      try {
        st.executeUpdate(SQL);
        commit(c); // 提交事务
        r = true;
      } catch (SQLException e) {
        System.out.println(operation + " 失败: " + e.getMessage());
        rollback(c); // 回滚事务
      }
    }
    return r;
  }
  /*
   * 
   * @param SQL 需要执行的 INSERT 语句
   * 
   * @param autoGeneratedKeys 指示是否需要返回由数据库产生的键
   * 
   * @param params 将要执行的SQL语句中包含的参数占位符的 参数值
   * 
   * @return 如果指定 autoGeneratedKeys 为 true 则返回由数据库产生的键; 如果指定 autoGeneratedKeys
   * 为 false 则返回受当前SQL影响的记录数目
   */
  public static int insert(String SQL, boolean autoGeneratedKeys, List<Object> params) {
    int var = -1;
    if (SQL == null || SQL.trim().isEmpty()) {
      throw new RuntimeException("你没有指定SQL语句,请检查是否指定了需要执行的SQL语句");
    }
    // 如果不是 insert 开头开头的语句
    if (!SQL.trim().toLowerCase().startsWith("insert")) {
      System.out.println(SQL.toLowerCase());
      throw new RuntimeException("你指定的SQL语句不是插入语句,请检查你的SQL语句");
    }
    // 获得 被执行的 SQL 语句的 前缀 ( 第一个单词 )
    SQL = SQL.trim();
    SQL = SQL.toLowerCase();
    if (params.size() > 0) { // 说明有参数
      PreparedStatement ps = prepare(SQL, autoGeneratedKeys);
      Connection c = null;
      try {
        c = ps.getConnection(); // 从 PreparedStatement 对象中获得 它对应的连接对象
      } catch (SQLException e) {
        e.printStackTrace();
      }
      try {
        for (int i = 0; i < params.size(); i++) {
          Object p = params.get(i);
          p = typeof(p);
          ps.setObject(i + 1, p);
        }
        int count = ps.executeUpdate();
        if (autoGeneratedKeys) { // 如果希望获得数据库产生的键
          ResultSet rs = ps.getGeneratedKeys(); // 获得数据库产生的键集
          if (rs.next()) { // 因为是保存的是单条记录,因此至多返回一个键
            var = rs.getInt(1); // 获得值并赋值给 var 变量
          }
        } else {
          var = count; // 如果不需要获得,则将受SQL影像的记录数赋值给 var 变量
        }
        commit(c);
      } catch (SQLException e) {
        System.out.println("数据保存失败: " + e.getMessage());
        rollback(c);
      }
    } else { // 说明没有参数
      Statement st = statement();
      Connection c = null;
      try {
        c = st.getConnection(); // 从 Statement 对象中获得 它对应的连接对象
      } catch (SQLException e) {
        e.printStackTrace();
      }
      // 执行 DDL 或 DML 语句,并返回执行结果
      try {
        int count = st.executeUpdate(SQL);
        if (autoGeneratedKeys) { // 如果企望获得数据库产生的键
          ResultSet rs = st.getGeneratedKeys(); // 获得数据库产生的键集
          if (rs.next()) { // 因为是保存的是单条记录,因此至多返回一个键
            var = rs.getInt(1); // 获得值并赋值给 var 变量
          }
        } else {
          var = count; // 如果不需要获得,则将受SQL影像的记录数赋值给 var 变量
        }
        commit(c); // 提交事务
      } catch (SQLException e) {
        System.out.println("数据保存失败: " + e.getMessage());
        rollback(c); // 回滚事务
      }
    }
    return var;
  }
  /** 提交事务 */
  private static void commit(Connection c) {
    if (c != null && !autoCommit) {
      try {
        c.commit();
      } catch (SQLException e) {
        e.printStackTrace();
      }
    }
  }
  /** 回滚事务 */
  private static void rollback(Connection c) {
    if (c != null && !autoCommit) {
      try {
        c.rollback();
      } catch (SQLException e) {
        e.printStackTrace();
      }
    }
  }
  /**
   * 释放资源
   **/
  public static void release(Object cloaseable) {
    if (cloaseable != null) {
      if (cloaseable instanceof ResultSet) {
        ResultSet rs = (ResultSet) cloaseable;
        try {
          rs.close();
        } catch (SQLException e) {
          e.printStackTrace();
        }
      }
      if (cloaseable instanceof Statement) {
        Statement st = (Statement) cloaseable;
        try {
          st.close();
        } catch (SQLException e) {
          e.printStackTrace();
        }
      }
      if (cloaseable instanceof Connection) {
        Connection c = (Connection) cloaseable;
        try {
          c.close();
        } catch (SQLException e) {
          e.printStackTrace();
        }
      }
    }
  }
}

2.定义MyInvocationHandlerMbatis

package com.ylw.mybatis.orm.mybatis.aop;
import com.ylw.mybatis.orm.annotation.ExtInsert;
import com.ylw.mybatis.orm.annotation.ExtParam;
import com.ylw.mybatis.orm.annotation.ExtSelect;
import com.ylw.mybatis.orm.utils.JDBCUtils;
import com.ylw.mybatis.orm.utils.SQLUtils;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
public class MyInvocationHandlerMbatis implements InvocationHandler {
  private Object object;
  public MyInvocationHandlerMbatis(Object object) {
    this.object = object;
  }
  // proxy 代理对象 method拦截方法 args方法上的参数值
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    System.out.println("使用动态代理技术拦截接口方法开始");
    // 使用白话问翻译,@ExtInsert封装过程
    // 1. 判断方法上是否存在@ExtInsert
    ExtInsert extInsert = method.getDeclaredAnnotation(ExtInsert.class);
    if (extInsert != null) {
      return extInsert(extInsert, proxy, method, args);
    }
    // 2.查询的思路
    // 1. 判断方法上是否存 在注解
    ExtSelect extSelect = method.getDeclaredAnnotation(ExtSelect.class);
    if (extSelect != null) {
      // 2. 获取注解上查询的SQL语句
      String selectSQL = extSelect.value();
      // 3. 获取方法上的参数,绑定在一起
      ConcurrentHashMap<Object, Object> paramsMap = paramsMap(proxy, method, args);
      // 4. 参数替换?传递方式
      List<String> sqlSelectParameter = SQLUtils.sqlSelectParameter(selectSQL);
      // 5.传递参数
      List<Object> sqlParams = new ArrayList<>();
      for (String parameterName : sqlSelectParameter) {
        Object parameterValue = paramsMap.get(parameterName);
        sqlParams.add(parameterValue);
      }
      // 6.将sql语句替换成?
      String newSql = SQLUtils.parameQuestion(selectSQL, sqlSelectParameter);
      System.out.println("newSQL:" + newSql + ",sqlParams:" + sqlParams.toString());
      // 5.调用jdbc代码底层执行sql语句
      // 6.使用反射机制实例对象### 获取方法返回的类型,进行实例化
      // 思路:
      // 1.使用反射机制获取方法的类型
      // 2.判断是否有结果集,如果有结果集,在进行初始化
      // 3.使用反射机制,给对象赋值
      ResultSet res = JDBCUtils.query(newSql, sqlParams);
      // 判断是否存在值
      if (!res.next()) {
        return null;
      }
      // 下标往上移动移位
      res.previous();
      // 使用反射机制获取方法的类型
      Class<?> returnType = method.getReturnType();
      Object object = returnType.newInstance();
      while (res.next()) {
        // 获取当前所有的属性
        Field[] declaredFields = returnType.getDeclaredFields();
        for (Field field : declaredFields) {
          String fieldName = field.getName();
          Object fieldValue = res.getObject(fieldName);
          field.setAccessible(true);
          field.set(object, fieldValue);
        }
        // for (String parameteName : sqlSelectParameter) {
        // // 获取参数值
        // Object resultValue = res.getObject(parameteName);
        // // 使用java的反射值赋值
        // Field field = returnType.getDeclaredField(parameteName);
        // // 私有方法允许访问
        // field.setAccessible(true);
        // field.set(object, resultValue);
        // }
      }
      return object;
    }
    return null;
  }
  private Object extInsert(ExtInsert extInsert, Object proxy, Method method, Object[] args) {
    // 方法上存在@ExtInsert,获取他的SQL语句
    // 2. 获取SQL语句,获取注解Insert语句
    String insertSql = extInsert.value();
    // System.out.println("insertSql:" + insertSql);
    // 3. 获取方法的参数和SQL参数进行匹配
    // 定一个一个Map集合 KEY为@ExtParamValue,Value 结果为参数值
    ConcurrentHashMap<Object, Object> paramsMap = paramsMap(proxy, method, args);
    // 存放sql执行的参数---参数绑定过程
    String[] sqlInsertParameter = SQLUtils.sqlInsertParameter(insertSql);
    List<Object> sqlParams = sqlParams(sqlInsertParameter, paramsMap);
    // 4. 根据参数替换参数变为?
    String newSQL = SQLUtils.parameQuestion(insertSql, sqlInsertParameter);
    System.out.println("newSQL:" + newSQL + ",sqlParams:" + sqlParams.toString());
    // 5. 调用jdbc底层代码执行语句
    return JDBCUtils.insert(newSQL, false, sqlParams);
  }
  private List<Object> sqlParams(String[] sqlInsertParameter, ConcurrentHashMap<Object, Object> paramsMap) {
    List<Object> sqlParams = new ArrayList<>();
    for (String paramName : sqlInsertParameter) {
      Object paramValue = paramsMap.get(paramName);
      sqlParams.add(paramValue);
    }
    return sqlParams;
  }
  private ConcurrentHashMap<Object, Object> paramsMap(Object proxy, Method method, Object[] args) {
    ConcurrentHashMap<Object, Object> paramsMap = new ConcurrentHashMap<>();
    // 获取方法上的参数
    Parameter[] parameters = method.getParameters();
    for (int i = 0; i < parameters.length; i++) {
      Parameter parameter = parameters[i];
      ExtParam extParam = parameter.getDeclaredAnnotation(ExtParam.class);
      if (extParam != null) {
        // 参数名称
        String paramName = extParam.value();
        Object paramValue = args[i];
        // System.out.println(paramName + "," + paramValue);
        paramsMap.put(paramName, paramValue);
      }
    }
    return paramsMap;
  }
  public Object extInsertSQL() {
    return object;
  }
}

3.定义SQLUtils

package com.ylw.mybatis.orm.utils;
import java.util.ArrayList;
import java.util.List;
/**
 * SQL拼接<br>
 */
public class SQLUtils {
  /**
   * 
   * 获取Insert语句后面values 参数信息<br>
   * @param sql
   * @return
   */
  public static String[] sqlInsertParameter(String sql) {
    int startIndex = sql.indexOf("values");
    int endIndex = sql.length();
    String substring = sql.substring(startIndex + 6, endIndex).replace("(", "").replace(")", "").replace("#{", "")
        .replace("}", "");
    String[] split = substring.split(",");
    return split;
  }
  /**
   * 
   * 获取select 后面where语句
   *
   * @param sql
   * @return
   */
  public static List<String> sqlSelectParameter(String sql) {
    int startIndex = sql.indexOf("where");
    int endIndex = sql.length();
    String substring = sql.substring(startIndex + 5, endIndex);
    String[] split = substring.split("and");
    List<String> listArr = new ArrayList<>();
    for (String string : split) {
      String[] sp2 = string.split("=");
      listArr.add(sp2[0].trim());
    }
    return listArr;
  }
  /**
   * 将SQL语句的参数替换变为?<br>
   * 
   * @param sql
   * @param parameterName
   * @return
   */
  public static String parameQuestion(String sql, String[] parameterName) {
    for (int i = 0; i < parameterName.length; i++) {
      String string = parameterName[i];
      sql = sql.replace("#{" + string + "}", "?");
    }
    return sql;
  }
  public static String parameQuestion(String sql, List<String> parameterName) {
    for (int i = 0; i < parameterName.size(); i++) {
      String string = parameterName.get(i);
      sql = sql.replace("#{" + string + "}", "?");
    }
    return sql;
  }
}

4.单元测试

@Test
public void test() throws SQLException {
    UserMapper userMapper = SqlSession.getMapper(UserMapper.class);
    User selectUser = userMapper.selectUser("Dumas", "26");
    System.out.println(
            "结果:" + selectUser.getNAME() + "," + selectUser.getAge());
}

运行结果:

手写MyBatis教程中,也有xml手写版,可以参考:https://blog.csdn.net/qq_35859844/article/details/86688098

总结

目录
相关文章
|
22天前
|
SQL XML Java
【mybatis】第二篇:@Select注解中加入字段判断
【mybatis】第二篇:@Select注解中加入字段判断
|
22天前
|
存储 关系型数据库 MySQL
【mybatis-plus】Springboot+AOP+自定义注解实现多数据源操作(数据源信息存在数据库)
【mybatis-plus】Springboot+AOP+自定义注解实现多数据源操作(数据源信息存在数据库)
|
2月前
|
Java 数据库连接 mybatis
mybatis plus字段为null或空字符串把原来的数据也更新了,只需要注解
mybatis plus字段为null或空字符串把原来的数据也更新了,只需要注解
22 0
|
2月前
|
Java 数据库连接 网络安全
mybatis使用全注解的方式案例(包含一对多关系映射)
mybatis使用全注解的方式案例(包含一对多关系映射)
12 0
|
2月前
|
关系型数据库 Java 数据库连接
如何利用Mybatis-Plus自动生成代码(超详细注解)
如何利用Mybatis-Plus自动生成代码(超详细注解)
28 1
|
2月前
|
XML Java 数据库连接
Mybatis-Plus学习小项目及详细教程
Mybatis-Plus学习小项目及详细教程
|
3月前
|
Java 关系型数据库 数据库连接
【JavaEE进阶】 MyBatis使用注解实现增删改查
【JavaEE进阶】 MyBatis使用注解实现增删改查
|
3月前
|
XML 监控 druid
【Java专题_02】springboot+mybatis+pagehelper分页插件+druid数据源详细教程
【Java专题_02】springboot+mybatis+pagehelper分页插件+druid数据源详细教程
|
3月前
|
SQL Java 数据库连接
【MyBatisPlus】通俗易懂 快速入门 详细教程
【MyBatisPlus】通俗易懂 快速入门 详细教程
79 0
|
4月前
|
Java 数据库连接 数据库
JAVAEE框架技术之10-myBatis注解式开发
JAVAEE框架技术之10-myBatis注解式开发
55 0
JAVAEE框架技术之10-myBatis注解式开发