Hello

Jooq(db scan, flyway)로 DB 사용해보기 본문

spring

Jooq(db scan, flyway)로 DB 사용해보기

nari0_0 2023. 11. 17. 11:55
728x90

모던 자바 인 액션 10장 람다를 이용한 도메인 전용 언어(DSL)에서 Jooq를 알게되어 공부한 내용을 정리해보려고 합니다.

DSL(Domain Specific Language) 이란

특정 영역에 특화된 언어를 말합니다.

 

내부 DSL : DSL 코드와 범용 코드를 동일한 프로그램 파일에 혼합하는 것으로,  예를 들어 자바로 구현한 DSL을 내부 DSL 이라고 할 수 있습니다. ex) QueryDSL, Jooq (자바 SQL DSL)

 

외부 DSL : 프로젝트에 DSL 추가. DSL 코드와 일반 코드를 별도의 파일에 보관하는 것으로, DSL 코드는 자동 코드 생성기를 통해 프로그래밍 언어 코드로 변환되거나 해당 생성기에 도메인별 코드로 로드되어 실행됩니다. ex) SQL

Jooq(Java Object Oriented Querying) 란

컴파일 시점에서 구문 오류 파악이 가능해 Type Safe 한 SQL 작성을 지원하는 상용 소프트웨어 라이브러리입니다.

테스트 환경

db는 로컬환경에 docker로 띄워 간단하게 사용합니다.

  • Springboot 3.1.5
  • Java 17
  • Jooq 3.18.4
  • Postgresql MySQL
  • Gradle

spring boot를 사용하는경우 spring-boot-start-jooq 제공해주기 때문에 dependency작성 후 plugin, dsl을 작성해 간단하게 사용할 수 있습니다.

필자가 테스트해본 jooq code generation

설정 관련 코드는 github 에 작성해 두었습니다.

  • DB 스캔
    • 해당 DB의 테이블을 모두 스캔함.
  • flyway(오픈소스 데이터베이스 마이그레이션 툴) 기반 스캔
    • 파일에 작성된 내용을 기준으로 스캔함. db와 파일 내용이 다르면 error

Test code

단일 테이블 조회, 벌크 인설트 테스트

@Data
public class Test2 {
    private Integer seq;
    private Integer code;
    private String desc;
    private Integer value;
    private LocalDateTime doDate;
}


@Repository
public class Test2JooqRepo {
    @Autowired
    private DSLContext dslContext;

    public List<Test2> getList() {
        return dslContext.select(TbTable2.TB_TABLE2)
                .from(TbTable2.TB_TABLE2)
                .orderBy(TbTable2.TB_TABLE2.SEQ.desc())
                .limit(10)
                .fetchInto(Test2.class);
    }

    public void bulkInsertTest1(List<Test2> list) {
        List<TbTable2Record> collect = list.stream().map(x -> dslContext.newRecord(TbTable2.TB_TABLE2, x)).collect(Collectors.toList());
        dslContext.batchInsert(collect).execute();
    }
}

---

@JooqTest
@Import(value = {Test1JooqRepoTest.TestConfig.class})
class Test1JooqRepoTest {
    @TestConfiguration
    public static class TestConfig {
        @Bean
        public Test2JooqRepo test2JooqRepo() {
            return new Test2JooqRepo();
        }
    }

    @Autowired
    private Test2JooqRepo jooqRepo;

    @Test
    void name() {
        List<Test2> list1 = jooqRepo.getList();
        Assertions.assertEquals(0, list1.size());
        List<Test2> list = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            Test2 x = new Test2();
            x.setCode(i);
            x.setDesc("desc" + i);
            x.setValue(i * 2);
            x.setDoDate(LocalDateTime.now());
            list.add(x);
        }
        jooqRepo.bulkInsertTest1(list);
        List<Test2> list2 = jooqRepo.getList();
        Assertions.assertEquals(3, list2.size());
    }
}

postgresql 사용 시 spring.test.database.replace=none 필요

조인 테이블 조회 (+ SimpleFlatMapper)

위 처럼 단일 테이블 조회일 경우 fetchInto(DTO.class)매핑시킬 수 있습니다.

jooq는 복잡한 도메인 모델을 매핑을 위해 RecordMapperProvider를 제공합니다.이를 구현한 서드파티 중 SimpleFlatMapper를 사용한 예제를 작성해 보았습니다.

- SimpleFlatMapper의 경우 select()를 해도 필드를 매핑 해주는게 좀 더 편했습니다. 필요한 컬럼만 지정해서 매핑하는 것도 가능합니다.

modelMapper도 사용해 보았는데 필드 매핑을 위해 alias를 붙여 구분이 필요했습니다.

버전 업데이트는 modelMapper이 꾸준하게 관리되고 있고 sfm은 2020년 마지막 반영 되었으며 최근 10월에 9.0.0alpha개발을 준비하기 위해 버전만 수정해 둔 것을 확인 할 수 있습니다.

https://github.com/arnaudroger/SimpleFlatMapper/tree/master/sfm-jooq

class JooqConfig{
       @Bean
        public DefaultConfiguration configuration(@Autowired DataSource dataSource) {
            DefaultConfiguration jooqConfiguration = new DefaultConfiguration();
            jooqConfiguration.set(SQLDialect.POSTGRES);
            jooqConfiguration.set(dataSource);
            jooqConfiguration.set(JooqMapperFactory.newInstance().ignorePropertyNotFound().newRecordMapperProvider());
            return jooqConfiguration;
        }
...
}

class SMFTest{
 @Test
    void name() {
        JAuthor jAuthor = Tables.AUTHOR.as("author");
        JBook jBook = Tables.BOOK.as("book");
        SelectQueryMapper<Book> mapper = SelectQueryMapperFactory.newInstance().newMapper(Book.class);

        List<Book> selectField = mapper.asList(dslContext.select(jAuthor.ID, jAuthor.FIRST_NAME, jAuthor.LAST_NAME,
                        jBook.ID)
                .from(jBook)
                .join(jAuthor)
                .on(jBook.AUTHOR_ID.eq(jAuthor.ID))
        );
        Assertions.assertNotNull(selectField.get(0));
        Assertions.assertNotNull(selectField.get(0).getAuthor().getId());
        Assertions.assertNull(selectField.get(0).getAuthor().getDatetime());

        List<Book> selectAll = mapper.asList(dslContext.select()
                .from(Tables.BOOK)
                .join(Tables.AUTHOR)
                .on(Tables.BOOK.AUTHOR_ID.eq(Tables.AUTHOR.ID)));

        Assertions.assertNotNull(selectAll.get(0));
        Assertions.assertNotNull(selectAll.get(0).getAuthor().getId());
    }
}

jooq만 사용해 매핑 하기위해서는 컬럼에 alias를 지정해 주어야합니다.

    public Book getBook() {
        JAuthor jAuthor = Tables.AUTHOR.as("author");
        JBook jBook = Tables.BOOK.as("book");

        return dslContext.select(jBook.ID,
                        jBook.TITLE,
                        jAuthor.ID.as("author.id"),
                        jAuthor.FIRST_NAME.as("author.firstName"),
                        jAuthor.LAST_NAME.as("author.lastName"))
                .from(Tables.BOOK)
                .join(Tables.AUTHOR)
                .on(Tables.BOOK.AUTHOR_ID.eq(Tables.AUTHOR.ID))
                .fetchSingleInto(Book.class);
    }
    ...

- mysql과 다르게 postSql, h2는 기본적으로 "public" schema 사용

https://groups.google.com/g/jooq-user/c/sRnyBl5ntGI

참고 :

https://sightstudio.tistory.com/54

https://testcontainers.com/guides/working-with-jooq-flyway-using-testcontainers/#_write_repository_test_using_jooqtest_slice_annotation

https://sightstudio.tistory.com/54

- gradle plugin

https://github.com/etiennestuder/gradle-jooq-plugin

- database name (db 종류별 name 확인)

https://www.jooq.org/doc/latest/manual/code-generation/codegen-advanced/codegen-config-database/codegen-database-name/

728x90