文档结构  
可译网翻译有奖活动正在进行中,查看详情 现在前往 注册?
原作者:Petri Kainulainen    来源:www.petrikainulainen.net [英文]
班纳睿    计算机    2017-01-03    0评/619阅
翻译进度:已翻译   参与翻译: 班纳睿 (14), CY2 (2), 行者江 (1)

我已经分享过关于ORM框架所导致的性能问题。尽管我不得不承认这些问题大部分都是由你自己造成的, 但我也在开始思考在只读操作里使用ORM是不值得的。

于是我开始寻找实现这些操作的替代方法。

接着我就遇到了 jOOQ ,它是这么介绍自己的:

jOOQ 可以从数据库生成Java代码,并允许你通过其流利的API构建类型安全的SQL查询。

这看起来相当有趣。这就是我为什么决定试一试 jOOQ ,并且把我的发现分享给大家。

这篇博文是我的 在Spring框架中使用 jOOQ 系列的第一部分。这里描述了我们将如何获取到所需的依赖并且配置好应用上下文。

第 1 段(可获 1.65 积分)

现在开始!

使用 Maven 获取所需的依赖包

此应用依赖以下项目:

  • Spring Framework 4.1.2.RELEASE. 本例子使用了 aop, beans, core, context, context-support, jdbc, 和 tx 模块.
  • cglib 3.1.
  • BoneCP 0.8.0. 我们使用的是 BoneCP 连接池
  • jOOQ 3.4.4.
  • H2 1.3.174. H2 作为数据库

如果你想获得更多关于 Spring 框架的信息,请浏览 section 2.2 of the Spring Framework Reference Documentation.

相对应的 pom.xml 部分内容如下:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>4.1.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-beans</artifactId>
    <version>4.1.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>4.1.2.RELEASE</version>
</Dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>4.1.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
    <version>4.1.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-expression</artifactId>
    <version>4.1.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>4.1.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>4.1.2.RELEASE</version>
</dependency>
         
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.1</version>
</dependency>
 
<dependency>
    <groupId>com.jolbox</groupId>
    <artifactId>bonecp</artifactId>
    <version>0.8.0.RELEASE</version>
</dependency>
 
<dependency>
    <groupId>org.jooq</groupId>
    <artifactId>jooq</artifactId>
    <version>3.4.4</version>
</dependency>
 
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.3.174</version>
</dependency>

 

第 2 段(可获 1.35 积分)

这篇文章的示例应用程序还有其他依赖项。你可以通过查看pom.xml文件来看到完整的依赖项列表。

让我们继续探索如何将jOOQ抛出的异常转换为Spring 的 DataAccessException。

将jOOQ异常转换为Spring的DataAccessException

为什么我们必须将jOOQ抛出的异常转换为Spring的 DataAccessException?

这样做的一个原因是,我们想让我们的集成跟Spring框架的DAO支持一样的方式工作。这种支持的一个重要部分就是拥有一致的异常层次结构

 

第 3 段(可获 1.24 积分)

Spring 可以很方便的把技术相关的异常比如 SQLException 翻译成它自己的异常类,这种异常类的层次结构是以 DataAccessException 作为根异常的。 这些异常将原始的异常包装起来,这样就不会有任何可能丢失掉包含有出错原因的信息的风险。

换句话说,如果我们想要我们的应用做“一个好公民”的话,就很有必要确保我们的配置能够将jOOQ抛出的异常转换为Spring的DataAccessException。

我们可以通过以下步骤创建一个提供这种功能的组件:

第 4 段(可获 1.14 积分)
  1. 创建一个JOOQToSpringExceptionTransformer 类,让该类继承DefaultExecuteListener 类。DefaultExecuteListener 类是 ExecuteListener 接口的公开默认实现,该接口为执行一个查询的不同的生命周期的事件提供了监听器方法。
  2. 重写 DefaultExecuteListener类的exception(ExecuteContext ctx)方法。 在执行生命周期的任何时刻如果有异常抛出,那么该方法就会被调用。可以通过以下步骤实现该方法:
    1. jOOQ 配置里获取一个 SQLDialect 对象。
    2. 创建一个实现了SQLExceptionTranslator 接口的对象,需要遵循以下规则:
      1. 如果配置好的SQL方言能够找到,就创建一个新的SQLErrorCodeSQLExceptionTranslator 对象,并且把该SQL方言的名字作为构造方法参数传给它。该类会通过分析供应商特定的错误代码,从而“选择”正确的DataAccessException
      2. 如果没有找到对应的SQL方言,就创建一个新的SQLStateSQLExceptionTranslator 对象。该类会通过分析存储在SQLException 中的SQL状态标识来“选择”正确的DataAccessException。
    3. 使用创建的SQLExceptionTranslator对象 创建一个DataAccessException 对象。
    4. 将抛出的DataAccessException 异常通过方法参数的形式传递给ExecuteContext 对象。
第 5 段(可获 2.2 积分)

JOOQToSpringExceptionTransformer 类的源代码如下所示:

import org.jooq.ExecuteContext;
import org.jooq.SQLDialect;
import org.jooq.impl.DefaultExecuteListener;
import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator;
import org.springframework.jdbc.support.SQLExceptionTranslator;
import org.springframework.jdbc.support.SQLStateSQLExceptionTranslator;


public class JOOQToSpringExceptionTransformer extends DefaultExecuteListener {

    @Override
    public void exception(ExecuteContext ctx) {
        SQLDialect dialect = ctx.configuration().dialect();
        SQLExceptionTranslator translator = (dialect != null)
                ? new SQLErrorCodeSQLExceptionTranslator(dialect.name())
                : new SQLStateSQLExceptionTranslator();

        ctx.exception(translator.translate("jOOQ", ctx.sql(), ctx.sqlException()));
    }
}

 

第 6 段(可获 0.13 积分)

这并不是我的观点。 我是从Adam Zell’s Gist这里看到的。

延伸阅读:

我们的任务还没有完成。我们把所有的片段放在一起,然后配置下我们的样例程序的应用上下文,这样才算圆满完成任务了。

配置应用上下文

这一部分解释了我们如何使用Java配置来配置我们应用程序的应用上下文。

让我们首先创建一个属性文件,它包含了我们样例程序的配置。

第 7 段(可获 1.25 积分)

实际的应用程序是基于Maven的配置(profiles)进行构建的。这就保证了我们可以在不同环境下使用不同的配置。你可以通过阅读我之前写的题为 使用Maven创建基于特定profile的配置文件的博文来得到更多的信息。

创建属性文件

我们可以按以下步骤来创建属性文件:

  1. 配置数据库连接。我们需要配置JDBC驱动类(driver class),JDBC 地址(url),数据库用户的用户名及其密码。
  2. 配置使用的SQL方言的名称。
  3. 配置创建样例程序的数据库SQL脚本的名字(如果你的应用程序不使用嵌入式数据库,则该步骤就不是必需的了)。
第 8 段(可获 1.54 积分)

application.properties 配置文件内容如下:

#Database Configuration
db.driver=org.h2.Driver
db.url=jdbc:h2:target/jooq-example
db.username=sa
db.password=

#jOOQ Configuration
jooq.sql.dialect=H2

#DB Schema
db.schema.script=schema.sql

这个页面 Javadoc of the SQLDialect Enum 说明了 jOOQ 支持的所有数据库列表。

接下来继续并找出如果通过使用 Java 配置来配置应用的上下文。

创建 Configuration 类

我们可以通过如下几步来配置应用的上下文:

第 9 段(可获 0.75 积分)
  1. 创建一个 PersistenceContext 的注解实现类。
  2. 给创建的类加上@Configuration 注解以确保它能被配置类识别到。
  3. 确保我们的应用的jOOQ 存储库能够在组件扫描时被找到。这一点我们可以通过给配置类增加@ComponentScan注解来完成。
  4. 通过给配置类增加@EnableTransactionManagement 注解来开启由注解驱动的事务管理。
  5. 确保我们应用的配置是从在类路径classpath中找到的application.properties 文件中加载的。我们可以给配置类增加@PropertySource 注解来实现这一点。
  6. 给配置类增加一个Environment 的属性,并且用 @Autowired 注解标识它。我们将会使用这个Environment 对象来得到从 application.properties 文件中加载的配置属性的值。
  7. 配置 DataSource bean。因为我们的应用使用了BoneCP,所以我们就需要创建一个新的 BoneCPDataSource 对象来作为我们的 DataSource bean。
  8. 配置 LazyConnectionDataSourceProxy bean。这个 bean 确保了数据库连接可以被懒加载的方式获取到(比如当第一个statement被创建的时候。
  9. 配置 TransactionAwareDataSourceProxy bean。这个 bean 可以保证所有的 JDBC 连接都会使用基于Spring管理的事务。换言之, JDBC 连接会自动加入跟线程绑定的事务。
  10. 配置 DataSourceTransactionManager bean。当我们创建一个新的DataSourceTransactionManager对象的时候,我们必须将LazyConnectionDataSourceProxy bean作为构造参数传递给它。
  11. 配置 DataSourceConnectionProvider bean。jOOQ 将会以构造参数的形式从DataSource 中获取将要使用的连接。当我们创建一个新的DataSourceConnectionProvider 对象时,我们必须将TransactionAwareDataSourceProxy bean作为构造参数传递给它。 这样就能确保有jOOQ创建的查询都会加入到由Spring管理的事务中了。
  12. 配置JOOQToSpringExceptionTransformer bean。
  13. 配置 DefaultConfiguration bean。这个类是Configuration 接口的默认实现,我们可以用它来配置jOOQ。我们需要配置三样东西:
    1. 我们需要设置 ConnectionProvider 用来获取和释放数据库连接。
    2. 还需要配置自定义执行监听器。换言之,我们需要将JOOQToSpringExceptionTransformer bean加入到已创建的DefaultConfiguration 对象中。这样可以确保所有由 jOOQ 抛出的异常都能被转换为Spring 的 DataAccessException
    3. 还要配置使用的SQL方言。
  14. 配置 DefaultDSLContext bean。我们会在使用jOOQ创建数据库查询时使用到它。
  15. 配置 DataSourceInitializer bean。在应用启动的时候我们使用它来创建H2数据库的数据库schema(当然,如果你不使用嵌入式数据库的话,那你就没必要配置这个bean了。)
第 10 段(可获 5.23 积分)

PersistenceContext 类的源代码如下所示:

import com.jolbox.bonecp.BoneCPDataSource;
import org.jooq.SQLDialect;
import org.jooq.impl.DataSourceConnectionProvider;
import org.jooq.impl.DefaultConfiguration;
import org.jooq.impl.DefaultDSLContext;
import org.jooq.impl.DefaultExecuteListenerProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
import org.springframework.jdbc.datasource.init.DataSourceInitializer;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

@Configuration
@ComponentScan({"net.petrikainulainen.spring.jooq.todo"})
@EnableTransactionManagement
@PropertySource("classpath:application.properties")
public class PersistenceContext {

    @Autowired
    private Environment env;

    @Bean(destroyMethod = "close")
    public DataSource dataSource() {
        BoneCPDataSource dataSource = new BoneCPDataSource();

        dataSource.setDriverClass(env.getRequiredProperty("db.driver"));
        dataSource.setJdbcUrl(env.getRequiredProperty("db.url"));
        dataSource.setUsername(env.getRequiredProperty("db.username"));
        dataSource.setPassword(env.getRequiredProperty("db.password"));

        return dataSource;
    }

    @Bean
    public LazyConnectionDataSourceProxy lazyConnectionDataSource() {
        return new LazyConnectionDataSourceProxy(dataSource());
    }

    @Bean
    public TransactionAwareDataSourceProxy transactionAwareDataSource() {
        return new TransactionAwareDataSourceProxy(lazyConnectionDataSource());
    }

    @Bean
    public DataSourceTransactionManager transactionManager() {
        return new DataSourceTransactionManager(lazyConnectionDataSource());
    }

    @Bean
    public DataSourceConnectionProvider connectionProvider() {
        return new DataSourceConnectionProvider(transactionAwareDataSource());
    }

    @Bean
    public JOOQToSpringExceptionTransformer jooqToSpringExceptionTransformer() {
        return new JOOQToSpringExceptionTransformer();
    }

    @Bean
    public DefaultConfiguration configuration() {
        DefaultConfiguration jooqConfiguration = new DefaultConfiguration();

        jooqConfiguration.set(connectionProvider());
        jooqConfiguration.set(new DefaultExecuteListenerProvider(
            jooqToSpringExceptionTransformer()
        ));

        String sqlDialectName = env.getRequiredProperty("jooq.sql.dialect");
        SQLDialect dialect = SQLDialect.valueOf(sqlDialectName);
        jooqConfiguration.set(dialect);

        return jooqConfiguration;
    }

    @Bean
    public DefaultDSLContext dsl() {
        return new DefaultDSLContext(configuration());
    }

    @Bean
    public DataSourceInitializer dataSourceInitializer() {
        DataSourceInitializer initializer = new DataSourceInitializer();
        initializer.setDataSource(dataSource());

        ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
        populator.addScript(
                new ClassPathResource(env.getRequiredProperty("db.schema.script"))
        );

        initializer.setDatabasePopulator(populator);
        return initializer;
    }
}

 

第 11 段(可获 0.13 积分)

如果你想使用XML配置文件来配置应用上下文,这个样例程序也提供了一个可以工作的 XML配置文件

更多信息:

那么我们如何知道这个配置文件起作用了呢?是个好问题。我们会在接下来讨论这个问题。

这真的能起作用吗?

第 12 段(可获 1.19 积分)

当我开始调查如何可以让使用jOOQ创建的数据库查询能够参与到由Spring管理的事务中的时候,我注意到这并非一个容易解决的问题

这篇博文的样例程序中有一些集成测试展示了在非常简单的场景下实现事务(包括提交和回滚)的过程。然而,当我们使用这篇文章中描述的方案时,有两件事我们必须要考虑在内:

1. 所有使用jOOQ创建的数据库查询必须在一个事务内执行。

TransactionAwareDataSourceProxy类 的javadoc 里面写道:

第 13 段(可获 1.24 积分)

用于自动加入线程绑定事务的DataSourceUtils的代理, 例如在返回的Connections上执行DataSourceTransactionManager.getConnection 和 close 调用所管理的代理将会在同一个事务内正常起作用,比如总是在有事务的连接上执行操作。如果不是在一个事务内,正常的DataSource行为就会起作用。

换言之,如果没有使用事务去执行多个复杂的操作,jOOQ将会为每一个操作使用不同的连接,从而将会导致竞态条件下的bug。

我是在阅读Ben Manes写的这条评论时发现这个问题的。

第 14 段(可获 1.08 积分)

2. TransactionAwareDataSourceProxy 在它的Javadoc里声明了不推荐使用它。

TransactionAwareDataSourceProxy类的Javadoc 里有这么一段话:

这个代理可以使数据访问代码能够和普通的JDBC API一起工作,并且能够加入由Spring管理的事务,就跟J2EE/JTA环境下的JDBC代码很类似。然而,如果可能,请尽量使用Spring的DataSourceUtilsJdbcTemplate或JDBC操作对象去获取事务的参与,即使没有使用对目标DataSource的代理,也尽量不要一开始就考虑定义如此一个代理。

第 15 段(可获 1.08 积分)

这是一段非常模糊的评论,因为它并没有解释为什么我们不应该使用它。 Adam Zell则认为是由于这个类使用了反射,使用它可能会有性能问题。

如果你遇到了性能问题,你也许可以试试在Adam Zell的Gist里描述的方法。

总结

我们现在已经成功了配置好了我们的示例程序的应用上下文。这篇教程教会了我们四件事情:

  • 我们学会了如何使用Maven得到所需的依赖。
  • 我们学会了如何将由jOOQ抛出的异常转换为Spring的DataAccessExceptions。
  • 我们还学会了如何使用配置使用jOOQ和Spring的应用程序的应用上下文。
  • 我们还快速浏览了当我们使用这篇博文中提到的那些方法时那些我们需要留意的东西。
第 16 段(可获 1.73 积分)

这套教程的下一部分将会描述如何使用jOOQ的代码生成支持功能。

另外,你可以从Github得到这篇博文里提到的示例程序的源代码

第 17 段(可获 0.38 积分)

文章评论