jpa converter 사용하기
열심히 entity 작성 중이다.
어제는 vo필드를 사용하기로 해서 AttributeOverrides 까지 사용하고 끝냈다.
오늘은 DB내에 있는 상태값 데이터들을 enum객체로 매핑하는 일이 필요했다.
기존에 jpa에서는 enumerated를 사용하여 String혹은 ordinal 옵션이 사용가능하다
(enum 키 값 - String / enum 순서 - ordinal)
하지만 두 가지 옵션 모두 나의 상황에는 조금 거리가 있는 문제.
찾다보니 역시 갓배민에서 답을 주었다.
참조링크
https://woowabros.github.io/experience/2019/01/09/enum-converter.html
정리하자면 이렇다
AttributeConverter 인터페이스는 Mybatis의 TypeHandler와 같은 역할을 해주는 녀석
DB의 값과 Enum의 값을 매칭해주는 녀석이다.
위의 링크에서 친절하게도 컨버터 유틸을 만들어주었고 나 또한 그대로 구현하였다.
import com.guiving.interfaces.CodeEnum;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import java.util.EnumSet;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class EnumValueConvertUtils {
public static <T extends Enum<T> & CodeEnum> T ofDBCode(Class<T> enumClass, String dbCode){
if(StringUtils.isBlank(dbCode))
return null;
return EnumSet.allOf(enumClass).stream()
.filter(v -> v.getCode().equals(dbCode))
.findAny()
.orElseThrow(() -> new IllegalArgumentException(String.format("enum=[%s], code=[%s]가 존재하지 않습니다.",enumClass.getName(),dbCode)));
}
public static <T extends Enum<T> & CodeEnum> String toDBcode(T enumValue) {
return enumValue.getCode();
}
}
링크에서는 legacyCode로 명명되어있지만 지금 나는 실제 사용하는 코드이기 때문에 그냥 Code라고 명명지었다…(흑흑) 이전 mybatis에서 사용하던 CodeEnum 인터페이스를 그대로 차용했다.
public interface CodeEnum {
String getKey();
String getCode();
String getComment();
}
위의 두 가지를 사용한 추상클래스를 만들었다.
import com.guiving.interfaces.CodeEnum;
import com.guiving.utils.enums.EnumValueConvertUtils;
import org.apache.commons.lang3.StringUtils;
import javax.persistence.AttributeConverter;
public class AbstractEnumAttributeConverter<E extends Enum<E> & CodeEnum> implements AttributeConverter<E, String> {
private Class<E> targetEnumClass;
private boolean nullable;
private String enumName;
AbstractEnumAttributeConverter(Class<E> E,boolean nullable, String enumName){
this.targetEnumClass = E;
this.nullable = nullable;
this.enumName = enumName;
}
@Override
public String convertToDatabaseColumn(E attribute) {
if (!nullable && attribute == null)
throw new IllegalArgumentException(String.format("%s 는 NULL로 지정할 수 없습니다.", enumName));
return EnumValueConvertUtils.toDBcode(attribute);
}
@Override
public E convertToEntityAttribute(String dbData){
if (!nullable && StringUtils.isBlank(dbData))
throw new IllegalArgumentException(String.format("%s 가 DB에 NULL 혹은 empty(%s)로 저장 되어 있습니다.", enumName,dbData));
return EnumValueConvertUtils.ofDBCode(targetEnumClass,dbData);
}
}
사실 참조링크에 있는 걸 다 가져다 배낀 수준이지만.
링크에는 추상클래스의 생성자가 따로 나오진 않았다.. 그래서 그냥 돌아갈 수 있게 내가 임의로 만들어 넣었는데
맞는지는 모르겠네
AbstractEnumAttributeConverter 추상 클래스를 사용하여 컨버터를 구현하면 매우 간단해진다.
@Converter(autoApply = true)
public class GenderConverter extends AbstractEnumAttributeConverter<Gender>{
public static final String ENUM_NAME = "성별";
public GenderConverter(){
super(Gender.class,false,ENUM_NAME);
}
}
이게 다..
한 가지는 컨버터 자체에 autoApply 옵션을 주어 entity에서 일일히 컨버터를 설정하지 않아도 되도록 했다.
(일일히 설정했다가 entity가 너무 더러워져 찾아보니 위처럼 하면 된다고 한다..)
그래서 작성된 entity
@ToString(exclude = "company")
@NoArgsConstructor
@Getter
@Entity
@Table(name="TB_OPERATOR")
public class Operator {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="op_idx")
private Long id;
@Column(name="op_email")
private String email;
@Column(name="op_password")
private String password;
@Column(name="op_uid")
private String uid;
@Temporal(TemporalType.DATE)
@Column(name = "op_join_date")
private Date joinDate;
@Temporal(TemporalType.DATE)
@Column(name = "op_birth")
private Date birthDate;
@Column(name="op_gender")
private Gender gender;
@Column(name="op_status")
private OperatorStatus status;
@Column(name = "op_type")
private JoinType joinType;
@Column(name = "op_language")
private Language language;
@Embedded
@AttributeOverride(name = "url",column = @Column(name = "op_img_url"))
private Picture picture;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "firstName",column = @Column(name = "op_f_name")),
@AttributeOverride(name = "lastName",column = @Column(name = "op_l_name"))
})
private Name name;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "phoneNumber",column = @Column(name = "op_phone_num")),
@AttributeOverride(name = "deviceModel",column = @Column(name = "op_device_model")),
@AttributeOverride(name = "deviceType",column = @Column(name = "op_device_type")),
@AttributeOverride(name = "deviceOS",column = @Column(name = "op_device_os_version")),
@AttributeOverride(name = "appVersion",column = @Column(name = "op_app_version")),
@AttributeOverride(name = "deviceToken",column = @Column(name = "op_device_token"))
})
private MobilePhone mobilePhone;
@ManyToOne
@JoinColumn(name = "op_company_idx")
private Company company;
}
@Data
@Embeddable
public class MobilePhone {
private String phoneNumber;
private DeviceType deviceType;
private String deviceModel;
private String deviceOS;
private String appVersion;
private String deviceToken;
}
@ToString
@Getter
@AllArgsConstructor
public enum DeviceType implements CodeEnum {
ANDROID("0", "android"),
IOS("1", "iOS");
private String code;
private String comment;
@Override
public String getKey() {
return name();
}
@Override
public String getComment() {
return comment;
}
@Override
public String getCode() {
return code;
}
}
MobilePhone을 보면 DeviceType의 enum이 존재한다. 이처럼 vo내에서도 간편하게 사용할 수 있다.
이전 프로젝트에서
CodeEnum 인터페이스를 사용해 추후 UI단에 노출되어야할 Enum 값들을 json으로 전달할 수 있도록 만들어 두었다.
(이 역시 갓 배민의 도움을 받아)
오늘은 좀 늦었군.. 여기까지