小马哥JAVA实战营-JDBC

小马哥是一个非常牛逼的技术大牛,最近在看他的课,感兴趣也可以关注一波小马哥(不是引流,是真的很推荐):
小马哥B站

JDBC规范文档

jdbc规范文档下载链接

JDBC的主要特征

  • 面向数据表行列编程
  • 驱动程序需要数据库定制(MySQL、Oracle)
  • SQL 语法与目标数据库保持一致
  • 事务(需要数据库支持)
  • 数据库元信息 (数据库信息、表结构信息等)

JDBC 核心API

  • 数据源接口 -javaxsql.DataSource
    主流实现:
  1. Apache DBCP 1/2 DPCP文档 目前已经基本不使用了作为了解
    DBCP 的实现间接依赖-Apache Commons Pool(池化)里面有对象池。
    对象池的概念
    池化 资源少(线程资源,数据库资源,IO资源)比如线程池,数据库连接池 ;消费者多
    池化的特点是有借有还,其核心思想是生产者和消费者模型。
  2. C3P0
  3. Alibaba Druid(字节码提升/优化)
  4. HicariCP

获取方式

  1. 普通对象初始化
    Spring Bean 通过spirng来管理
    API 实现 通过api 来实例化
  2. JNDI 依赖查找 JNDI的一些资料
  • JDBC 驱动接口java.sql.Driver

  • 驱动动管理器接口 -java.sql.DriverManager(重点)
    用于管理driver,有三种获取Driver的方式

  1. Class.forName 的时候,会在驱动管理器进行注册源码如下:
// classforName 的时候会加载这个类,类加载的时候静态方法执行就会注册驱动
package com.mysql.cj.jdbc;
import java.sql.DriverManager;
import java.sql.SQLException;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }
    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}
  1. 同时DriverManager 加载的时候也会注册驱动,通过系统属性,是JVM共享的
    读取系统属性jdbc.drivers后,再经过":" 分割尝试获取多值,在通过classLoader获取实现类,意思是驱动类用:分割开
static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

可以看到加载了系统属性jdbc.drivers

drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
    public String run() {
        return System.getProperty("jdbc.drivers");
    }
});

可以看到最终还是通过类加载从而注册了驱动,和第一种方式的原理几乎一样

String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
    try {
        println("DriverManager.Initialize: loading " + aDriver);
        Class.forName(aDriver, true,
                ClassLoader.getSystemClassLoader());
    } catch (Exception ex) {
        println("DriverManager.Initialize: load failed: " + ex);
    }
}
  1. 还有一种获取方式是通过 java SPI 来获取Driver ServiceLoader通过这个类去加载,需要在静态资源目录下放入类的实现,然后加载。
    小马哥JAVA实战营-JDBC_第1张图片

小马哥JAVA实战营-JDBC_第2张图片

那么是怎么拿到一个连接呢,方式是循环的区测试连接是否可用从而拿到一个连接,主要是通过connect方法去验证连接的可用性, 源码如下:

private static Connection getConnection(
     String url, java.util.Properties info, Class<?> caller) throws SQLException {
     /*
      * When callerCl is null, we should check the application's
      * (which is invoking this class indirectly)
      * classloader, so that the JDBC driver class outside rt.jar
      * can be loaded from here.
      */
     ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
     synchronized(DriverManager.class) {
         // synchronize loading of the correct classloader.
         if (callerCL == null) {
             callerCL = Thread.currentThread().getContextClassLoader();
         }
     }
     if(url == null) {
         throw new SQLException("The url cannot be null", "08001");
     }
     println("DriverManager.getConnection(\"" + url + "\")");
     // Walk through the loaded registeredDrivers attempting to make a connection.
     // Remember the first exception that gets raised so we can reraise it.
     SQLException reason = null;

     for(DriverInfo aDriver : registeredDrivers) {
         // If the caller does not have permission to load the driver then
         // skip it.
         if(isDriverAllowed(aDriver.driver, callerCL)) {
             try {
                 println("    trying " + aDriver.driver.getClass().getName());
                 Connection con = aDriver.driver.connect(url, info);
                 if (con != null) {
                     // Success!
                     println("getConnection returning " + aDriver.driver.getClass().getName());
                     return (con);
                 }
             } catch (SQLException ex) {
                 if (reason == null) {
                     reason = ex;
                 }
             }
         } else {
             println("    skipping: " + aDriver.getClass().getName());
         }
     }
     // if we got here nobody could connect.
     if (reason != null)    {
         println("getConnection failed: " + reason);
         throw reason;
     }
     println("getConnection: no suitable driver found for "+ url);
     throw new SQLException("No suitable driver found for "+ url, "08001");
 }

课堂问题:

  1. 当多个driver同时被加载到classLoader后,到底用那一个?
    我们可以看到上面源码里面是通过url来判断的,如果尝试连接成功,则返回成功的,也就是一直尝试到第一个成功的为止。
  2. java SPI加载遍历的时候应该注意配置的时候要是配置错误就会导致错误后面的Driver无法加载?
    是的,因为加载的时候next() 逻辑里面也是class.forName(),所以如果抛出异常下面的while语句不在执行,后面的驱动类就无法加载
AccessController.doPrivileged(new PrivilegedAction<Void>() {
          public Void run() {

              ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
              Iterator<Driver> driversIterator = loadedDrivers.iterator();

              /* Load these drivers, so that they can be instantiated.
               * It may be the case that the driver class may not be there
               * i.e. there may be a packaged driver with the service class
               * as implementation of java.sql.Driver but the actual class
               * may be missing. In that case a java.util.ServiceConfigurationError
               * will be thrown at runtime by the VM trying to locate
               * and load the service.
               *
               * Adding a try catch block to catch those runtime errors
               * if driver not available in classpath but it's
               * packaged as service and that service is there in classpath.
               */
              try{
              	// 这里如果存在异常,则while循环终止了
                  while(driversIterator.hasNext()) {
                      driversIterator.next();
                  }
              } catch(Throwable t) {
              // Do nothing
              }
              return null;
          }
      });

小马哥JAVA实战营-JDBC_第3张图片
小马哥JAVA实战营-JDBC_第4张图片
总结一下:驱动管理里面加载是同步进行的,如果系统属性jdbc.drviers 配置了或配置了spi都会进行加载,每个驱动具体的实现类在类加载的时候会向驱动管理注册驱动。

  • 数据连接接口 -java.sql.Connection
    连接可以创建Statement,Statement的主要类型
  1. 普通SQL-java.sql.Statement
  2. 预编译SQL命令-java.sql.PrepareStatement
  3. 存储过程SQL命令-java.sql.CallableStatement
  • SQL 命令接口 java.sql.Statement
    DDL语句:crud 一般用executeUpdate()
    如果用execute方法
    如果成功的话不需要返回值(或者是返回false) 如果失败SQLException。也就是我们用这个方法的时候只需要关注是否抛异常,不抛出异常则是成功。
    小马哥JAVA实战营-JDBC_第5张图片
    下面来借用derby 来演示一下executeUpdate() 和 execute() 的区别
public static final String DROP_USERS_TABLE_DDL_SQL = "DROP TABLE users";

public static final String CREATE_USERS_TABLE_DDL_SQL = "CREATE TABLE users(" +
     "id INT NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY (START WITH 1, INCREMENT BY 1), " +
     "name VARCHAR(16) NOT NULL, " +
     "password VARCHAR(64) NOT NULL, " +
     "email VARCHAR(64) NOT NULL, " +
     "phoneNumber VARCHAR(64) NOT NULL" +
     ")";

public static final String INSERT_USER_DML_SQL = "INSERT INTO users(name,password,email,phoneNumber) VALUES " +
     "('A','******','[email protected]','1') , " +
     "('B','******','[email protected]','2') , " +
     "('C','******','[email protected]','3') , " +
     "('D','******','[email protected]','4') , " +
     "('E','******','[email protected]','5')";
// /db/user-platform这是一个目录,运行后java会在当前程序根目录下创建derby 内存数据库
String databaseURL = "jdbc:derby:/db/user-platform;create=true";
Connection connection = DriverManager.getConnection(databaseURL);
Statement statement = connection.createStatement();
// 删除 users 表 运行结果为false
System.out.println(statement.execute(DROP_USERS_TABLE_DDL_SQL));
// 创建 users 表 运行结果为false
System.out.println(statement.execute(CREATE_USERS_TABLE_DDL_SQL));
// 运行结果为5
System.out.println(statement.executeUpdate(INSERT_USER_DML_SQL)); 

在这里插入图片描述
关于Derby数据库:添加链接描述
ORM 的核心思想(反射),代码demo

static Map<Class, String> typeMethodMappings = new HashMap<Class, String>() {{
     put(Long.class, "getLong");
     put(String.class, "getString");
 }};
 public static void main(String[] args) throws Exception {
     String databaseURL = "jdbc:derby:/db/user-platform;create=true";
     Connection connection = DriverManager.getConnection(databaseURL);
     Statement statement = connection.createStatement();
     // 执行查询语句(DML)
     ResultSet resultSet = statement.executeQuery("SELECT id,name,password,email,phoneNumber FROM users");
     // BeanInfo user这个类的属性及类型 userBeanInfo.getPropertyDescriptors() 可以通过这个方法拿到所有的属性 第二个参数是stop class 到那个父类停止 如果有继承关系会获取所有的属性包含父类的
     BeanInfo userBeanInfo = Introspector.getBeanInfo(User.class, Object.class);
     List<User> users = new ArrayList<>();
     // 如果存在并且游标滚动
     while (resultSet.next()) {
         User user = new User();
         for (PropertyDescriptor propertyDescriptor : userBeanInfo.getPropertyDescriptors()) {
             String fieldName = propertyDescriptor.getName();
             Class<?> type = propertyDescriptor.getPropertyType();
          	 // 如果存在 列名的映射也可以在这里写一个map来映射
             String methodName = typeMethodMappings.get(type);
             // long getLong(String columnLabel) throws SQLException; 里面只有一个参数且为String 所以第二个参数填String.class
             Method resultMethod = resultSet.getClass().getMethod(methodName, String.class);
             Object resultVal = resultMethod.invoke(resultSet, fieldName);
             // propertyDescriptor.getWriteMethod() 就是set方法
             // propertyDescriptor.getReadMethod() 就是read方法
             Method writeMethod = propertyDescriptor.getWriteMethod();
             writeMethod.invoke(user, resultVal);
         }
         users.add(user);
         }
    connection.close();
     }
  • SQL 执行结果接口 -java.sql.ResultSet
    结果集,返回查询的结果集合
  • 表元数据接口 -java.sql.ResultSetMetaData
    这个接口的应用,比如逆向工程,可以生成一些sql 和 类,下面看一个小demo看如何生成sql的,如何生成类等研究源码后来补充
String databaseURL = "jdbc:derby:/db/user-platform;create=true";
Connection connection = DriverManager.getConnection(databaseURL);
Statement statement = connection.createStatement();
// 执行查询语句(DML)
ResultSet resultSet = statement.executeQuery("SELECT id,name,password,email,phoneNumber FROM users");
// ResultSetMetaData 元信息
ResultSetMetaData metaData = resultSet.getMetaData();
// 获取当前的表名称 它的参数是column 填任何列的数量都是返回表名称
System.out.println("当前表的名称:" + metaData.getTableName(1));
System.out.println("当前表的列个数:" + metaData.getColumnCount());
for (int i = 1; i <= metaData.getColumnCount(); i++) {
    System.out.println("列名称:" + metaData.getColumnLabel(i) + ", 类型:" + metaData.getColumnClassName(i));
}
// 反向生成生成sql
StringBuilder builder = new StringBuilder("SELECT ");
for (int i = 1; i <= metaData.getColumnCount(); i++) {
   builder.append(metaData.getColumnLabel(i)).append(",");
}
// 去掉最后一个逗号
builder.deleteCharAt(builder.length() - 1);
builder.append(" FROM ").append(metaData.getTableName(1));
System.out.println(builder.toString());
connection.close();

小马哥JAVA实战营-JDBC_第6张图片

  • SQL 执行异常 java.sql.SQLException
    基本特点:
  1. 几乎所有的JDBC API操作都需要try catch java.sql.SQLException
  2. java.sql.SQLException 属于检查类型异常,继承Exception
    下面代码是直接复制的小马哥的代码,着重理解getAll() 方法,这里演示了如果自己手写JDBCTemplate,需要怎样实现并处理异常,对于我这个小菜来说收货颇多。
package org.geektimes.projects.user.repository;
import org.geektimes.context.ClassicComponentContext;
import org.geektimes.function.ThrowableFunction;
import org.geektimes.projects.user.domain.User;
import org.geektimes.projects.user.sql.DBConnectionManager;
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.*;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import static org.apache.commons.lang.ClassUtils.wrapperToPrimitive;
public class DatabaseUserRepository implements UserRepository {
  private static Logger logger = Logger.getLogger(DatabaseUserRepository.class.getName());
  /**
   * 通用处理方式,处理异常
   */
  private static Consumer<Throwable> COMMON_EXCEPTION_HANDLER = e -> logger.log(Level.SEVERE, e.getMessage());
  public static final String INSERT_USER_DML_SQL =
          "INSERT INTO users(name,password,email,phoneNumber) VALUES " +
                  "(?,?,?,?)";
  public static final String QUERY_ALL_USERS_DML_SQL = "SELECT id,name,password,email,phoneNumber FROM users";
  private final DBConnectionManager dbConnectionManager;
  public DatabaseUserRepository() {
      this.dbConnectionManager = ClassicComponentContext.getInstance().getComponent("bean/DBConnectionManager");
  }
  private Connection getConnection() {
      return dbConnectionManager.getConnection();
  }
  @Override
  public boolean save(User user) {
      return false;
  }
  @Override
  public boolean deleteById(Long userId) {
      return false;
  }
  @Override
  public boolean update(User user) {
      return false;
  }
  @Override
  public User getById(Long userId) {
      return null;
  }
  @Override
  public User getByNameAndPassword(String userName, String password) {
      return executeQuery("SELECT id,name,password,email,phoneNumber FROM users WHERE name=? and password=?",
              resultSet -> {
                  // TODO
                  return new User();
              }, COMMON_EXCEPTION_HANDLER, userName, password);
  }
  @Override
  public Collection<User> getAll() {
      return executeQuery("SELECT id,name,password,email,phoneNumber FROM users", resultSet -> {
          // BeanInfo -> IntrospectionException
          BeanInfo userBeanInfo = Introspector.getBeanInfo(User.class, Object.class);
          List<User> users = new ArrayList<>();
          // 如果存在并且游标滚动 
          while (resultSet.next()) { 
              User user = new User();
              for (PropertyDescriptor propertyDescriptor : userBeanInfo.getPropertyDescriptors()) {
                  String fieldName = propertyDescriptor.getName();
                  Class fieldType = propertyDescriptor.getPropertyType();
                  String methodName = resultSetMethodMappings.get(fieldType);
                  // 可能存在映射关系(不过此处是相等的) 方法映射的是表字段和对象的字段
                  String columnLabel = mapColumnLabel(fieldName);
                  Method resultSetMethod = ResultSet.class.getMethod(methodName, String.class);
                  // 通过放射调用 getXXX(String) 方法
                  Object resultValue = resultSetMethod.invoke(resultSet, columnLabel);
                  // 获取 User 类 Setter方法
                  // PropertyDescriptor ReadMethod 等于 Getter 方法
                  // PropertyDescriptor WriteMethod 等于 Setter 方法
                  Method setterMethodFromUser = propertyDescriptor.getWriteMethod();
                  // 以 id 为例,  user.setId(resultSet.getLong("id"));
                  setterMethodFromUser.invoke(user, resultValue);
              }
          }
          return users;
      }, e -> {
          // 异常处理
      });
  }
  /**
   * @param sql
   * @param function
   * @param 
   * @return
   */
  protected <T> T executeQuery(String sql, ThrowableFunction<ResultSet, T> function,
                               Consumer<Throwable> exceptionHandler, Object... args) {
      Connection connection = getConnection();
      try {
          PreparedStatement preparedStatement = connection.prepareStatement(sql);
          for (int i = 0; i < args.length; i++) {
              Object arg = args[i];
              Class argType = arg.getClass();
              // 获取原生的类型 拆箱后的类型
              Class wrapperType = wrapperToPrimitive(argType);
              if (wrapperType == null) {
                  wrapperType = argType;
              }
              // 这里也是把set方法映射一下
              String methodName = preparedStatementMethodMappings.get(argType);
              Method method = PreparedStatement.class.getMethod(methodName, wrapperType);
              method.invoke(preparedStatement, i + 1, arg);
          }
          ResultSet resultSet = preparedStatement.executeQuery();
          return function.apply(resultSet);
      } catch (Throwable e) {
          // 异常处理
          exceptionHandler.accept(e);
      }
      return null;
  }
  private static String mapColumnLabel(String fieldName) {
      return fieldName;
  }
  /**
   * 数据类型与 ResultSet 方法名映射
   */
  static Map<Class, String> resultSetMethodMappings = new HashMap<>();

  static Map<Class, String> preparedStatementMethodMappings = new HashMap<>();
  static {
      resultSetMethodMappings.put(Long.class, "getLong");
      resultSetMethodMappings.put(String.class, "getString");
      // long
      preparedStatementMethodMappings.put(Long.class, "setLong");
      // string
      preparedStatementMethodMappings.put(String.class, "setString");
  }
}

里面用到了小马哥设计的可以抛出异常的Function,小马哥dubbo 源码里面复制过来的代码如下:

@FunctionalInterface
public interface ThrowableFunction<T, R> {

   /**
    * Applies this function to the given argument.
    *
    * @param t the function argument
    * @return the function result
    * @throws Throwable if met with any error
    */
   R apply(T t) throws Throwable;

   /**
    * Executes {@link ThrowableFunction}
    *
    * @param t the function argument
    * @return the function result
    * @throws RuntimeException wrappers {@link Throwable}
    */
   default R execute(T t) throws RuntimeException {
       R result = null;
       try {
           result = apply(t);
       } catch (Throwable e) {
           throw new RuntimeException(e.getCause());
       }
       return result;
   }
   /**
    * Executes {@link ThrowableFunction}
    *
    * @param t        the function argument
    * @param function {@link ThrowableFunction}
    * @param       the source type
    * @param       the return type
    * @return the result after execution
    */
   static <T, R> R execute(T t, ThrowableFunction<T, R> function) {
       return function.execute(t);
   }
}
  • 事务保护点接口 -java.sgl.Savepoint
    如果我们记录了保护点可以从那个点进行回滚
    下面这两个方法是关于事务的保存点和回滚的位于Connection接口里面
Savepoint setSavepoint(String name) throws SQLException;
void rollback(Savepoint savepoint) throws SQLException;

你可能感兴趣的:(学习笔记,Java,java,jdbc,反射,数据库,sql,ORM,SQL驱动管理)