JPA(Hibernate) HBM2DDL 컬럼 순서 지정하기
Published in
22 min readMay 12, 2016
하이버네이트의 hbm2ddl을 통해 데이터베이스에 테이블을 생성할 경우, 하이버네이트는 엔티티의 컬럼들을 알파벳 순(PK는 앞쪽에 생성)으로 생성합니다.
@Entity
public class CompanyTest {@Id
private String companyCode;private String companyName;private String address;private String zipCode;private String ceoName;}
이렇게 생긴 엔티티 클래스를 생성하면 데이터베이스에는 companyCode, address, ceoName, companyName, zipCode 순으로 컬럼이 생성됩니다. ㅠ.ㅠ종종 데이터베이스에 테이블을 복구할때, hbm2ddl을 유용하게 사용하고 있는데, 매번 컬럼순서를 조정해주는 불편함때문에 애너테이션으로 컬럼순서를 원하는대로 조정 할 수 있는 간단한 유틸리티를 만들었습니다.유틸리티는 다음과 같은 방식으로 동작합니다.
- SchemaExport를 생성합니다.
- SchemaExport를 통해 DDL Script를 임시경로에 만듭니다.
- DDL Script를 읽어와서 create table로 시작하는 DDL을 검사합니다.
- create table로 시작하는 DDL의 경우, 해당 엔티티 정보를 하이버네이트 ClassMetadata를 통해 가져옵니다.
- 리플렉션을 이용해 해당 엔티티 클래스의 필드정보와 해당 필드의 애너테이션 검사를 통해 해당 필드가 몇번째 컬럼으로 만들어 져야하는지 확인합니다.
- 순서가 모두 결정되면 jdbcTemplate을 통해 DDL을 실행합니다.
SpringBoot 기반의 간단한 테스트를 통해 유틸리티를 실행할 수 있도록 코드를 만들었습니다.spring.datasource.username=sa
spring.datasource.password=
spring.datasource.url=jdbc:h2:~/hbm2ddl/dbspring.jpa.hibernate.ddl-auto=none
spring.jpa.database-platform=H2
먼저 스프링 부트가 기동되면서 hbl2ddl이 실행되지 않도록 ddl-auto 프로퍼티 값을 none으로 설정했습니다.그리고 테스트를 위해 3개의 엔티티 클래스를 생성했습니다.@Entity
public class CompanyTest {@Id
@ColumnPosition(1)
private String companyCode;@ColumnPosition(2)
private String companyName;@ColumnPosition(3)
private String address;@ColumnPosition(4)
private String zipCode;@ColumnPosition(5)
private String ceoName;}@Entity
public class Store {@Id
@ColumnPosition(1)
private String storeCode;@ColumnPosition(2)
private String storeName;private String address;@ColumnPosition(3)
private String zipCode;private String ceoName;}@DynamicInsert
@DynamicUpdate
@Entity
public class User {@Id
@Column(name = "USER_CD", length = 20)
@ColumnPosition(1)
private String userCd;@Column(name = "USER_NM", length = 30)
@ColumnPosition(2)
private String userNm;@Column(name = "USER_PS", length = 128)
@ColumnPosition(3)
private String userPs;@Column(name = "USER_TYPE", length = 15)
@ColumnPosition(4)
private String userType;@Column(name = "EMAIL", length = 30)
@ColumnPosition(5)
private String email;@Column(name = "HP_NO", length = 15)
@ColumnPosition(6)
private String hpNo;@Column(name = "LAST_LOGIN_AT")
@ColumnPosition(8)
private LocalDateTime lastLoginAt;@Column(name = "PASSWORD_UPDATED_AT")
@ColumnPosition(9)
private LocalDateTime passwordUpdatedAt;@Column(name = "USE_YN", length = 1)
@ColumnPosition(10)
private String useYn;@Column(name = "REMARK", length = 200)
@ColumnPosition(11)
private String remark;
}
@ColumnPosition 애너테이션을 다음과 같이 간단하게 만들었습니다.@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ColumnPosition {
int value();
}이제 hbm2ddl이 DDL을 뽑아내는 클래스인 SchemaExport를 생성합니다.Properties prop = new Properties();
prop.put("hibernate.dialect", getSessionFactory().getDialect().toString());
prop.put("hibernate.hbm2ddl.auto", "create");
prop.put("hibernate.show_sql", "true");
prop.put("hibernate.connection.username", environment.getProperty("spring.datasource.username", ""));
prop.put("hibernate.connection.password", environment.getProperty("spring.datasource.password", ""));
prop.put("hibernate.connection.url", environment.getProperty("spring.datasource.url", ""));PersistenceUnitInfo info = localContainerEntityManagerFactoryBean.getPersistenceUnitInfo();
PersistenceUnitInfoDescriptor persistenceUnitInfoDescriptor = new PersistenceUnitInfoDescriptor(info);
EntityManagerFactoryBuilderImpl entityManagerFactoryBuilder = new EntityManagerFactoryBuilderImpl(persistenceUnitInfoDescriptor, prop);ServiceRegistry serviceRegistry = entityManagerFactoryBuilder.buildServiceRegistry();
Configuration configuration = entityManagerFactoryBuilder.buildHibernateConfiguration(serviceRegistry);
configuration.setNamingStrategy(new SpringNamingStrategy());new SchemaExport(serviceRegistry, configuration);
하이버네이트 4버전을 기준으로는 위와 같이 작성하면 되고,하이버네이트 5에서는 몇몇 클래스들이 변경되어 다음과 같이 작성하면 됩니다.BootstrapServiceRegistry bsr = new BootstrapServiceRegistryBuilder().build();
StandardServiceRegistryBuilder ssrBuilder = new StandardServiceRegistryBuilder( bsr );Properties prop = new Properties();
prop.put("hibernate.dialect", getSessionFactory().getDialect().toString());
prop.put("hibernate.hbm2ddl.auto", "create");
prop.put("hibernate.show_sql", "true");
prop.put("hibernate.connection.username", environment.getProperty("spring.datasource.username", ""));
prop.put("hibernate.connection.password", environment.getProperty("spring.datasource.password", ""));
prop.put("hibernate.connection.url", environment.getProperty("spring.datasource.url", ""));
ssrBuilder.applySettings( prop );
StandardServiceRegistry standardServiceRegistry = ssrBuilder.build();MetadataSources metadataSources = new MetadataSources( standardServiceRegistry );
MetadataBuilder metadataBuilder = metadataSources.getMetadataBuilder();EnumSet<TargetType> targetTypes = EnumSet.of(TargetType.DATABASE, TargetType.STDOUT);
new SchemaExport().createOnly(targetTypes, metadataBuilder.build());public void createSchema() throws IOException, ClassNotFoundException {
SchemaExport schemaExport = getSchemaExport();
String scriptOutputPath = System.getProperty("java.io.tmpdir") + "/schema.sql";
schemaExport.setOutputFile(scriptOutputPath);
schemaExport.create(false, true);List<String> DDLs = IOUtils.readLines(new FileInputStream(scriptOutputPath), "UTF-8");
List<String> convertedDDLs = new ArrayList<>();for (String DDL : DDLs) {
if (DDL.toLowerCase().startsWith("create table")) {
convertedDDLs.add(convert(DDL));
} else {
convertedDDLs.add(DDL);
}
}for (String convertedDDL : convertedDDLs) {
System.out.println(convertedDDL);
jdbcTemplate.execute(convertedDDL);
}
}
SchemaExport를 이용해 임시경로에 schema.sql 파일을 생성하고, 생성한 파일을 읽어온 후, create table로 시작하는 DDL을 찾아서 convert() 메서드를 호출하도록 했습니다.convert() 메서드를 살펴보면,private String convert(String ddl) throws ClassNotFoundException {
StringBuilder convertedDDL = new StringBuilder();int startColumnBody = ddl.indexOf('(');
int endColumnBody = ddl.lastIndexOf(')');String tableName = ddl.substring("create table ".length(), startColumnBody).trim();
String columnBody = ddl.substring(startColumnBody + 1, endColumnBody);List<ColumnDefinition> columnDefinitions = Arrays.stream(columnBody.split(", ")).map(ColumnDefinition::new).collect(toList());ClassMetadata classMetadata = getClassMetaData(tableName);Class<?> clazz = Class.forName(classMetadata.getEntityName());
Field[] fields = clazz.getDeclaredFields();for (Field field : fields) {
setPosition(field, columnDefinitions);
}convertedDDL
.append("create table")
.append(" ")
.append(tableName)
.append(" ")
.append("(");StringJoiner columns = new StringJoiner(", ");columnDefinitions
.stream()
.sorted(
Comparator.comparingInt(ColumnDefinition::getPosition)
.thenComparing(ColumnDefinition::getColumnName))
.forEach(entityField -> {
columns.add(entityField.getColumnDefinition());
});convertedDDL.append(columns.toString());convertedDDL.append(")");return convertedDDL.toString();
}
DDL을 substring 해서 테이블 이름과, 컬럼들의 선언부를 찾은 후 컬럼들은 ColumnDefinition 타입의 클래스로 생성했습니다.이후 테이블 이름으로 ClassMetadata를 가져와서 엔티티 클래스를 생성했고, 해당 엔티티 클래스에 있는 필드들을 가져와 setPosition() 메서드를 호출하고 있습니다.ColumnDefinition 클래스와 setPosition() 메서드를 살펴보면,public class ColumnDefinition {private String columnName;private String definition;private String columnDefinition;private int position = Integer.MAX_VALUE - 10;public ColumnDefinition(String columnDefinition) {
this.columnDefinition = columnDefinition;
this.columnName = columnDefinition.split(" ")[0];
this.definition = columnDefinition.split(" ")[1];if (columnDefinition.toLowerCase().startsWith("primary key")) {
position = Integer.MAX_VALUE;
}
}public String getColumnDefinition() {
return columnDefinition;
}public String getColumnName() {
return columnName;
}public int getPosition() {
return position;
}public String getDefinition() {
return definition;
}public void setPosition(int position) {
this.position = position;
}
}
ColumnDefinition 클래스는 user_name varchar(100)과 같은 DDL을, user_name, varchar(100)과 같이 이름/타입으로 구분하도록 하고, 해당 컬럼이 어디에 위치해야할지 저장할 position 필드를 가지고 있습니다. 이때 PK를 생성하는 구문이면 가장 마지막에 위치하도록 예외처리를 했습니다.public void setPosition(Field field, List<ColumnDefinition> columnDefinitions) {
String name = field.getName();
String columnName;
int position = Integer.MAX_VALUE - 10;if (field.getAnnotation(Transient.class) == null) {
Column column = field.getAnnotation(Column.class);
ColumnPosition columnPosition = field.getAnnotation(ColumnPosition.class);if (column != null && !"".equals(column.name())) {
columnName = column.name();
} else {
columnName = new SpringNamingStrategy().columnName(name);
}if (columnPosition != null && columnPosition.value() > 0) {
position = columnPosition.value();
}for (ColumnDefinition columnDefinition : columnDefinitions) {
if (columnDefinition.getColumnName().toLowerCase().equals(columnName.toLowerCase())) {
columnDefinition.setPosition(position);
}
}
}
}
다음은 setPosition() 메서드입니다. 엔티티의 해당 필드를 검사하여 컬럼 위치 애너테이션이 있는지 찾아서 ColumnDefinition의 컬럼이름과 일치하는 녀석의 position 값을 설정하도록 했습니다.for (Field field : fields) {
setPosition(field, columnDefinitions);
}convertedDDL
.append("create table")
.append(" ")
.append(tableName)
.append(" ")
.append("(");StringJoiner columns = new StringJoiner(", ");columnDefinitions
.stream()
.sorted(
Comparator.comparingInt(ColumnDefinition::getPosition)
.thenComparing(ColumnDefinition::getColumnName))
.forEach(entityField -> {
columns.add(entityField.getColumnDefinition());
});convertedDDL.append(columns.toString());convertedDDL.append(")");return convertedDDL.toString();
리스트의 ColumnDefinition들을 position 순서대로 정렬 후, @ColumnPosition 애너테이션이 없는 필드는 알파베 순으로 정렬되도록 했습니다.이제 하이버네이트가 생성했던 알파벳순서의 DDL은, 엔티티 클래스의 @ColumnPosition 애너테이션 유무와, position 값에 따라 순서있는 DDL로 변환이 되었습니다.마지막으로 jdbcTemplate을 통해 변환된 DDL을 실행해주면, 다음과 같이 지정한 컬럼순서대로 테이블이 생성됩니다!// 하이버네이트 SchemaExport가 생성한 DDL
Hibernate: drop table company_test if exists
Hibernate: drop table store if exists
Hibernate: drop table user if exists
Hibernate: create table company_test (company_code varchar(255) not null, address varchar(255), ceo_name varchar(255), company_name varchar(255), zip_code varchar(255), primary key (company_code))
Hibernate: create table store (store_code varchar(255) not null, address varchar(255), ceo_name varchar(255), store_name varchar(255), zip_code varchar(255), primary key (store_code))
Hibernate: create table user (user_cd varchar(20) not null, email varchar(30), hp_no varchar(15), last_login_at binary(255), password_updated_at binary(255), remark varchar(200), use_yn varchar(1), user_nm varchar(30), user_ps varchar(128), user_type varchar(15), primary key (user_cd))
2016-05-13 05:51:44.801 INFO 60497 --- [ main] org.hibernate.tool.hbm2ddl.SchemaExport : HHH000230: Schema export complete// 지정한 컬럼 순서대로 변환된 DDL
drop table company_test if exists
drop table store if exists
drop table user if exists
create table company_test (company_code varchar(255) not null, company_name varchar(255), address varchar(255), zip_code varchar(255), ceo_name varchar(255), primary key (company_code))
create table store (store_code varchar(255) not null, store_name varchar(255), zip_code varchar(255), address varchar(255), ceo_name varchar(255), primary key (store_code))
create table user (user_cd varchar(20) not null, user_nm varchar(30), user_ps varchar(128), user_type varchar(15), email varchar(30), hp_no varchar(15), last_login_at binary(255), password_updated_at binary(255), use_yn varchar(1), remark varchar(200), primary key (user_cd))
예제 소스코드는 Github에 올려두었습니다.https://github.com/brant-hwang/hbm2ddl-column-position