현재 프로젝트를 하면서 Expense
라는 테이블이 필요해졌다.
DB는 MySql을 사용하고 있다.
ColumnName | DataType |
---|---|
Id | BIGINT |
Currency | VARCHAR(50) |
Amount | DECIMAL(38,3) |
Category_id | BIGINT |
해당 테이블을 java코드로 작성하면 다음과 같다.
@Entity
@Getter
@NoArgsConstructor(access = PROTECTED)
public class Expense extends BaseEntity {
@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
@Column(nullable = false)
private String currency;
private Amount amount;
@ManyToOne(fetch = LAZY, cascade = CascadeType.PERSIST)
@JoinColumn(name = "category_id", nullable = false)
private Category category;
...
}
Expense
의 비용은 값객체인 Amount
클래스를 만들어서 표현했다.
@Getter
public class Amount extends Number {
@Column(name = "amount", precision = 38, scale = 3)
private BigDecimal value;
...
}
application.yml
의 jpa.hibernate.ddl-auto: create
로 하고 실행시켜보자. Expense
테이블을 생성하는 쿼리는 다음과 같다.
create table expense (
category_id bigint not null,
id bigint not null auto_increment,
currency varchar(255) not null,
amount varbinary(255),
primary key (id)
) engine=InnoDB
amount
가 varbinary
라는 타입으로 할당된 것을 볼 수 있다. varbinary
는 바이너리 데이터를 저장할 때 사용되는 타입이라고 한다.
BigDecimal
타입을 Amount
의 value
에 할당하고 Expense
객체를 save
하면 varbinary
의 최댓값을 초과했다는 예외가 발생한다.
Caused by: com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Data too long for column 'amount' at row 1
특별한 처리없이 값객체를 사용하면 varbinary
타입으로 할당되어, 예측하지 않은 결과가 일어날 수 있다는 걸 배웠다. 그럼 값객체는 어떻게 사용해야 할까?
@Embeddable
@Getter
@NoArgsConstructor(access = PROTECTED)
public class Amount extends Number {
private BigDecimal value;
...
}
값객체에 @Embeddable
어노테이션을 붙여주면 해결된다. @Embeddable
은 해당 엔티티가 다른 엔티티에게 내장될 것임을 나타낸다고 한다. 밸덩 참고
값객체외에도 객체간의 상속 관계를 표현할 때 자주 사용되는 어노테이션이다. (부모클래스를 테이블화하고 싶지 않은 경우 @Embeddable
을 적용)
내장 객체를 필드로 둘 때는 @Embedded
를 사용한다.
@Entity
@Getter
@NoArgsConstructor(access = PROTECTED)
@SQLDelete(sql = "UPDATE expense SET status = 'DELETED' WHERE id = ?")
@Where(clause = "status = 'USABLE'")
public class Expense extends BaseEntity {
@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
@Column(nullable = false)
private String currency;
@Embedded
private Amount amount;
}
Amount
에 @Embeddable
을 붙인 상태에서 어플리케이션을 실행하면 다음과 같은 DDL이 생성된다.
create table expense (
value decimal(38,2),
category_id bigint not null,
created_at datetime(6) not null,
id bigint not null auto_increment,
modified_at datetime(6) not null,
currency varchar(255) not null,
status enum ('DELETED','USABLE') not null,
primary key (id)
) engine=InnoDB;
두둥! 타입은 varbinary
에서 decimal(38,2)
로 잘 바뀐 것을 볼 수 있지만 컬럼명이 value
인 것을 알 수 있다. 컬럼명을 amount
로 바꾸기 위해 value
필드에 @Column
을 사용해서 이름을 설정해준다.
@Embeddable
@Getter
@NoArgsConstructor(access = PROTECTED)
public class Amount extends Number {
public static final Amount ZERO = new Amount(0);
@Column(name = "amount", precision = 38, scale = 3)
private BigDecimal value;
...
}
이제 어플리케이션을 실행시키면 원하는 DDL문이 실행되는 것을 볼 수 있다.
create table expense (
amount decimal(38,3),
category_id bigint not null,
created_at datetime(6) not null,
id bigint not null auto_increment,
modified_at datetime(6) not null,
currency varchar(255) not null,
status enum ('DELETED','USABLE') not null,
primary key (id)
) engine=InnoDB;
@Entity
@Getter
@NoArgsConstructor(access = PROTECTED)
@SQLDelete(sql = "UPDATE expense SET status = 'DELETED' WHERE id = ?")
@Where(clause = "status = 'USABLE'")
public class Expense extends BaseEntity {
@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
@Column(nullable = false)
private String currency;
// @Embedded
private Amount amount;
}
@Embeddable
과 @Embedded
중 하나만 붙여도 내장객체로 잘 동작한다. hibernate를 사용하면 둘 중 하나의 어노테이션만 사용해도, 동작한다고 한다. 스택오버플로우 참고
그래도 명시적으로 내장객체임을 암시하기 위해 두 어노테이션을 사용하는 게 좋을 것 같다.
헐 홍고 교수님 잘 지내시나여 이 블로그를 이제서야 보다닛