数据库操作(jdbc)

前言

在构建一个系统的过程中难免需要对数据存储,而存储一般会有缓存(内存)、数据库(硬盘)两种存储介质。

本篇文章我们主要来介绍下在我们通过spring构建应用的过程中如何进行数据库连接、以及数据库连接的几种方式进行简单介绍。

spring中连接数据库有如下几种方式:

  • 直接通过驱动连接数据库的方式

  • spring提供的JdbcTemplate

  • spring集成Mybatis,通过Mybatis的方式进行数据库连接

原始JDBC方式

一般初学者在学到jdbc这个阶段都会动手写下下面这样的链接数据库的代码,只需三步就可以从数据库总拿到数据,这个时候是不是在窃喜终于按照教程把数据拿出来了。见下面代码:

Class.forName("org.apache.phoenix.jdbc.PhoenixDriver");
Connection connection = DriverManager.getConnection("jdbc:phoenix:10.1.168.1:2181/hbase");
ResultSet rs = connection.createStatement().executeQuery("select * from table limit 10 ");

针对上面的链接数据的代码来深入挖下为什么这样就能连接上数据库:

  • org.apache.phoenix.jdbc.PhoenixDriver这个类在在加载(也就是执行Class.forName(driver))的过程中会执行其静态代码块DriverManager.registerDriver(INSTANCE);

  • 执行registerDriver方法后会往静态registeredDrivers list中添加PhoenixDriver类。

public static synchronized void registerDriver(java.sql.Driver driver,
            DriverAction da)
        throws SQLException {

        /* Register the driver if it has not already been added to our list */
        if(driver != null) {
            registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
        } else {
            // This is for compatibility with the original DriverManager
            throw new NullPointerException();
        }

        println("registerDriver: " + driver);

    }
  • 后面就是进行连接操作DriverManager.getConnection(url)方法源代码如下:

//  Worker method called by the public getConnection() methods.
    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");
    }


}

for(DriverInfo aDriver : registeredDrivers)去遍历所有的Drivers然后创建连接,获得连接了后面就可以开始进行数据库操作了。

JdbcTemplate方式

这种方式是spring针对原始的JDBC的方式进行了一层封装将所有的操作都托管给了DataSource。

1、直接通过JdbcTemplate

获取JdbcTemplate

在spring中我们可以通过配置文件生成JdbcTemplate:

<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource"></property>
</bean>

或者

@Bean
public JdbcTemplate getJdbcTemplate() {
    DruidDataSource druidDataSource = new DruidDataSource();
    return new JdbcTemplate(druidDataSource);
}

获得了JdbcTemplate就可以拿他进行数据库操作了。

操作数据库
SqlRowSet rowSet = jdbcTemplate.queryForRowSet(sql);

通过上面的范式就可以获取数据库中的数据了。这个可以换成queryForXXX(sql)更多查询方式

2、间接通过JdbcTemplate的方式

对于一个普通的类通过继承org.springframework.jdbc.core.support.JdbcDaoSupport这个类,然后向类中注入DataSource就可以实现JDBC的功能了。

<bean id = "exampleDao" class = "com.liutxer.ExampleDao">
    <property name="dataSource" ref = "dataSource"></property>
</bean>

这样类exampleDao 通过org.springframework.jdbc.core.support.JdbcDaoSupport#getJdbcTemplate这个方法获得JdbcTemplate进行数据库操作。

下面看下为什么会这样?

其实原理比较简单JdbcDaoSupport这个类组合了JdbcTemplate在进行DataSource注入的时候会去创建一个JdbcTemplate,后面就可以通过JdbcDaoSupport#getJdbcTemplate方法拿到创建好的实例操作数据库了。

Mybatis的方式

1、SqlSessionTemplate的方式

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean" p:dataSource-ref="dataSource"
          p:mapperLocations="classpath*:phoenix/**/*.xml"/>
<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate" c:_0-ref="sqlSessionFactory"
          scope="prototype"/>

通过上面的方式获得操作数据的句柄(sqlSessionTemplate),示例通过句柄操作数据获得数据。
sqlSessionTemplate.selectList()这里换成selectxxx()等,具体可以参见org.mybatis.spring.SqlSessionTemplate中的方法。

2、MapperFactoryBean生成mapper

<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
  <property name="mapperInterface" value="com.liutxer.dao.UserMapper" />
  <property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>

使用上面的方式会生成一个MapperFactoryBean在Spring中获取userMapper对象的时候会自动通过MapperFactoryBean创建出来,这样就可以直接使用userMapper中的接口方法去查询数据库。

注意1:mapperInterface接口中的接口名称必须和mapper.xml配置中id一致,这样才能匹配到具体的sql语句。

注意2:同时如果接口中参数名称和sql语句中参数不一致可以通过在接口中加入注解@Param("code")来进行参数匹配List<User> findUserById(@Param("id") String userId);

注意3:Mybatis从数据库中拿到数据会自动进行a_b => aB的匹配,所以代码中用驼峰数据库中用下划线的方式,Mybatis能够进行自动匹配

3、MapperScannerConfigurer自动生成mapper

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="org.mybatis.spring.sample.mapper" />
    <!-- optional unless there are multiple session factories defined -->
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
</bean>

这种方式原理和上面那种方式很相似都是通过MapperFactoryBean来生成mapperMapperScannerConfigurerspring启动的过程中会去扫描basePackage下面所有的接口动态生成MapperFactoryBean

注:sqlSessionFactoryBeanName这个参数不是必须得,如果spring容器中有多个sqlSessionFactory才需要明确指出来

为什么要使用这种方式?

这种方式主要是解决第二种方式针对每个mapper接口都要进行一次匹配操作,而导致配置拖沓。

扒开外衣,还原本质(Mybatis)

其实Mybatis三种实现数据库操作的方式最终都是通过sqlSessionTemplate来操作数据库的。为什么这么说,下面来一层层剖析。
org.mybatis.spring.mapper.MapperScannerConfigurer#postProcessBeanDefinitionRegistry这个方法在spring容器启动的过程中会被调用,函数体:

@Override
  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    if (this.processPropertyPlaceHolders) {
      processPropertyPlaceHolders();
    }

    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
    scanner.setAddToConfig(this.addToConfig);
    scanner.setAnnotationClass(this.annotationClass);
    scanner.setMarkerInterface(this.markerInterface);
    scanner.setSqlSessionFactory(this.sqlSessionFactory);
    scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
    scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
    scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
    scanner.setResourceLoader(this.applicationContext);
    scanner.setBeanNameGenerator(this.nameGenerator);
    scanner.registerFilters();
    scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
  }

定义了一个scanner(ClassPathMapperScanner),最后会调用scanner.scan进行扫描basePackage,跟踪调用层次关系最后会调用到org.mybatis.spring.mapper.ClassPathMapperScanner#processBeanDefinitions方法

private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
    GenericBeanDefinition definition;
    for (BeanDefinitionHolder holder : beanDefinitions) {
      definition = (GenericBeanDefinition) holder.getBeanDefinition();

      if (logger.isDebugEnabled()) {
        logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName() 
          + "' and '" + definition.getBeanClassName() + "' mapperInterface");
      }

      // the mapper interface is the original class of the bean
      // but, the actual class of the bean is MapperFactoryBean
      definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59
      definition.setBeanClass(this.mapperFactoryBean.getClass());

      definition.getPropertyValues().add("addToConfig", this.addToConfig);

      boolean explicitFactoryUsed = false;
      if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
        definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
        explicitFactoryUsed = true;
      } else if (this.sqlSessionFactory != null) {
        definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
        explicitFactoryUsed = true;
      }

      if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
        if (explicitFactoryUsed) {
          logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
        }
        definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
        explicitFactoryUsed = true;
      } else if (this.sqlSessionTemplate != null) {
        if (explicitFactoryUsed) {
          logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
        }
        definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
        explicitFactoryUsed = true;
      }

      if (!explicitFactoryUsed) {
        if (logger.isDebugEnabled()) {
          logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
        }
        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
      }
    }
  }

实例化一个GenericBeanDefinition放到容器,从方法体可以看到definition.setBeanClass(this.mapperFactoryBean.getClass());
definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);很明显了这就是在实例化一个MapperFactoryBean对象。

下面转战MapperFactoryBean类,发现进行实例化的时候,设置SqlSessionFactory对象的时候进行了SqlSessionTemplate的实例化。

    if (!this.externalSqlSession) {
      this.sqlSession = new SqlSessionTemplate(sqlSessionFactory);
    }
  }

这里就生成了一个SqlSessionTemplate 来进行数据库操作。

再来彻底点转战SqlSessionTemplate的实现,new SqlSessionTemplate(sqlSessionFactory)
最后会调用到

public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
    notNull(executorType, "Property 'executorType' is required");

    this.sqlSessionFactory = sqlSessionFactory;
    this.executorType = executorType;
    this.exceptionTranslator = exceptionTranslator;
    this.sqlSessionProxy = (SqlSession) newProxyInstance(
        SqlSessionFactory.class.getClassLoader(),
        new Class[] { SqlSession.class },
        new SqlSessionInterceptor());
  }

这个方法体重点是this.sqlSessionProxy这个属性,由JDK提供的动态代理来动态实例恶化SqlSession.class这里SqlSessionInterceptor通过openSession创建SqlSession

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

主要是实例化DefaultSqlSession执行器executor

调用sqlSessionTemplate.selectList方法,最终调用

@Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

调用链继续,后面调用:

@Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
      throws SQLException {
    Statement stmt = null;
    try {
      flushStatements();
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameterObject, rowBounds, resultHandler, boundSql);
      Connection connection = getConnection(ms.getStatementLog());
      stmt = handler.prepare(connection);
      handler.parameterize(stmt);
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

看到了什么?

ConnectionStatementHandler,又回到了我们最开始讨论的纯JDBC方式从数据库中获取数据。再往下异步就可以看到

@Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.<E> handleResultSets(ps);
  }

获取结果的步骤了。

总结

好吧!写了这么多,感觉也有点乱了,各位看官能看到这里也说明足够有耐性了。JdbcTemplate的深挖就不继续了,比起Mybatis这种封装方式轻量级太多,往下扒两层就出来了。

坚持深挖源码的习惯,保持好的学习方式。

阅读 1.8k

推荐阅读
liutxer的点滴技术
用户专栏

本专栏主要用来和大家分享关于我的技术人生

4 人关注
26 篇文章
专栏主页