1. 개요

이 기사에서는 Jooq 객체 지향 쿼리 (Jooq)를 소개하고 Spring Framework와 공동으로 설정하는 간단한 방법을 소개합니다.

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

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

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

2. Maven 의존성

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

2.1. jOOQ

<dependency>
    <groupId>org.jooq</groupId>
    <artifactId>jooq</artifactId>
    <version>3.2.14</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_NAMELAST_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. 속성 Maven 플러그인

Jooq 코드를 생성하기 위해 세 가지 다른 Maven 플러그인을 사용합니다. 이들 중 첫 번째는 Properties Maven 플러그인입니다.

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

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

이 플러그인 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 Maven 플러그인

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 구성

이 섹션에서는 Spring 애플리케이션 컨텍스트에서 사용할 메타 데이터와 Bean을 포함 하는 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
}

다음으로 환경 오브젝트를 사용 하여 구성 데이터를 가져 오면 데이터 소스 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; 
}

이제 우리는 데이터베이스 액세스 작업에 사용할 몇 가지 빈을 정의합니다.

@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으로 선언합니다 .

@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. Spring과 함께 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()));

잘못된 쿼리로 인해 오류가 발생하면 예외가 throw되고 트랜잭션이 롤백됩니다. 다음 예에서 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 프로젝트 에서 찾을 수 있습니다 .