Введение в JOOQ с Spring

Введение в Jooq с весны

1. обзор

В этой статье будет представлен объектно-ориентированный запрос Jooq - Jooq - и простой способ его настройки в сотрудничестве с Spring Framework.

Большинство приложений Java имеют своего рода постоянство SQL и получают доступ к этому уровню с помощью инструментов более высокого уровня, таких как JPA. И хотя это полезно, в некоторых случаях вам действительно нужен более тонкий инструмент с более тонкими нюансами, чтобы получить доступ к вашим данным или фактически воспользоваться всем, что может предложить базовая БД.

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

2. Maven Зависимости

Следующие зависимости необходимы для запуска кода в этом руководстве.

2.1. jOOQ


    org.Jooq
    jooq
    3.7.3

2.2. весна

Для нашего примера требуется несколько зависимостей Spring; однако, чтобы упростить задачу, нам просто нужно явно включить два из них в файл POM:


    org.springframework
    spring-context
    4.2.5.RELEASE


    org.springframework
    spring-jdbc
    4.2.5.RELEASE

2.3. База данных

Для простоты нашего примера мы будем использовать встроенную базу данных H2:


    com.h2database
    h2
    1.4.191

3. Генерация кода

3.1. Структура базы данных

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

Чтобы упростить задачу, мы сгенерируем только три таблицы:book для книг,author для авторов и еще одну таблицу с именемauthor_book, чтобы представить отношения «многие ко многим» между авторами и книги. Таблицаauthor имеет три столбца:id,first_name иlast_name.. Таблицаbook содержит только столбецtitle иid. ) s первичный ключ.

Следующие SQL-запросы, хранящиеся в файле ресурсовintro_schema.sql, будут выполняться в базе данных, которую мы уже настроили ранее, чтобы создать необходимые таблицы и заполнить их образцами данных:

DROP TABLE IF EXISTS author_book, author, book;

CREATE TABLE author (
  id             INT          NOT NULL PRIMARY KEY,
  first_name     VARCHAR(50),
  last_name      VARCHAR(50)  NOT NULL
);

CREATE TABLE book (
  id             INT          NOT NULL PRIMARY KEY,
  title          VARCHAR(100) NOT NULL
);

CREATE TABLE author_book (
  author_id      INT          NOT NULL,
  book_id        INT          NOT NULL,

  PRIMARY KEY (author_id, book_id),
  CONSTRAINT fk_ab_author     FOREIGN KEY (author_id)  REFERENCES author (id)
    ON UPDATE CASCADE ON DELETE CASCADE,
  CONSTRAINT fk_ab_book       FOREIGN KEY (book_id)    REFERENCES book   (id)
);

INSERT INTO author VALUES
  (1, 'Kathy', 'Sierra'),
  (2, 'Bert', 'Bates'),
  (3, 'Bryan', 'Basham');

INSERT INTO book VALUES
  (1, 'Head First Java'),
  (2, 'Head First Servlets and JSP'),
  (3, 'OCA/OCP Java SE 7 Programmer');

INSERT INTO author_book VALUES (1, 1), (1, 3), (2, 1);

3.2. Плагин Properties Maven

Мы будем использовать три различных плагина Maven для генерации кода Jooq. Первым из них является плагин Properties Maven.

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

В этом разделе мы определим свойства для подключений к базе данных, включая класс драйвера JDBC, URL-адрес базы данных, имя пользователя и пароль, в файле с именемintro_config.properties. Вывод этих свойств позволяет легко переключать базу данных или просто изменять данные конфигурации.

Цельread-project-properties этого плагина должна быть привязана к раннему этапу, чтобы данные конфигурации могли быть подготовлены для использования другими плагинами. В этом случае он привязан к фазеinitialize:


    org.codehaus.mojo
    properties-maven-plugin
    1.0.0
    
        
            initialize
            
                read-project-properties
            
            
                
                    src/main/resources/intro_config.properties
                
            
        
    

3.3. Плагин SQL Maven

Плагин SQL Maven используется для выполнения операторов SQL для создания и заполнения таблиц базы данных. Он будет использовать свойства, которые были извлечены из файлаintro_config.properties плагином Properties Maven, и принимать операторы SQL из ресурсаintro_schema.sql.

Плагин SQL Maven настроен следующим образом:


    org.codehaus.mojo
    sql-maven-plugin
    1.5
    
        
            initialize
            
                execute
            
            
                ${db.driver}
                ${db.url}
                ${db.username}
                ${db.password}
                
                    src/main/resources/intro_schema.sql
                
            
        
    
    
        
            com.h2database
            h2
            1.4.191
        
    

Обратите внимание, что этот плагин должен быть размещен позже, чем плагин Properties Maven в файле POM, поскольку их цели выполнения связаны с одной и той же фазой, и Maven выполнит их в том порядке, в котором они перечислены.

3.4. Плагин jOOQ Codegen

Плагин Jooq Codegen генерирует код Java из структуры таблицы базы данных. Его цельgenerate должна быть привязана к фазеgenerate-sources, чтобы гарантировать правильный порядок выполнения. Метаданные плагина выглядят следующим образом:


    org.Jooq
    jooq-codegen-maven
    ${org.jooq.version}
    
        
            generate-sources
            
                generate
            
            
                
                    ${db.driver}
                    ${db.url}
                    ${db.username}
                    ${db.password}
                
                
                    
                        com.example.jooq.introduction.db
                        src/main/java
                    
                
            
        
    

3.5. Генерация кода

Чтобы завершить процесс генерации исходного кода, нам нужно запустить фазу Mavengenerate-sources. В Eclipse мы можем сделать это, щелкнув проект правой кнопкой мыши и выбравRun As ->Maven generate-sources. После выполнения команды генерируются исходные файлы, соответствующие таблицамauthor,book,author_book (и нескольким другим для поддерживающих классов).

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

КлассAuthor:

public class Author extends TableImpl {
    public static final Author AUTHOR = new Author();

    // other class members
}

КлассBook:

public class Book extends TableImpl {
    public static final Book BOOK = new Book();

    // other class members
}

КлассAuthorBook:

public class AuthorBook extends TableImpl {
    public static final AuthorBook AUTHOR_BOOK = new AuthorBook();

    // other class members
}

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

4. Конфигурация пружины

4.1. Перевод исключений jOOQ в Spring

Чтобы исключения, возникающие при выполнении Jooq, соответствовали поддержке Spring для доступа к базе данных, нам необходимо преобразовать их в подтипы классаDataAccessException.

Давайте определим реализацию интерфейсаExecuteListener для преобразования исключений:

public class ExceptionTranslator extends DefaultExecuteListener {
    public void exception(ExecuteContext context) {
        SQLDialect dialect = context.configuration().dialect();
        SQLExceptionTranslator translator
          = new SQLErrorCodeSQLExceptionTranslator(dialect.name());
        context.exception(translator
          .translate("Access database using Jooq", context.sql(), context.sqlException()));
    }
}

Этот класс будет использоваться приложением Spring.

4.2. Настройка Spring

В этом разделе будут рассмотрены шаги по определениюPersistenceContext, содержащего метаданные и bean-компоненты, которые будут использоваться в контексте приложения Spring.

Начнем с применения необходимых аннотаций к классу:

  • @Configuration: сделать класс распознаваемым как контейнер для beans

  • @ComponentScan: настройте директивы сканирования, включая параметрvalue, чтобы объявить массив имен пакетов для поиска компонентов. В этом руководстве пакет, который нужно найти, сгенерирован плагином Jooq Codegen Maven.

  • @EnableTransactionManagement: включить управление транзакциями с помощью Spring

  • @PropertySource: Укажите расположение файлов свойств, которые нужно загрузить. Значение в этой статье указывает на файл, содержащий данные конфигурации и диалект базы данных, который совпадает с файлом, упомянутым в подразделе 4.1.

@Configuration
@ComponentScan({"com.example.Jooq.introduction.db.public_.tables"})
@EnableTransactionManagement
@PropertySource("classpath:intro_config.properties")
public class PersistenceContext {
    // Other declarations
}

Затем используйте объектEnvironment для получения данных конфигурации, которые затем используются для настройки bean-компонентаDataSource:

@Autowired
private Environment environment;

@Bean
public DataSource dataSource() {
    JdbcDataSource dataSource = new JdbcDataSource();

    dataSource.setUrl(environment.getRequiredProperty("db.url"));
    dataSource.setUser(environment.getRequiredProperty("db.username"));
    dataSource.setPassword(environment.getRequiredProperty("db.password"));
    return dataSource;
}

Теперь мы определим несколько bean-компонентов для работы с операциями доступа к базе данных:

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

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

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

@Bean
public ExceptionTranslator exceptionTransformer() {
    return new ExceptionTranslator();
}

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

Наконец, мы предоставляем реализацию JooqConfiguration и объявляем ее как bean-компонент Spring, который будет использоваться классомDSLContext:

@Bean
public DefaultConfiguration configuration() {
    DefaultConfiguration JooqConfiguration = new DefaultConfiguration();
    jooqConfiguration.set(connectionProvider());
    jooqConfiguration.set(new DefaultExecuteListenerProvider(exceptionTransformer()));

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

    return jooqConfiguration;
}

5. Использование jOOQ с Spring

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

Мы начнем с объявления объектаDSLContext с автоматическим подключением и экземпляров классов, созданных Jooq, которые будут использоваться всеми методами тестирования:

@Autowired
private DSLContext dsl;

Author author = Author.AUTHOR;
Book book = Book.BOOK;
AuthorBook authorBook = AuthorBook.AUTHOR_BOOK;

5.1. Вставка данных

Первый шаг - вставить данные в таблицы:

dsl.insertInto(author)
  .set(author.ID, 4)
  .set(author.FIRST_NAME, "Herbert")
  .set(author.LAST_NAME, "Schildt")
  .execute();
dsl.insertInto(book)
  .set(book.ID, 4)
  .set(book.TITLE, "A Beginner's Guide")
  .execute();
dsl.insertInto(authorBook)
  .set(authorBook.AUTHOR_ID, 4)
  .set(authorBook.BOOK_ID, 4)
  .execute();

ЗапросSELECT для извлечения данных:

Result> result = dsl
  .select(author.ID, author.LAST_NAME, DSL.count())
  .from(author)
  .join(authorBook)
  .on(author.ID.equal(authorBook.AUTHOR_ID))
  .join(book)
  .on(authorBook.BOOK_ID.equal(book.ID))
  .groupBy(author.LAST_NAME)
  .fetch();

Приведенный выше запрос приводит к следующему выводу:

+----+---------+-----+
|  ID|LAST_NAME|count|
+----+---------+-----+
|   1|Sierra   |    2|
|   2|Bates    |    1|
|   4|Schildt  |    1|
+----+---------+-----+

Результат подтверждается APIAssert:

assertEquals(3, result.size());
assertEquals("Sierra", result.getValue(0, author.LAST_NAME));
assertEquals(Integer.valueOf(2), result.getValue(0, DSL.count()));
assertEquals("Schildt", result.getValue(2, author.LAST_NAME));
assertEquals(Integer.valueOf(1), result.getValue(2, DSL.count()));

Когда происходит сбой из-за неправильного запроса, генерируется исключение, и транзакция откатывается. В следующем примере запросINSERT нарушает ограничение внешнего ключа, что приводит к исключению:

@Test(expected = DataAccessException.class)
public void givenInvalidData_whenInserting_thenFail() {
    dsl.insertInto(authorBook)
      .set(authorBook.AUTHOR_ID, 4)
      .set(authorBook.BOOK_ID, 5)
      .execute();
}

5.2. Обновление данных

Теперь давайте обновим существующие данные:

dsl.update(author)
  .set(author.LAST_NAME, "example")
  .where(author.ID.equal(3))
  .execute();
dsl.update(book)
  .set(book.TITLE, "Building your REST API with Spring")
  .where(book.ID.equal(3))
  .execute();
dsl.insertInto(authorBook)
  .set(authorBook.AUTHOR_ID, 3)
  .set(authorBook.BOOK_ID, 3)
  .execute();

Получите необходимые данные:

Result> result = dsl
  .select(author.ID, author.LAST_NAME, book.TITLE)
  .from(author)
  .join(authorBook)
  .on(author.ID.equal(authorBook.AUTHOR_ID))
  .join(book)
  .on(authorBook.BOOK_ID.equal(book.ID))
  .where(author.ID.equal(3))
  .fetch();

Выход должен быть:

+----+---------+----------------------------------+
|  ID|LAST_NAME|TITLE                             |
+----+---------+----------------------------------+
|   3|example |Building your REST API with Spring|
+----+---------+----------------------------------+

Следующий тест подтвердит, что Jooq работал как положено:

assertEquals(1, result.size());
assertEquals(Integer.valueOf(3), result.getValue(0, author.ID));
assertEquals("example", result.getValue(0, author.LAST_NAME));
assertEquals("Building your REST API with Spring", result.getValue(0, book.TITLE));

В случае сбоя выдается исключение, и транзакция откатывается, что мы подтверждаем тестом:

@Test(expected = DataAccessException.class)
public void givenInvalidData_whenUpdating_thenFail() {
    dsl.update(authorBook)
      .set(authorBook.AUTHOR_ID, 4)
      .set(authorBook.BOOK_ID, 5)
      .execute();
}

5.3. Удаление данных

Следующий метод удаляет некоторые данные:

dsl.delete(author)
  .where(author.ID.lt(3))
  .execute();

Вот запрос для чтения затронутой таблицы:

Result> result = dsl
  .select(author.ID, author.FIRST_NAME, author.LAST_NAME)
  .from(author)
  .fetch();

Вывод запроса:

+----+----------+---------+
|  ID|FIRST_NAME|LAST_NAME|
+----+----------+---------+
|   3|Bryan     |Basham   |
+----+----------+---------+

Следующий тест проверяет удаление:

assertEquals(1, result.size());
assertEquals("Bryan", result.getValue(0, author.FIRST_NAME));
assertEquals("Basham", result.getValue(0, author.LAST_NAME));

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

@Test(expected = DataAccessException.class)
public void givenInvalidData_whenDeleting_thenFail() {
    dsl.delete(book)
      .where(book.ID.equal(1))
      .execute();
}

6. Заключение

В этом руководстве были представлены основы Jooq, библиотеки Java для работы с базами данных. В нем описаны этапы создания исходного кода из структуры базы данных и способы взаимодействия с этой базой данных с использованием вновь созданных классов.

Реализацию всех этих примеров и фрагментов кода можно найти вa GitHub project.