1. 개요

이 기사에서는 Jooq Object Oriented Querying(Jooq)과 이를 Spring Framework와 공동으로 설정하는 간단한 방법을 소개합니다.

대부분의 Java 애플리케이션에는 일종의 SQL 지속성이 있으며 JPA와 같은 상위 수준 도구를 사용하여 해당 계층에 액세스합니다. 이것이 유용하긴 하지만 어떤 경우에는 데이터에 접근하거나 기본 DB가 제공하는 모든 것을 실제로 활용하기 위해 더 정교하고 미묘한 도구가 필요합니다.

Jooq는 일반적인 ORM 패턴을 피하고 형식이 안전한 쿼리를 작성하고 깨끗하고 강력한 유창한 API를 통해 생성된 SQL을 완벽하게 제어할 수 있는 코드를 생성합니다.

이 기사는 Spring MVC에 중점을 둡니다. jOOQ에 대한 Spring Boot 지원 문서에서는   Spring Boot에서 jOOQ를 사용하는 방법을 설명합니다.

2. 메이븐 의존성

이 사용방법(예제)의 코드를 실행하려면 다음 의존성이 필요합니다.

2.1. jOOQ

<dependency>
    <groupId>org.jooq</groupId>
    <artifactId>jooq</artifactId>
    <version>3.14.15</version>
</dependency>

2.2. Spring

이 예제에는 몇 가지 Spring 의존성이 필요합니다. 그러나 간단하게 하기 위해 POM 파일에 두 가지를 명시적으로 포함하기만 하면 됩니다.

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.2.2.RELEASE</version>
</dependency>

2.3. 데이터 베이스

예제를 쉽게 만들기 위해 H2 임베디드 데이터베이스를 사용합니다.

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.191</version>
</dependency>

3. 코드 생성

3.1. 데이터베이스 구조

이 기사 전체에서 작업할 데이터베이스 구조를 소개하겠습니다. 출판사가 관리하는 책과 저자에 대한 정보를 저장하기 위해 데이터베이스를 생성해야 한다고 가정합니다. 여기서 저자는 많은 책을 저술할 수 있고 책은 여러 저자에 의해 공동 집필될 수 있습니다.

간단하게 하기 위해 책을 위한 책 , 저자를 위한 저자 , 저자와 책 간의 다대다 관계를 나타내는 author_book 이라는 또 다른 테이블의 세 가지 테이블만 생성합니다 . 작성자 테이블에는 id , first_name 및 last_name 세 열이 있습니다 . 책 테이블 에는 제목 열과 id 기본 키만 포함됩니다 .

intro_schema.sql 리소스 파일 에 저장된 다음 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. 속성 메이븐 플러그인

우리는 Jooq 코드를 생성하기 위해 세 가지 다른 Maven 플러그인을 사용할 것입니다. 첫 번째는 Properties Maven 플러그인입니다.

이 플러그인은 리소스 파일에서 구성 데이터를 읽는 데 사용됩니다. 데이터를 POM에 직접 추가할 수 있으므로 필수는 아니지만 속성을 외부에서 관리하는 것이 좋습니다.

이 섹션에서는 JDBC 드라이버 클래스, 데이터베이스 URL, 사용자 이름 및 암호를 포함하여 데이터베이스 연결에 대한 속성을 intro_config.properties 파일에 정의합니다 . 이러한 속성을 외부화하면 데이터베이스를 쉽게 전환하거나 구성 데이터만 변경할 수 있습니다.

이 플러그인의 read -project-properties 목표는 다른 플러그인에서 사용할 구성 데이터를 준비할 수 있도록 초기 단계에 바인딩되어야 합니다. 이 경우 초기화 단계에 바인딩됩니다.

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>properties-maven-plugin</artifactId>
    <version>1.0.0</version>
    <executions>
        <execution>
            <phase>initialize</phase>
            <goals>
                <goal>read-project-properties</goal>
            </goals>
            <configuration>
                <files>
                    <file>src/main/resources/intro_config.properties</file>
                </files>
            </configuration>
        </execution>
    </executions>
</plugin>

3.3. SQL 메이븐 플러그인

SQL Maven 플러그인은 SQL 문을 실행하여 데이터베이스 테이블을 생성하고 채우는 데 사용됩니다. Properties Maven 플러그인에 의해 intro_config.properties 파일 에서 추출된 속성을 사용 하고 intro_schema.sql 리소스 에서 SQL 문을 가져옵니다 .

SQL Maven 플러그인은 아래와 같이 구성됩니다.

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>sql-maven-plugin</artifactId>
    <version>1.5</version>
    <executions>
        <execution>
            <phase>initialize</phase>
            <goals>
                <goal>execute</goal>
            </goals>
            <configuration>
                <driver>${db.driver}</driver>
                <url>${db.url}</url>
                <username>${db.username}</username>
                <password>${db.password}</password>
                <srcFiles>
                    <srcFile>src/main/resources/intro_schema.sql</srcFile>
                </srcFiles>
            </configuration>
        </execution>
    </executions>
    <dependencies>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.191</version>
        </dependency>
    </dependencies>
</plugin>

이 플러그인은 POM 파일에서 Properties Maven 플러그인보다 나중에 위치해야 합니다. 실행 목표가 둘 다 동일한 단계에 바인딩되어 있고 Maven이 나열된 순서대로 플러그인을 실행하기 때문입니다.

3.4. jOOQ Codegen 플러그인

Jooq Codegen 플러그인은 데이터베이스 테이블 구조에서 Java 코드를 생성합니다. 정확한 실행 순서를 보장하기 위해 해당 생성 목표는 소스 생성 단계 에 바인딩되어야 합니다 . 플러그인 메타데이터는 다음과 같습니다.

<plugin>
    <groupId>org.jooq</groupId>
    <artifactId>jooq-codegen-maven</artifactId>
    <version>${org.jooq.version}</version>
    <executions>
        <execution>
            <phase>generate-sources</phase>
            <goals>
                <goal>generate</goal>
            </goals>
            <configuration>
                <jdbc>
                    <driver>${db.driver}</driver>
                    <url>${db.url}</url>
                    <user>${db.username}</user>
                    <password>${db.password}</password>
                </jdbc>
                <generator>
                    <target>
                        <packageName>com.baeldung.jooq.introduction.db</packageName>
                        <directory>src/main/java</directory>
                    </target>
                </generator>
            </configuration>
        </execution>
    </executions>
</plugin>

3.5. 코드 생성

소스 코드 생성 프로세스를 완료하려면 Maven 생성 소스 단계를 실행해야 합니다. Eclipse에서 프로젝트를 마우스 오른쪽 버튼으로 클릭하고 Run As –> Maven generate-sources를 선택하여 이 작업을 수행할 수 있습니다 . 명령이 완료된 후 author , book , author_book 테이블(및 지원 클래스에 대한 여러 기타)에 해당하는 소스 파일이 생성됩니다.

Jooq이 생성한 것을 보기 위해 테이블 ​​클래스를 파헤쳐 보겠습니다. 각 클래스에는 이름의 모든 문자가 대문자라는 점을 제외하고는 클래스와 동일한 이름의 정적 필드가 있습니다. 다음은 생성된 클래스의 정의에서 가져온 코드 스니펫입니다.

작성자 클래스 :

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

    // other class members
}

도서 클래스 :

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

    // other class members
}

AuthorBook 클래스 :

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

    // other class members
}

이러한 정적 필드에서 참조하는 인스턴스는 프로젝트의 다른 레이어로 작업할 때 해당 테이블을 나타내는 데이터 액세스 개체 역할을 합니다.

4. 스프링 구성

4.1. jOOQ 예외를 Spring으로 변환

데이터베이스 액세스에 대한 Spring 지원과 일치하는 Jooq 실행에서 발생한 예외를 만들기 위해 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를 정의하는 단계를 살펴봅니다 .

필요한 어노테이션을 클래스에 적용하여 시작하겠습니다.

  • @Configuration : 클래스를 Bean의 컨테이너로 인식하도록 합니다.
  • @ComponentScan : 구성 요소를 검색하기 위해 패키지 이름 배열을 선언하는 값 옵션을 포함하여 스캐닝 지시문을 구성합니다. 이 예제에서 검색할 패키지는 Jooq Codegen Maven 플러그인에 의해 생성된 패키지입니다.
  • @EnableTransactionManagement : Spring에서 트랜잭션을 관리할 수 있도록 합니다.
  • @PropertySource : 로드할 속성 파일의 위치를 ​​나타냅니다. 이 문서의 값은 데이터베이스의 구성 데이터와 언어를 포함하는 파일을 가리키며, 이는 하위 섹션 4.1에서 언급된 것과 동일한 파일입니다.
@Configuration
@ComponentScan({"com.baeldung.Jooq.introduction.db.public_.tables"})
@EnableTransactionManagement
@PropertySource("classpath:intro_config.properties")
public class PersistenceContext {
    // Other declarations
}

다음으로 환경 개체를 사용하여 구성 데이터를 가져온 다음 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());
}

마지막으로 Jooq 구성 구현을 제공 하고 DSLContext 클래스 에서 사용할 Spring 빈으로 선언합니다 .

@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 사용하기

이 섹션에서는 일반적인 데이터베이스 액세스 쿼리에서 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<Record3<Integer, String, Integer>> 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|
+----+---------+-----+

결과는 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()));

잘못된 쿼리로 인해 실패가 발생하면 예외가 발생하고 트랜잭션이 롤백됩니다. 다음 예에서 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, "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();

필요한 데이터를 얻습니다.

Result<Record3<Integer, String, String>> 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|Baeldung |Building your REST API with Spring|
+----+---------+----------------------------------+

다음 테스트는 Jooq가 예상대로 작동하는지 확인합니다.

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

실패할 경우 예외가 발생하고 트랜잭션이 롤백되며 테스트를 통해 이를 확인합니다.

@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<Record3<Integer, String, String>> 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. 결론

이 사용방법(예제)에서는 데이터베이스 작업을 위한 Java 라이브러리인 Jooq의 기본 사항을 소개했습니다. 데이터베이스 구조에서 소스 코드를 생성하는 단계와 새로 생성된 클래스를 사용하여 해당 데이터베이스와 상호 작용하는 방법을 다루었습니다.

이러한 모든 예제 및 코드 스니펫의 구현은 GitHub 프로젝트 에서 찾을 수 있습니다 .

Persistence footer banner