數據庫操做(jdbc)

前言

在構建一個系統的過程當中不免須要對數據存儲,而存儲通常會有緩存(內存)、數據庫(硬盤)兩種存儲介質。html

本篇文章咱們主要來介紹下在咱們經過spring構建應用的過程當中如何進行數據庫鏈接、以及數據庫鏈接的幾種方式進行簡單介紹。java

spring中鏈接數據庫有以下幾種方式:spring

  • 直接經過驅動鏈接數據庫的方式sql

  • spring提供的JdbcTemplate數據庫

  • spring集成Mybatis,經過Mybatis的方式進行數據庫鏈接apache

原始JDBC方式

通常初學者在學到jdbc這個階段都會動手寫下下面這樣的連接數據庫的代碼,只需三步就能夠從數據庫總拿到數據,這個時候是否是在竊喜終於按照教程把數據拿出來了。見下面代碼:api

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);session

  • 執行registerDriver方法後會往靜態registeredDrivers list中添加PhoenixDriver類。mybatis

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。

一、直接經過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)更多查詢方式

二、間接經過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的方式

一、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中的方法。

二、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可以進行自動匹配

三、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這種封裝方式輕量級太多,往下扒兩層就出來了。

堅持深挖源碼的習慣,保持好的學習方式。

相關文章
相關標籤/搜索