Въведение в Jooq с пролетта

1. Общ преглед

Тази статия ще представи Jooq обектно ориентирано заявяване - Jooq - и лесен начин да го настроите в сътрудничество с Spring Framework.

Повечето приложения на Java имат някакъв вид постоянство на SQL и имат достъп до този слой с помощта на инструменти от по-високо ниво като JPA. И макар че това е полезно, в някои случаи наистина се нуждаете от по-фин, по-нюансиран инструмент, за да стигнете до вашите данни или всъщност да се възползвате от всичко, което базовият DB може да предложи.

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

Тази статия се фокусира върху Spring MVC. Нашата статия Поддръжка на Spring Boot за jOOQ описва как да използвате jOOQ в Spring Boot.

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

Следните зависимости са необходими за стартиране на кода в този урок.

2.1. jOOQ

 org.jooq jooq 3.2.14 

2.2. Пролет

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

 org.springframework spring-context 5.2.2.RELEASE   org.springframework spring-jdbc 5.2.2.RELEASE 

2.3. База данни

За да улесним нещата за нашия пример, ще използваме вградената база данни H2:

 com.h2database h2 1.4.191 

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

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

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

За да улесним, ще генерираме само три таблици: книгата за книги, автор за автори и друга таблица, наречена author_book, за да представим връзката много към много между авторите и книгите. Таблицата автор има три колони: id , first_name и last_name. Таблицата с книги съдържа само колона със заглавие и първичен ключ за идентификатор .

Следните 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. Свойства Maven Plugin

Ще използваме три различни приставки Maven, за да генерираме Jooq кода. Първият от тях е плъгинът Properties Maven.

Този плъгин се използва за четене на конфигурационни данни от ресурсен файл. Не се изисква, тъй като данните могат да бъдат добавени директно към POM, но е добра идея да управлявате свойствата външно.

В този раздел ще дефинираме свойства за връзки към база данни, включително клас JDBC драйвер, URL адрес на база данни, потребителско име и парола, във файл с име intro_config.properties . Екстернализирането на тези свойства улеснява превключването на базата данни или просто промяната на конфигурационните данни.

Най- четените с проекта свойства Целта на този плъгин да се обвърже с ранна фаза, така че данните за конфигурация може да се приготви за използване от други плъгини. В този случай той е обвързан с фазата на инициализация :

 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 код от структура на таблица на база данни. Целта му за генериране трябва да бъде обвързана с фазата генериране на източници, за да се осигури правилният ред на изпълнение. Метаданните на приставката изглеждат по следния начин:

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

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

За да завършим процеса на генериране на изходен код, трябва да стартираме фазата на Maven генериране на източници . В Eclipse можем да направим това, като щракнем с десния бутон на мишката върху проекта и изберем Run As -> Maven generate-sources . След приключване на командата се генерират изходни файлове, съответстващи на таблиците автор , книга , авторска книга (и няколко други за поддържащи класове).

Нека да разгледаме класовете на таблици, за да видим какво е произвел Jooq. Всеки клас има статично поле със същото име като класа, с изключение на това, че всички букви в името са с главни букви. По-долу са кодови фрагменти, взети от дефинициите на генерираните класове:

The Author класа:

public class Author extends TableImpl { public static final Author AUTHOR = new Author(); // other class members }

The Book class:

public class Book extends TableImpl { public static final Book BOOK = new Book(); // other class members }

The AuthorBook class:

public class AuthorBook extends TableImpl { public static final AuthorBook AUTHOR_BOOK = new AuthorBook(); // other class members }

The instances referenced by those static fields will serve as data access objects to represent the corresponding tables when working with other layers in a project.

4. Spring Configuration

4.1. Translating jOOQ Exceptions to Spring

In order to make exceptions thrown from Jooq execution consistent with Spring support for database access, we need to translate them into subtypes of the DataAccessException class.

Let's define an implementation of the ExecuteListener interface to convert exceptions:

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())); } }

This class will be used by the Spring application context.

4.2. Configuring Spring

This section will go through steps to define a PersistenceContext that contains metadata and beans to be used in the Spring application context.

Let's get started by applying necessary annotations to the class:

  • @Configuration: Make the class to be recognized as a container for beans
  • @ComponentScan: Configure scanning directives, including the value option to declare an array of package names to search for components. In this tutorial, the package to be searched is the one generated by the Jooq Codegen Maven plugin
  • @EnableTransactionManagement: Enable transactions to be managed by Spring
  • @PropertySource: Indicate the locations of the properties files to be loaded. The value in this article points to the file containing configuration data and dialect of the database, which happens to be the same file mentioned in subsection 4.1.
@Configuration @ComponentScan({"com.baeldung.Jooq.introduction.db.public_.tables"}) @EnableTransactionManagement @PropertySource("classpath:intro_config.properties") public class PersistenceContext { // Other declarations }

Next, use an Environment object to get the configuration data, which is then used to configure the DataSource bean:

@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; }

Now we define several beans to work with database access operations:

@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()); }

Finally, we provide a Jooq Configuration implementation and declare it as a Spring bean to be used by the DSLContext class:

@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. Using jOOQ With Spring

This section demonstrates the use of Jooq in common database access queries. There are two tests, one for commit and one for rollback, for each type of “write” operation, including inserting, updating, and deleting data. The use of “read” operation is illustrated when selecting data to verify the “write” queries.

We will begin by declaring an auto-wired DSLContext object and instances of Jooq generated classes to be used by all testing methods:

@Autowired private DSLContext dsl; Author author = Author.AUTHOR; Book book = Book.BOOK; AuthorBook authorBook = AuthorBook.AUTHOR_BOOK;

5.1. Inserting Data

The first step is to insert data into tables:

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

A SELECT query to extract data:

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

The above query produces the following output:

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

The result is confirmed by the Assert API:

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

When a failure occurs due to an invalid query, an exception is thrown and the transaction rolls back. In the following example, the INSERT query violates a foreign key constraint, resulting in an exception:

@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. Updating Data

Now let's update the existing data:

dsl.update(author) .set(author.LAST_NAME, "Baeldung") .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();

Get the necessary data:

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

The output should be:

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

The following test will verify that Jooq worked as expected:

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

In case of a failure, an exception is thrown and the transaction rolls back, which we confirm with a test:

@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. Deleting Data

The following method deletes some data:

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

Here is the query to read the affected table:

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

The query output:

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

The following test verifies the deletion:

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

On the other hand, if a query is invalid, it will throw an exception and the transaction rolls back. The following test will prove that:

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

6. Conclusion

Този урок представи основите на Jooq, Java библиотека за работа с бази данни. Той обхваща стъпките за генериране на изходен код от структура на база данни и как да взаимодейства с тази база данни, като използва новосъздадените класове.

Прилагането на всички тези примери и кодови фрагменти може да бъде намерено в проект на GitHub.