Статьи

Использование jOOQ с Spring: Конфигурация

У меня была доля проблем с производительностью, вызванных ORM. Хотя я должен признать, что большинство этих проблем действительно были вызваны вашей, я начал думать, что использование ORM в операциях только для чтения не стоит.

Я начал искать альтернативные способы реализации этих операций.

Вот как я столкнулся с JOOQ, который утверждает, что:

jOOQ генерирует код Java из вашей базы данных и позволяет вам создавать безопасные для SQL запросы с помощью своего свободного API.

Это выглядит очень интересно, и я решил попробовать jOOQ и поделиться своими результатами с вами.

Этот пост является первой частью моей серии «Использование jOOQ с Spring» и описывает, как мы можем получить необходимые зависимости и настроить контекст приложения нашего приложения.

Давайте начнем.

Получение необходимых зависимостей с Maven

Зависимости нашего приложения:

  • Spring Framework 3.2.6. На данный момент в нашем примере используются модули aop , bean , core , context , context-support , jdbc и tx .
  • cglib 3.1.
  • BoneCP 0.8.0. Мы используем BoneCP в качестве пула соединений нашего примера приложения.
  • JOOQ 3.2.2.
  • H2 1.3.174. Мы используем H2 в качестве базы данных нашего примера приложения.

Если вы хотите получить больше информации о модулях Spring Framework, прочтите раздел 1.2 Справочной документации Spring Framework .

Причина, по которой в этом примере приложения используется Spring Framework 3.2.6 вместо 4.0, заключается в том, что в данный момент spring-test-dbunit не совместима с Spring Framework 4.0 .

Соответствующая часть файла pom.xml выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>3.2.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-beans</artifactId>
    <version>3.2.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>3.2.6.RELEASE</version>
</Dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>3.2.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
    <version>3.2.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-expression</artifactId>
    <version>3.2.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>3.2.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>3.2.6.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.2.2</version>
</dependency>
 
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.3.174</version>
</dependency>
  • Пример приложения этого блога имеет и другие зависимости. Вы можете увидеть полный список зависимостей, взглянув на файл pom.xml .

Давайте продолжим, чтобы выяснить, как мы можем преобразовать исключения, генерируемые jOOQ, в Spring DataAccessExceptions .

Преобразование исключений jOOQ в Spring DataAccessExceptions

Почему мы должны преобразовывать исключения, выданные jOOQ, в Spring DataAccessExceptions ?

Одна из причин этого заключается в том, что мы хотим, чтобы наша интеграция работала так же, как поддержка Spring Framework DAO . Одной из важных частей этой поддержки является согласованная иерархия исключений :

Spring предоставляет удобный перевод от специфичных для технологии исключений, таких как SQLException, к собственной иерархии классов исключений с исключением DataAccessException в качестве корневого исключения. Эти исключения обертывают исходное исключение, поэтому нет никакого риска, что кто-либо может потерять какую-либо информацию о том, что могло пойти не так.

Другими словами, если мы хотим, чтобы наше приложение было «хорошим гражданином», имеет смысл убедиться, что наша конфигурация преобразует исключения, создаваемые jOOQ, в Spring DataAccessExceptions .

Мы можем создать компонент, который обеспечивает эту функциональность, выполнив следующие действия:

  1. Создайте класс JOOQToSpringExceptionTransformer, который расширяет класс DefaultExecuteListener . Класс DefaultExecuteListener является общедоступной реализацией по умолчанию интерфейса ExecuteListener, который предоставляет методы прослушивателя для различных событий жизненного цикла при выполнении одного запроса.
  2. Переопределите метод исключения (ExecuteContext ctx) класса DefaultExecuteListener . Этот метод вызывается, если в любой момент жизненного цикла выполнения возникает исключение. Реализуйте метод, выполнив следующие действия:
    1. Получить объект SQLDialect из конфигурации jOOQ .
    2. Создайте объект, который реализует интерфейс SQLExceptionTranslator , следуя этим правилам:
      1. Если настроенный диалект SQL найден, создайте новый объект SQLErrorCodeSQLExceptionTranslator и передайте имя диалекта SQL в качестве аргумента конструктора. Этот класс «выбирает» правильное исключение DataAccessException путем анализа кодов ошибок, специфичных для поставщика.
      2. Если диалект SQL не найден, создайте новый объект SQLStateSQLExceptionTranslator . Этот класс «выбирает» правильное исключение DataAccessException путем анализа состояния SQL, сохраненного в SQLException .
    3. Создайте объект DataAccessException с помощью созданного объекта SQLExceptionTranslator .
    4. Передайте брошенное исключение DataAccessException объекту ExecuteContext, указанному в качестве аргумента метода.

Исходный код класса JOOQToSpringExceptionTransformer выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
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()));
    }
}

Это не моя идея . Я получил эту идею из Gist Адама Целла .

Дополнительное чтение:

Наша работа еще не закончена. Давайте соберем все части вместе и закончим нашу работу, настроив контекст приложения нашего примера приложения.

Настройка контекста приложения

В этом разделе объясняется, как мы можем настроить контекст приложения нашего приложения с помощью конфигурации Java.

Давайте начнем с создания файла свойств, который содержит конфигурацию нашего примера приложения.

Процесс сборки реального приложения основан на профилях Maven. Это гарантирует, что мы можем использовать разные конфигурации в разных средах. Вы можете получить больше информации об этом, прочитав мой пост в блоге под названием « Создание файлов конфигурации для конкретного профиля с помощью Maven».

Создание файла свойств

Мы можем создать файл свойств, выполнив следующие действия:

  1. Настройте соединение с базой данных. Нам нужно настроить класс драйвера JDBC, URL JDBC, имя пользователя базы данных и пароль пользователя базы данных.
  2. Настройте имя используемого диалекта SQL.
  3. Настройте имя сценария SQL, который создает базу данных нашего примера приложения (это необязательный шаг, который не требуется, если ваше приложение не использует встроенную базу данных).

Файл application.properties выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
#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

Давайте продолжим и выясним, как мы можем настроить контекст приложения нашего приложения с помощью конфигурации Java.

Создание класса конфигурации

Мы можем настроить контекст приложения нашего приложения, выполнив следующие действия:

  1. Создайте класс PersistenceContext .
  2. Убедитесь, что созданный класс распознается как класс конфигурации, пометив класс аннотацией @Configuration .
  3. Убедитесь, что репозитории jOOQ нашего приложения найдены во время сканирования компонента. Мы можем сделать это, пометив класс конфигурации аннотацией @ComponentScan .
  4. Включите управление транзакциями на основе аннотаций, аннотируя класс конфигурации аннотацией @EnableTransactionManagement .
  5. Убедитесь, что конфигурация нашего приложения загружена из файла application.properties, который находится в пути к классам. Мы можем сделать это путем аннотации класса конфигурации с аннотацией @PropertySource .
  6. Добавьте поле Environment в класс конфигурации и добавьте аннотацию @Autowired . Мы используем объект Environment, чтобы получить значения свойств свойств конфигурации, которые загружаются из файла application.properties .
  7. Настройте компонент DataSource . Поскольку наше приложение использует BoneCP, мы создали объект BoneCPDataSource в качестве источника данных.
  8. Настройте bean- компонент LazyConnectionDataSourceProxy . Этот компонент гарантирует, что соединение с базой данных выбирается лениво (т. Е. При создании первого оператора).
  9. Настройте bean-компонент TransactionAwareDataSourceProxy . Этот компонент гарантирует, что все соединения JDBC осведомлены о транзакциях, управляемых Spring. Другими словами, соединения JDBC участвуют в транзакциях, связанных с потоками.
  10. Настройте компонент DataSourceTransactionManager . Мы должны передать bean- компонент LazyConnectionDataSourceProxy в качестве аргумента конструктора при создании нового объекта DataSourceTransactionManager .
  11. Настройте bean-компонент DataSourceConnectionProvider . jOOQ получит используемые соединения из источника данных, заданного в качестве аргумента конструктора. Мы должны передать bean-компонент TransactionAwareDataSourceProxy в качестве аргумента конструктора при создании нового объекта DataSourceConnectionProvider . Это гарантирует, что созданные запросы jOOQ участвуют в транзакциях, управляемых Spring.
  12. Настройте bean- компонент JOOQToSpringExceptionTransformer .
  13. Настройте bean-компонент DefaultConfiguration . Этот класс является реализацией интерфейса конфигурации по умолчанию, и мы можем использовать его для настройки jOOQ. Мы должны настроить три вещи:
    1. Мы должны установить ConnectionProvider, который используется для получения и освобождения соединений с базой данных.
    2. Мы должны настроить пользовательские слушатели выполнения . Другими словами, мы должны добавить bean- компонент JOOQToSpringExceptionTransformer в созданный объект DefaultConfiguration . Это гарантирует, что исключения, выданные jOOQ, будут преобразованы в Spring DataAccessExceptions .
    3. Мы должны настроить используемый диалект SQL.
  14. Настройте bean- компонент DefaultDSLContext . Мы используем этот компонент при создании запросов к базе данных с помощью jOOQ.
  15. Настройте компонент DataSourceInitializer . Мы используем этот компонент для создания схемы базы данных базы данных H2 при запуске нашего приложения (если вы не используете встроенную базу данных, вам не нужно настраивать этот компонент).

Исходный код класса PersistenceContext выглядит следующим образом:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
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;
    }
}

Если вы хотите настроить контекст приложения, используя файлы конфигурации XML, в примере приложения также есть рабочий файл конфигурации XML .

Кредиты:

Как мы узнаем, что эта конфигурация работает? Это хороший вопрос. Мы поговорим об этом в следующем разделе.

Это действительно работает?

Когда я начал исследовать, как я могу гарантировать, что запросы к базе данных, созданные с помощью jOOQ, участвуют в транзакциях, управляемых Spring, я заметил, что решить эту проблему непросто .

Пример приложения этого блога имеет несколько интеграционных тестов, которые гарантируют, что транзакции (фиксация и откат) работают в очень простом сценарии. Однако при использовании решения, описанного в этом блоге, мы должны учитывать две вещи:

1. Все запросы к базе данных, созданные с помощью jOOQ, должны выполняться внутри транзакции.

Javadoc класса TransactionAwareDataSourceProxy гласит:

Делегирует DataSourceUtils для автоматического участия в связанных с потоками транзакциях, например, управляемых DataSourceTransactionManager . Вызовы getConnection и закрытые вызовы для возвращенных соединений будут вести себя должным образом внутри транзакции, т. е. всегда работать на транзакционном соединении. Если не в транзакции, применяется нормальное поведение источника данных.

Другими словами, если вы выполняете несколько сложных операций без транзакции, jOOQ будет использовать разные соединения для каждой операции. Это может привести к ошибкам состояния гонки.

Я заметил эту проблему, когда я прочитал этот комментарий, написанный Беном Манесом .

2. Использование TransactionAwareDataSourceProxy не рекомендуется его Javadoc.

Javadoc класса TransactionAwareDataSourceProxy имеет раздел, который выглядит следующим образом:

Этот прокси-сервер позволяет коду доступа к данным работать с простым API JDBC и по-прежнему участвовать в транзакциях, управляемых Spring, аналогично коду JDBC в среде J2EE / JTA. Однако, если возможно, используйте объекты операций SpringSataStilUtils , JdbcTemplate или JDBC, чтобы получить участие в транзакции даже без прокси-сервера для целевого источника данных, избегая необходимости определять такой прокси-сервер в первую очередь.

Это довольно расплывчатый комментарий, потому что он не дает объяснения, почему мы не должны его использовать. Адам Зелл предположил, что поскольку класс использует отражение, его использование может вызвать проблемы с производительностью.

Если у вас проблемы с производительностью, вы можете использовать подход, описанный в
Суть Адама Целля .

Резюме

Теперь мы успешно настроили контекст приложения нашего примера приложения. Этот урок научил четырем вещам:

  • Мы узнали, как мы можем получить необходимые зависимости с Maven.
  • Мы узнали, как мы можем преобразовать исключения, генерируемые jOOQ, в Spring DataAccessExceptions .
  • Мы узнали, как мы можем настроить контекст приложения, которое использует jOOQ и Spring.
  • Мы быстро рассмотрели то, что мы должны учитывать при использовании подхода, описанного в этом сообщении в блоге.

Следующая часть этого руководства описывает, как мы можем использовать поддержку генерации кода в jOOQ .

Пример приложения этого блога доступен на Github .

Дополнительное чтение:

Влад Михалча тоже написал про jOOQ . Проверьте его блог!