JPA + Hibernate에서 DDL script 파일로 저장하기
Page content
JPA를 사용할 때부터 계속 하려고 했던건데, 이번에 소기의 목적에서 아주 일부분은 달성하여 진행했던 과정을 기록차원에서 남긴다.
선요약
- 지정한 패키지의
@Entity
annotation이 선언된 클래스를 검색하여 DDL script를 파일로 생성한다. - DDL script 생성시 column comment도 함께 생성한다(제일 고민을 많이 함).
@Embedded
annotation이 달린 항목도 comment를 생성한다.
TODO
- Table comment 추가
- Embedded 순환 처리
구현부
작업의 흐름
- 우선 Reflections을 이용하여 지정한 package안에 존재하는 클래스 중
@Entity
annotation이 선언된 클래스를 검색한다. - 검색된 클래스 목록에서 클래스내에 정의된
Field
를 추출한다. - 추출된 클래스별
Field
에서@Column
,@JoinColumn
및@Embedded
annotation이 선언된Field
를 검사한다. - 3가지의 annotation중 하나가 선언된
Field
에서 내가 작성한 annotation(@ColumnComment
)이 선언되어 있는지 확인한다. 만약@ColumnComment
annotation이 선언되어 있다면 Hibernate가 가지고 있는org.hibernate.mapping.Column
객체의setComment(String)
함수를 호출하여 column comment를 할당한다. - 위의 과정을 거친
org.hibernate.boot.spi.MetadataImplementor
객체를org.hibernate.tool.hbm2ddl.SchemaExport
클래스를 이용하여 지정된 파일로 저장한다.
1. @Entity
annotation이 선언된 클래스 목록 추출하기
String _basePackage = "net.clfif3.sample";
ConfigurationBuilder _builder = new ConfigurationBuilder();
FilterBuilder _filterBuilder = new FilterBuilder();
_builder.addUrls(ClassPathHelper.forPackage(_basePackage));
_filterBuilder = _filterBuilder.includePackage(_basePackage));
_builder.filterInputsBy(_filterBuilder);
_builder.setExpandSuperTypes(false);
Reflections _reflections = new Reflections(_builder);
//
// @Entity annotation이 선언된 클래스 검색 및 반환
//
Set<Class<?>> _entitySet = _reflections.getTypeAnnotatedWith(Entity.class);
//
// Meta data 생성
//
MetadataSources _metadata = new MetadataSources(
new StandardServiceRegistryBuilder()
.applySetting("hibernate.dialect", MySQL8Dialect.class.getName()).build()
);
//
// 검색된 클래스를 _metadata에 추가
//
_entitySet.forEach(_metadata::addAnnotatedClass);
//
// Meta data 기반으로 Hibernate에서 hbm2ddl을 처리하기 위한 변환된 데이터를 생성
// 해당 결과에서 실제 변환된 정보를 추출(`MetadataImpl#getEntityBindingMap()`)
//
MetadataImplementor _implementor = (MetadataImplementor)_metadata.buildMetadata();
Map<String, PersistentClass> _classMap = ((MetadataImpl)_implementor).getEntityBindingMap();
2. 검색된 클래스내에 정의된 Field
목록 추출
_classMap.forEach((key, value) => {
Class<?> _mappedClass = value.getMappedClass();
Field[] _declaredFields = _mappedClass.getDeclaredFields();
});
3. 추출된 Field
에서 @Column
, @JoinColumn
및 @Embedded
annotation이 선언된 항목 검색
중간의 checkExistColumn(org.hibernate.mapping.Column, Field)
함수는 마지막 전체 소스 참고
value.getTable()
.getColumnIterator()
.forEachRemaining(hibernateColumn -> {
//
// hibernate column에 명시된 이름을 이용하여
// 정의된 필드에서 동일한 명칭을 사용하는 필드를 검색
//
Optional<Field> _target = Arrays.stream(_declaredFields)
.filter(field -> {
return checkExistColumn(hibernateColumn, field);
}).findAny();
});
4. 3번에서 검색된 Field
중 @ColumnComment
(내가 만든) annotation의 선언여부 확인
_target.ifPresent(field -> {
ColumnComment _comment = null;
if (field.getDeclaredAnnotation(Embedded.class) != null) {
// @Embedded annotation 선언됨
Field[] _fields = field.getType().getDeclaredFields();
Optional<Field> __target = Arrays.stream(_fields).filter(_field -> {
Column _persistentColumn = _field.getDeclaredAnnotation(Column.class);
// @Column(name = "") 항목으로 비교
return _persistentColumn != null && hibernateColumn.getName().equals(_persistentColumn.name());
}).findAny();
if (__target.isPresent()) {
_comment = __target.get().getDeclaredAnnotation(ColumnComment.class);
}
} else {
_comment = field.getDeclaredAnnotation(ColumnComment.class);
}
if (_comment != null) {
// @ColumnComment 선언이 발견되었으면
hibernateColumn.setComment(_comment.value());
}
});
5. 4번까지의 작업이 완료된 MetadataImplementor
를 이용하여 파일에 저장
SchemaExport _export = new SchemaExport();
String _path = GenerateDataDefinitionLanguage.class.getResource("/").getPath();
String _fileName = String.format("%sddl_script_%d.sql", _path, System.currentTimeMillis());
_export.setDelimiter(";");
_export.setOutputFile(_fileName);
_export.setFormat(true);
EnumSet<TargetType> _targetTypeEnumSet = EnumSet.of(TargetType.SCRIPT);
_export.execute(_targetTypeEnumSet, SchemaExport.Action.CREATE, _implementor);
전체 코드
Sample entity
package net.cliff3.sample;
// import 생략
// 장비정보
@Entity
@Table(name = "sensor")
public class Sensor implements Serializable {
/**
* Serial version uid
*/
private static final long serialVersionUID = -3010495378602325388L;
@Id
@Getter
@Setter
@Column(name = "seq")
@ColumnComment(value = "일련번호")
@GeneratedValue(strategy = GenerationType.IDENTITY)
@NotNull(message = "일련번호는 필수 입니다.", groups = {Update.class, Delete.class})
private Long sequence;
/**
* 제조사 정보
*/
@Getter
@Setter
@ColumnComment("제조사 일련번호")
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "vendor_seq", nullable = false, foreignKey = @ForeignKey(name = "fk_sensor_vendor_seq"))
private Vendor vendor;
/**
* 장비명칭
*/
@Getter
@Setter
@ColumnComment("장비명칭")
@Column(name = "name", length = 200, nullable = false)
private String name;
/**
* 설치 좌표 정보
*/
@Getter
@Setter
@Embedded
private EmbeddedCoordinate coordinate = new EmbeddedCoordinate();
// 기타 컬럼 정의
}
package net.cliff3.sample;
// import 생략
// 위도/경도 좌표 정보
@Embeddable
public class EmbeddedCoordinate implements Serializable {
/**
* Serial version uid
*/
private static final long serialVersionUID = 1415339858077612640L;
/**
* 위도
*/
@Getter
@Setter
@ColumnComment("위도")
@Column(name = "latitude", length = 25)
private String latitude;
/**
* 경도
*/
@Getter
@Setter
@ColumnComment("경도")
@Column(name = "longitude", length = 25)
private String longitude;
}
Custom annotation(@ColumnComment)
package net.cliff3.sample;
// import 생략
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value = ElementType.FIELD)
public @interface ColumnComment {
String value() default "";
}
DDL 생성 전체 코드
package net.cliff3.sample;
// import 생략
public class GenerateDataDefinitionLanguage {
public static void main(Strnig[] args) {
String _basePackage = "net.clfif3.sample";
ConfigurationBuilder _builder = new ConfigurationBuilder();
FilterBuilder _filterBuilder = new FilterBuilder();
_builder.addUrls(ClassPathHelper.forPackage(_basePackage));
_filterBuilder = _filterBuilder.includePackage(_basePackage));
_builder.filterInputsBy(_filterBuilder);
_builder.setExpandSuperTypes(false);
Reflections _reflections = new Reflections(_builder);
Set<Class<?>> _entitySet = _reflections.getTypeAnnotatedWith(Entity.class);
MetadataSources _metadata = new MetadataSources(
new StandardServiceRegistryBuilder()
.applySetting("hibernate.dialect", MySQL8Dialect.class.getName()).build()
);
_entitySet.forEach(_metadata::addAnnotatedClass);
MetadataImplementor _implementor = (MetadataImplementor)_metadata.buildMetadata();
Map<String, PersistentClass> _classMap = ((MetadataImpl)_implementor).getEntityBindingMap();
_classMap.forEach((key, value) -> {
Class<?> _mappedClass = value.getMappedClass();
Field[] _declaredFields = _mappedClass.getDeclaredFields();
value.getTable()
.getColumnIterator()
.forEachRemaining(hibernateColumn -> {
Optional<Field> _target = Arrays.stream(_declaredFields)
.filter(field -> {
return checkExistColumn(hibernateColumn, field);
}).findAny();
_target.ifPresent(field -> {
ColumnComment _comment = null;
if (field.getDeclaredAnnotation(Embedded.class) != null) {
Field[] _fields = field.getType().getDeclaredFields();
Optional<Field> __target = Arrays.stream(_fields).filter(_field -> {
Column _persistentColumn = _field.getDeclaredAnnotation(Column.class);
return _persistentColumn != null && hibernateColumn.getName().equals(_persistentColumn.name());
}).findAny();
if (__target.isPresent()) {
_comment = __target.get().getDeclaredAnnotation(ColumnComment.clas);
}
} else {
_comment = field.getDeclaredAnnotation(ColumnComment.class);
}
if (_comment != null) {
hibernateColumn.setComment(_comment.value());
}
});
})
});
SchemaExport _export = new SchemaExport();
String _path = GenerateDataDefinitionLanguage.class.getResource("/").getPath();
String _fileName = String.format("%sddl_script_%d.sql", _path, System.currentTimeMillis());
_export.setDelimiter(";");
_export.setOutputFile(_fileName);
_export.setFormat(true);
EnumSet<TargetType> _targetTypeEnumSet = EnumSet.of(TargetType.SCRIPT);
_export.execute(_targetTypeEnumSet, SchemaExport.Action.CREATE, _implementor);
}
private static boolean checkExistColumn(org.hibernate.mapping.Column hibernateColumn, Field field) {
// @Column annotation
Column _persistentColumn = field.getDeclaredAnnotation(Column.class);
if (_persistentColumn != null) {
return _persistentColumn.name().equals(hibernateColumn.getName());
}
// @JoinColumn annotation
JoinColumn _joinColumn = field.getDeclaredAnnotation(JoinColumn.class);
if (_joinColumn != null) {
return _joinColumn.name().equals(hibernateColumn.getName());
}
// @Embedded annotation
Embedded _embedded = field.getDeclaredAnnotation(Embedded.class);
if (_embedded != null) {
Field[] _embeddedFields = field.getType().getDeclaredFields();
Optional<Field> _ff = Arrays.stream(_embeddedFields)
.filter(ef -> {
Column _efc = ef.getDeclaredAnnotation(Column.class);
return _efc != null && _efc.name().equals(hibernateColumn.getName());
}).findAny();
return _ff.isPresent();
}
return false;
}
}
-- 생성된 DDL script
create table sensor (
seq bigint not null auto_increment comment '일련번호',
vendor_seq bigint not null comment '제조사 일련번호',
name varchar(200) not null comment '장비명칭',
latitude varchar(25) comment '위도',
longitude varchar(25) comment '경도',
primary key(seq)
) engine=InnoDB;