개요
이전에 작업했던 프로젝트를 Mybatis에서 JPA, Querydsl로 마이그레이션하는 작업을 하고 있다.
기존 초기화를 script(schema.sql(테이블 생성), data.sql(더미데이터 생성)) 파일을 사용하다가 JPA를 사용하게 되면서 ddl-auto를 사용해 Table 생성하기로 했다.
DDL이란?
DDL(Data Definition Language)은 SQL의 하위 집합으로 데이터베이스의 구조와 테이블, 뷰, 인덱스 프로시저와 같은 개체를 정의하는데 사용된다. DDL문은 데이터 베이스 개체를 생성, 변경 및 삭제 하는데 사용된다. (CREATE, ALTER, DROP, TRUNCATE, RENAME)
데이터 초기화 설정
SQL script (기존)
기존에는 script 기반으로 데이터를 초기화했다.
src/main/resources 디렉토리에 schema.sql, data.sql을 파일을 생성해두면 애플리케이션 시작 시점에 schema.sql, data.sql 파일이 차례대로 실행된다. schema.sql을 통해서 스키마를 생성하고 data.sql을 통해 데이터를 생성한다.
아래는 script로 데이터 초기화하기 위한 application.yml 설정이다.
# 기존
spring:
sql:
init:
mode: always
schema-locations: classpath:schema.sql
data-locations: classpath:data.sql
Spring Boot는 내장된 DataSource의 스키마를 자동으로 생성한다.이 동작을 제어하기 위해서 spring.sql.init.mode 속성을 사용할 수 있다.
- always: 항상 데이터베이스를 초기화
- embedded: 임베디드 데이터베이스를 사용중이면 항상 초기화. mode 속성의 기본값이다.
- never: 데이터베이스를 초기화하지 않는다.
이 속성은 Spring Boot 2.5.0부터 도입되었고 이전 버전에서 해당 기능을 사용하고싶다면 spring.datasource.initialization-mode를 사용해야 한다.
SQL script + DDL Generation (수정)
나는 schema.sql 스크립트를 사용하지 않고 ddl-auto로 테이블을 생성한 뒤, data.sql로 더미데이터를 생성하고 싶었다.
schema.sql 파일을 삭제하는 방법도 있지만 아직 마이그레이션이 완료되지 않았기 때문에 schema-locations 옵션을 주석처리 후 schema.sql 파일명을 변경해서 실행하지 않도록 했다. (schema.sql -> no-schema.sql)
# ddl-auto 적용
spring:
sql:
init:
mode: always
# schema-locations: classpath:no-schema.sql # schema.sql 파일명 수정 -> no-schema.sql
data-locations: classpath:data.sql
# JPA 설정 정보
jpa:
hibernate:
ddl-auto: create # create, create-drop, update, validate, none
defer-datasource-initialization: true
- ddl-auto: Spring은 Hibernate가 DDL 생성에 사용하는 spring.jpa.hibernate.ddl-auto 속성을 제공한다. 속성 값으로는 create, update, create-drop, validate, none이 있다.
- create: 기존 테이블을 삭제한 후 새로운 테이블을 생성한다.
- update: 객체 모델을 기존 스키마와 비교한 후, 수정된 사항에 따라 스키마를 업데이트한다. 사용되지 않는 테이블이나 컬럼이 있더라도 삭제하지 않는다.
- create-drop: 모든 작업이 완료된 후 데이터베이스를 삭제한다. 일반적으로 단위 테스트에서 사용된다.
- validate: 테이블과 열이 존재하는지 여부만 검증한다. 만약 검증에 실패하면 예외를 발생시킨다.
- none: DDL 생성을 하지 않는다.
- defer-datasource-initialization: 기본적으로 data.sql 스크립트는 Hibernate가 초기화되기 전에 실행된다. 즉, 테이블이 생성되기 전에 더미데이터를 생성하려고 하기 때문에 오류가 발생한다. 이를 해결하기 위해서는 더미데이터 생성을 ddl-auto가 실행된 후에 더미데이터가 생성되도록 해야 한다. defer-datasource-initialization 속성 값을 true로 설정하면 ddl 생성 후 data.sql 스크립트를 통해 더미데이터를 생성해준다.
문제 발생
ddl-auto 설정 이후 문제가 발생했다. TASK_EVALUATION 테이블만 생성되지 않았던 것.
콘솔을 확인해보면 task_evaluation 테이블을 생성했다는 걸 확인할 수 있다.
Hibernate:
create table task_evaluation (
charge_team_id bigint not null,
task_id bigint not null,
ceo_point integer,
cond_ceo varchar(10),
cond_officer varchar(10),
level_ceo varchar(10),
level_officer varchar(10),
note varchar(500),
officer_point integer,
state varchar(1) default N,
task_gb varchar(10),
total_point double,
weight double,
ins_date timestamp,
ins_ip varchar(15),
ins_user varchar(50),
mod_date timestamp,
mod_ip varchar(15),
mod_user varchar(50),
primary key (charge_team_id, task_id)
)
alter table task_evaluation
add constraint FK7xru9ioepwekfal3hybwk83l0
foreign key (charge_team_id)
references organization
alter table task_evaluation
add constraint FK8vktactaa3ko3hu0yur1sbsmd
foreign key (task_id)
references task
원인
현재 프로젝트에서는 내장 H2를 사용하고 있었다. H2는 데이터베이스 작업 중 발생한 디버그 로그를 trace.db에 기록해서 자동으로 생성한다. (H2 파일을 저장한 위치에 생성된다.) race.db를 보고 문제를 확인해 볼 수 있었다.
jdbc[3]: exception
org.h2.jdbc.JdbcSQLSyntaxErrorException: Column "N" not found; SQL statement:
create table public.task_evaluation (
charge_team_id bigint not null,
task_id bigint not null,
ins_date timestamp,
ins_ip varchar(15),
ins_user varchar(50),
mod_date timestamp,
mod_ip varchar(15),
mod_user varchar(50),
ceo_point integer,
cond_ceo varchar(10),
cond_officer varchar(10),
level_ceo varchar(10),
level_officer varchar(10),
note varchar(500),
officer_point integer,
state varchar(1) default N,
task_gb varchar(10),
total_point double,
weight double,
primary key (charge_team_id, task_id)
) [42122-214]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:502)
at org.h2.message.DbException.getJdbcSQLException(DbException.java:477)
at org.h2.message.DbException.get(DbException.java:223)
at org.h2.message.DbException.get(DbException.java:199)
at org.h2.expression.ExpressionColumn.getColumnException(ExpressionColumn.java:244)
at org.h2.expression.ExpressionColumn.optimizeOther(ExpressionColumn.java:226)
at org.h2.expression.ExpressionColumn.optimize(ExpressionColumn.java:213)
at org.h2.table.Column.setDefaultExpression(Column.java:249)
at org.h2.command.Parser.parseColumnForTable(Parser.java:5965)
at org.h2.command.Parser.parseTableColumnDefinition(Parser.java:9331)
at org.h2.command.Parser.parseCreateTable(Parser.java:9271)
at org.h2.command.Parser.parseCreate(Parser.java:6784)
at org.h2.command.Parser.parsePrepared(Parser.java:763)
at org.h2.command.Parser.parse(Parser.java:689)
at org.h2.command.Parser.parse(Parser.java:661)
at org.h2.command.Parser.prepareCommand(Parser.java:569)
at org.h2.engine.SessionLocal.prepareLocal(SessionLocal.java:631)
at org.h2.engine.SessionLocal.prepareCommand(SessionLocal.java:554)
at org.h2.jdbc.JdbcConnection.prepareCommand(JdbcConnection.java:1116)
at org.h2.jdbc.JdbcStatement.executeInternal(JdbcStatement.java:237)
at org.h2.jdbc.JdbcStatement.execute(JdbcStatement.java:223)
at com.zaxxer.hikari.pool.ProxyStatement.execute(ProxyStatement.java:94)
at com.zaxxer.hikari.pool.HikariProxyStatement.execute(HikariProxyStatement.java)
at org.hibernate.tool.schema.internal.exec.GenerationTargetToDatabase.accept(GenerationTargetToDatabase.java:54)
at org.hibernate.tool.schema.internal.SchemaCreatorImpl.applySqlString(SchemaCreatorImpl.java:458)
at org.hibernate.tool.schema.internal.SchemaCreatorImpl.applySqlStrings(SchemaCreatorImpl.java:442)
at org.hibernate.tool.schema.internal.SchemaCreatorImpl.createFromMetadata(SchemaCreatorImpl.java:325)
at org.hibernate.tool.schema.internal.SchemaCreatorImpl.performCreation(SchemaCreatorImpl.java:169)
at org.hibernate.tool.schema.internal.SchemaCreatorImpl.doCreation(SchemaCreatorImpl.java:138)
at org.hibernate.tool.schema.internal.SchemaCreatorImpl.doCreation(SchemaCreatorImpl.java:124)
at org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator.performDatabaseAction(SchemaManagementToolCoordinator.java:168)
at org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator.process(SchemaManagementToolCoordinator.java:85)
at org.hibernate.internal.SessionFactoryImpl.<init>(SessionFactoryImpl.java:335)
at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:471)
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1498)
at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:58)
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:365)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:409)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:396)
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:341)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1863)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1800)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:620)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1157)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:911)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:731)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:307)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292)
at co.pes.PesApplication.main(PesApplication.java:18)
2024-11-14 14:36:57 jdbc[3]: exception
org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "TASK_EVALUATION" not found; SQL statement:
alter table public.task_evaluation
add constraint FK7xru9ioepwekfal3hybwk83l0
foreign key (charge_team_id)
references organiz [42102-214]
2024-11-14 14:36:57 jdbc[3]: exception
org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "TASK_EVALUATION" not found; SQL statement:
alter table public.task_evaluation
add constraint FK8vktactaa3ko3hu0yur1sbsmd
foreign key (task_id)
references [42102-214]
"N" 컬럼을 찾을 수 없다고 한다. 이 테이블에는 N 컬럼이 없는데,,?
스키마를 state 필드 쪽을 보면 답을 알 수 있다. default로 N이 설정되어 있는데, N이 아닌 'N'으로 나와야 한다.
state varchar(1) default N,
TaskEvaluation Entity를 확인해보자
@SuperBuilder
@Getter
@EqualsAndHashCode(callSuper = true)
@Entity(name = "TASK_EVALUATION")
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class TaskEvaluationEntity extends BaseEntity {
...
@Column(length = 1)
@ColumnDefault("N")
private String state; // 상태 (N: 임시저장 / F: 최종제출)
...
}
@ColumnDefault 애너테이션은 DDL을 생성할 때 해당 컬럼의 default 값을 설정하게 해주는 애너테이션이다.
문제는 "N"이라는 값을 넣었을 때 해당 값이 그대로 N으로 스키마에서 사용된다.
해결
스키마에 'N'으로 나올 수 있도록 한 번 더 감싸주면 문제없이 스키마가 생성된다.
@SuperBuilder
@Getter
@EqualsAndHashCode(callSuper = true)
@Entity(name = "TASK_EVALUATION")
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class TaskEvaluationEntity extends BaseEntity {
...
@Column(length = 1)
@ColumnDefault("'N'")
private String state; // 상태 (N: 임시저장 / F: 최종제출)
...
}
Hibernate:
create table public.task_evaluation (
charge_team_id bigint not null,
task_id bigint not null,
ins_date timestamp,
ins_ip varchar(15),
ins_user varchar(50),
mod_date timestamp,
mod_ip varchar(15),
mod_user varchar(50),
ceo_point integer,
cond_ceo varchar(10),
cond_officer varchar(10),
level_ceo varchar(10),
level_officer varchar(10),
note varchar(500),
officer_point integer,
state varchar(1) default 'N',
task_gb varchar(10),
total_point double,
weight double,
primary key (charge_team_id, task_id)
)
참조
https://www.baeldung.com/spring-boot-data-sql-and-schema-sql
https://pravusid.kr/java/2018/10/10/spring-database-initialization.html
'Spring' 카테고리의 다른 글
Spring Boot - Service 단위 테스트하기(JUnit5, Mockito, AssertJ) (1) | 2024.12.16 |
---|---|
Spring Boot - Controller 단위 테스트하기(JUnit, MockMvc, Mockito) (5) | 2024.12.13 |
Spring Boot Test - Mockito로 Static Method Mock 만드는 방법 (2) | 2024.11.01 |
Spring Interceptor - 인터셉터로 로그인 체크하기 (0) | 2023.09.12 |
Spring - 회원 팔로우 기능 구현 (1) | 2023.06.08 |