'Programming'에 해당하는 글 384건

jOOQ gradle 설정

Programming/Jooq 2021. 11. 25. 02:12

jooq

 

jOOQ 는 데이터베이스로부터 Java code 를 생성하여 Typesafe (SQL 구문 오류가 없는) 쿼리를 사용할 수 있다.

 

jooq 설정시 maven 을 사용한다면 사이트 메뉴얼을 참조하면 될 것이고, gradle 을 사용한다면 두가지 선택지가 있다.

 

 

 

 

Running the code generator with Gradle

Anything else you'd like to tell us? How did you hear from jOOQ? What made you try jOOQ? What are your expectations?

www.jooq.org

 

GitHub - etiennestuder/gradle-jooq-plugin: Gradle plugin that integrates jOOQ.

Gradle plugin that integrates jOOQ. . Contribute to etiennestuder/gradle-jooq-plugin development by creating an account on GitHub.

github.com

 

 

programmatic 설정은 왠지 간단해 보였는데 code generate 는 실패했다.ㅋㅋ; 어쨌든 공식 사이트에서도 서드 파티 플러그인을 추천하니 어쩔수 없이...

 

Spring Boot + Jooq + Mysql 로 테스트 했지만, 아래에 Spring Boot 관련 라이브러리는 대부분 제외했다. Spring Boot 연동도 문제없음.

 


1. dependency 설정

 

build.gradle - jooq 로 자바 코드를 생성하고 DSL 을 사용하는데 필요한 plugin 과 DB 커넥터 추가. 

 

// Jooq / Mysql connector
plugins {
    id 'java'
    id 'nu.studer.jooq' version '6.0.1'  // gradle-jooq-plugin
}

sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    runtimeOnly 'mysql:mysql-connector-java:8.0.25'
    jooqGenerator('mysql:mysql-connector-java:8.0.25')
}

 

 

2. Code generation 설정

 

org.jooq.codegen 으로 DB 의 테이블에 매핑되는 클래스를 생성하도록 설정이 필요하다. JDBC 설정부터 DB의 어느 테이블을 클래스로 변환하고 제외할 것인지 상세하게 설정할 수 있다. codegen 으로 생성된 클래스로 DSL 질의를 실행할 수 있다. build.gradle 에 이어서 추가...

 

jooq {
    version = '3.15.1'  // default (can be omitted)
    edition = nu.studer.gradle.jooq.JooqEdition.OSS  // default (can be omitted)

    configurations {
        main {  // name of the jOOQ configuration
            generateSchemaSourceOnCompilation = true  // default (can be omitted)

            generationTool {

                logging = 'WARN' // TRACE, DEBUG, INFO, WARN, ERROR, FATAL
                onError = 'LOG'  // FAIL, LOG, SILENT

                jdbc {
                    driver = 'com.mysql.cj.jdbc.Driver'
                    url = 'jdbc:mysql://example-db-cluster.cluster-clas4lq5kfel.ap-northeast-2.rds.amazonaws.com:3306/portal'
                    user = 'user'
                    password = 'password'
                    properties {
                        property {
                            key = 'ssl'
                            value = 'true'
                        }
                    }
                }

                generator {
                    name = 'org.jooq.codegen.DefaultGenerator'
                    strategy {
                        // org.jooq.codegen.KeepNamesGeneratorStrategy - DB 네이밍 유지: 언더바, 대소문자 등... 기본값은 PascalCaseNames
                        name = 'org.jooq.codegen.DefaultGeneratorStrategy'
                    }
                    database {
                        // https://www.jooq.org/doc/3.15/manual/code-generation/codegen-advanced/codegen-config-database/codegen-database-name/
                        name = 'org.jooq.meta.mysql.MySQLDatabase'
                        inputSchema = 'portal'

                        includes = '.*' // 정규표현식
                        excludes = 'test_.* | temp_.*'
                    }
                    generate {
                        generatedSerialVersionUID = 'CONSTANT'
                        javaTimeTypes = true    // java.time.*

                        deprecated = false
                        records = true
                        immutablePojos = true
                        fluentSetters = true
                    }
                    target {
                        // packageName = 'nu.studer.sample'
                        packageName = 'com.example.api.jooqgen'
                        // directory = 'build/generated-src/jooq/main'  // default (can be omitted)
                        directory = 'src/main/java'  // default (can be omitted)
                    }
                }
            }
        }
    }
}

 

공식 예제에서 약간의 내용만 바꿨다. 눈치채지 못할 만큼...ㅋ DB 로부터 Java code 를 생성하기 위한 스크립트이기 때문에, jooq 버전, DBCP 설정도 들어가고, 생성 전략, 코드 생성할 디렉토리 등등... 조금 길어보이긴 하지만 별거 없다. 만약 DB 스키마가 이미 존재한다면 여기까지 하고 generateJooq 태스크를 실행하면 설정한 디렉토리에  Java 클래스들이 생성되는 것을 확인할 수 있다. 별 문제가 없다면...

 

 

3. DSL 테스트

 

portal.account 테이블에 id, email 이 있다 치고, entity, controller 대충 만들고 DSL 로 쿼리 뽑기.

 

package com.example.api;

import com.example.joooq.gen.tables.Account;
import org.jooq.DSLContext;
import org.jooq.impl.DSL;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public class AccountRepository {
    private final DSLContext dsl;

    public AccountRepository(DSLContext dsl) {
        this.dsl = dsl;
    }

    public List<Account> getList1() {
        return dsl.select(DSL.field("id"), DSL.field("email"))
                .from(DSL.table("account")).fetchInto(AccountEntity.class);
    }

    public List<Account> getList2() {
        return dsl.select(Account.ACCOUNT.IDX, Account.ACCOUNT.EMAIL)
                .from(Account.ACCOUNT).fetchInto(AccountEntity.class);
    }
}

 

뙇!

getList1 의 쿼리 결과가 잘 나온다면 DB 연동이 잘 된것이고,

getList2 의 쿼리 결과가 잘 나온다면 생성된 jooq 코드가 잘 연동된 것이고.

 



* 연동 후기

 

처음에 programmatic 방식으로 접근했다가 codegen 에 실패했는데... 에러도 안나고 뭐... 그래서 plugin 방식으로 설정을 마쳤다. spring 이고 jooq plugin 이고 다 최신버전으로 설치했는데, jooq 버전을 비롯하여 중간중간 중복된 라이브러리들이 많이 거슬렸다. 가이드를 보고 spring 의 dependency-management 와 버전을 맞추면 해결된다.

 

jooq 3.15 버전부터 R2dbc 라이브러리가 추가되서 자동구성시 제외하려면 추가.

 

@SpringBootApplication(exclude = {R2dbcAutoConfiguration.class})

 

target directory 이슈... codegen 에서 기본 타겟 디렉토리는 build/generated-src/jooq/main 이며 모든 개발자가 각각 빋드하여 코드를 생성해야 한다. 이를 소스셋(src/main/java) 로 변경한다면 버전관리가 가능할 수 있지만 해당 디렉토리를 삭제하고 generate 하기 때문에 target 디렉토리를 src/main/java 로 했다가는 그 안의 소스 패키지가 빌드할 때마다 다 날아간다. 이를 막기 위한 옵션으로 clean 속성을 제공하기는 하나, 정작 해당 값을 false 로 지정하면 에러가 발생한다.

 

generator.target.clean must not be set to false. Disabling the cleaning of the output directory can lead to unexpected behavior in a Gradle build.

 

기본값대로 build 디렉토리를 이용하거나, 만약 소스가 포함되지 않은 별도의 타겟 디렉토리 구성이 필요하다면 독립된 디렉토리나 별도의 모듈에 추가하는 것을 추천한다.

 

 


WRITTEN BY
손가락귀신
정신 못차리면, 벌 받는다.

,

Spring Boot REST API 프로젝트에 API 문서 자동화 툴로 뭘 써야 하나...

 

 

Swagger vs Spring rest docs

 

  • Swagger - 적용 난이도 쉬움, 이쁜 UI, API 테스트 기능, 코드 기반
  • Spring rest docs - 적용 난이도 보통, 단순 UI, 테스트 기반

 

장단점이 분명하지만, 내부용으로 후딱 만들어 쓸 것이라면 Swagger. 외부에 제공해야 한다면 정갈해 보이는 Spring rest docs 이 아닐까 싶다.

 

 

문서화를 위한 gradle 구성

 

  • Step 1. 코드 테스트 -> 에러가 발생하지 않으면 해당 api 내용(snippets: asciidoc 파일) 생성.
  • Step 2. Test 로부터 생성된 snippets 을 import 하도록 api 문서 템플릿(asciidoc 파일) 을 생성하고 빌드하여 API 문서(html) 완성.
  • Step 3. 생성된 html 파일을 외부에 노출하도록 static 디렉토리에 복제.

 

plugins {
    id 'org.springframework.boot' version '2.5.6'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
    // gradle 7 이상은 org.asciidoctor.convert 대신 org.asciidoctor.jvm.convert 사용
    id 'org.asciidoctor.jvm.convert' version '3.3.2'  // Step 1
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

bootJar {  // Step 3
    dependsOn asciidoctor
    copy {
        from "${asciidoctor.outputDir}"
        into "${sourceSets.main.output.resourcesDir}/static/docs"
    }
}

asciidoctor {  // Step 2
    sourceDir 'src/main/asciidoc'
    attributes \
        'snippets': file('build/generated-snippets')
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'  // Step 1
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
    useJUnitPlatform()
}

 

이 build.gradle 파일은 가장 기본적인 세팅이므로 추후 익숙해지면 task 를 통합하거나 build 경로를 수정하여 커스터마이징 해서 편하게 쓰면 되겠다. 예를 들면...

 

asciidoctor {
    dependsOn test
    sourceDir 'src/main/asciidoc'
    outputDir "${sourceSets.main.output.resourcesDir}/static/docs"
    attributes \
        'snippets': file('build/generated-snippets')
}

 

3개의 task (단위 Test - html 생성 - html 복사) 를 하나로 통합한 예...

 

아무튼 build.gradle 파일을 수정했으면 JUnit 와 MockMvc 를 사용하여 Test 를 작성한다. (model, controller 생략)
MockMvc, REST Assured 중 서버 구동없이 컨트롤러 단위 테스트가 쉬운 MockMvc 를 사용한다.

 

@WebMvcTest(TestController.class)
@AutoConfigureRestDocs(outputDir = "build/generated-snippets")
public class TestControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void test() throws Exception {

        mockMvc.perform(RestDocumentationRequestBuilders.get("/book/{id}",1).accept(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(document("book",
                        pathParameters(
                                parameterWithName("id").description("book unique id")
                        ),
                        responseFields(
                                fieldWithPath("id").description("book unique id"),
                                fieldWithPath("title").description("title")
                        )
                ))
                .andExpect(jsonPath("$.id", is(notNullValue())))
                .andExpect(jsonPath("$.title", is(notNullValue())));
    }
}

 

* 아래 에러 발생시:
urlTemplate not found. If you are using MockMvc did you use RestDocumentationRequestBuilders to build the request?

> pathParameters 을 사용할 때는 MockMvcBuilders.get 대신 RestDocumentationRequestBuilders.get 사용

 

Test 코드에서 document(identifier, ...) 에 기반하여 snippets 의 내용을 구성하게 되며, andExpect 에서 에러가 발생하지 않으면 outputDir 에 snippets 들이 생성된다.

 

curl-request.adoc
http-request.adoc
http-response.adoc
...

 

정상적으로 생성됐다면 이 snippets 들을 불러올 템플릿 asciidoc 파일을 생성한다. (/src/main/asciidoc/)

 

= Document Title
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toc-title: API 명세서
:toclevels: 4

[[api]]

== Book Api
api 에 관련된 설명을 이곳에 적습니다..

include::{snippets}/book/curl-request.adoc[]
include::{snippets}/book/http-request.adoc[]
include::{snippets}/book/path-parameters.adoc[]
include::{snippets}/book/http-response.adoc[]
include::{snippets}/book/response-fields.adoc[]

 

* asciidoc 파일 작성시 참고

https://docs.asciidoctor.org/asciidoc/latest/

https://narusas.github.io/2018/03/21/Asciidoc-basic.html

 

 

Asciidoc 기본 사용법

Asciidoc의 기본 문법을 설명한다

narusas.github.io

 

AsciiDoc Language Documentation :: Asciidoctor Docs

AsciiDoc is a lightweight and semantic markup language primarily designed for writing technical documentation. The language can be used to produce a variety of presentation-rich output formats, all from content encoded in a concise, human-readable, plain t

docs.asciidoctor.org

 

Spring REST Docs는 기본적으로 Asciidoctor 를 사용하지만, Markdown 을 사용할 수도 있다.

간략하게 이런식으로 작성하고 bootJar 를 실행하면 /build/docs/asciidoc/book.html 파일이 생성된 것을 확인할 수 있다.

 

spring-rest-docs

 

bootRun 을 실행하면 해당 파일이 /build/resources/main/static/docs 에 복사되며 http://localhost/docs/book.html 에서 확인 가능.

 

 


WRITTEN BY
손가락귀신
정신 못차리면, 벌 받는다.

,

흔히 html 스타일을 꾸미는데 사용하는 class 와 style 속성을 바인딩 할 수 있다.

 

 

 :class 

 

1. 객체 구문

 

<div class="static"
    :class="{ active: isActive, 'text-danger': hasError }"
></div>
<!-- isActive 와 text-danger 값이 true 일 때 -->
<div class="static active text-danger"></div>

 

위와 같이 객체 형식으로 여러 class 바인딩 설정이 가능하며, 일반 class 와도 함께 사용하는 것이 가능하다.

 

 

2. 배열 구문

 

<script>
data() {
    return {
        activeClass: 'active',
        errorClass: 'text-danger'
    }
}
</script>

<div :class="[activeClass, errorClass]"></div>
<div :class="[isActive ? activeClass : '', errorClass]"></div>
<div :class="[{ active: isActive }, errorClass]"></div>

 

위와 같이 배열 형식은 삼항표현식도 가능하고, 객체 구문을 포함할 수도 있다.

 

 

3. 컴포넌트 사용

 

<div id="app">
    <my-component class="baz1"></my-component>
    <my-component class="baz2"></my-component>
</div>

<script>
const app = Vue.createApp({})

app.component('my-component', {
    template: `
        <p :class="$attrs.class">Hi!</p>
        <span>This is a child component</span>
    `
})
</script>

 

컴포넌트에서 여러 루트 엘리먼트(위에서는 p 와 span 태그)를 정의한 경우 $attrs 속성을 사용하여 클래스를 각각 전달 받을 수도 있다.

 

 

 :style 

 

:style 구문의 사용법은 class 와 매우 비슷하게 객체/배열 구문을 사용할 수 있다. JavaScript 객체인 것을 제외하고는 css 와 흡사하다.

 

<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>

 


WRITTEN BY
손가락귀신
정신 못차리면, 벌 받는다.

,

컴포넌트 옵션의 속성인 data, methods 이외에, computed, watch 도 자주 사용한다. computed 와 watch 는 A 데이터를 기반으로 B 데이터를 변경 할 수 있다는 공통점이 있지만 다른 차이점을 구분해서 사용해야 한다.

 

 

computed

  • 익명함수에는 반드시 값을 리턴
  • A 데이터에 기반하여 자동으로 B 데이터를 처리/캐싱할 때.(getter/setter)
  • 많은 경우 watch 보다는 computed 를 사용

 

watch

  • 대상의 값이 변경될 때마다 변경 이전값과 이후값을 인자로 받아 콜백함수 실행
  • A 데이터에 기반하여 특정 액션을 실행하고자 할 때 사용, 특히 route 활용
  • computed 로 불가능하다면...

 

<div id="demo">{{ count }}</div>

<script>
...
    data() {
        return {
            count: 3
        }
    },
    computed: {
        count() {
            return count++;
        }
    },
    watch: {
        count(newVal, oldVal) {
            console.log(newVal, oldVal);
        }
    }
...
</script>

 

위 예제에 컴포넌트 인스턴스가 생성될 때 count 의 초기값 3 은 computed 에 의해 4가 되고 watch 는 4 와 3 을 인자로 보여준다. computed 는 지정한 데이터(count) 로부터 즉시 자동계산을 하였고, 그로 인해 count 가 변경되자 count 를 주시하던 watch 는 log 를 출력했다. 위 코드에서 computed 를 삭제하면 log 는 출력되지 않는다. count 값이 변경되지 않았기 때문에... 쉽게 확인할 수 있는 computed 와 watch 의 차이!

 

vue 공식 문서에는 선언형 프로그래밍인 computed 와 명령형 프로그래밍인 watch 중 어느 것을 사용해도 상관없다면, computed 를 사용하라고 권장하고 있다.

 


WRITTEN BY
손가락귀신
정신 못차리면, 벌 받는다.

,

 

Vue.js 에서 각 컴포넌트가 생성될 때 일련의 초기화 단계를 거치는데 그 단계마다 특정 코드를 실행할 수 있는 라이프사이클 훅이 있다. 그냥 한번 렌더링되는 과정인데 저렇게 단계가 많다. ㅜ 이들은 컴포넌트 인스턴스 속성들처럼 정의한다.

 

Vue.createApp({
    data() {
        return { count: 1 }
    },
    beforeCreate() { ... },
    created() {
        console.log('count is: ' + this.count) // => "count is: 1"
    },
    beforeMount() { ... },
    mounted() { ... },
    beforeUpdate() { ... },
    updated() { ... },
    unmounted() { ... }
})

 

  1. beforeCreate : 인스턴스가 초기화되고 호출됨
  2. created : 인스턴스가 생성된 후 호출됨 (data, computed, methods, watch, event callback 사용가능
  3. beforeMount : 마운트가 시작되기 직전에 호출됨 (render 함수가 처음으로 호출됨)
  4. mounted : 인스턴스가 마운트된 후 호출됨
  5. beforeUpdate : 데이터가 변경되고 DOM 에 적용되기 전에 호출됨
  6. updated : 데이터 변경 후 DOM 이 다시 렌더링되고 패치된 후에 호출됨
  7. unmounted : 컴포넌트 인스턴스가 마운트 해제된 후 호출됨

 

mounted / updated 훅은 모든 자식 컴포넌트까지 마운트 되었음을 보장하지 않으며, 전체 화면이 렌더링된 후의 작업은 vm.$nextTick 을 사용한다. 특정 외부 라이브러리들을 사용할 때 뒤늦게 렌더링되서 컨트롤되지 않을 때 사용하면 유용하다.

 

mounted() {
    this.$nextTick(function () {
        // 전체 화면내용이 렌더링된 후에 아래의 코드가 실행됩니다.
    })
}

 


WRITTEN BY
손가락귀신
정신 못차리면, 벌 받는다.

,