Post

Hibernate @NotNull와 @Column(nullable = false)

1. 종속성

예제에서 Spring Boot 애플리케이션을 사용한다.

다음은 필요한 종속성을 보여주는 pom.xml 파일이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
    </dependency>
</dependencies>

1) 샘플 엔티티

전체에서 사용할 매우 간단한 엔터티 정의이다.

1
2
3
4
5
6
7
8
9
@Entity
public class Item {

    @Id
    @GeneratedValue
    private Long id;

    private BigDecimal price;
}

2. @NotNull 주석

@NotNull 주석은 Bean Validation 사양에 정의되어 있다. 즉, 엔티티에만 사용이 제한되지 않는다. 반대로, 다른 모든 Bean에서도 @NotNull을 사용할 수 있다.

Item의 가격 필드에 @NotNull 주석을 추가한다.

1
2
3
4
5
6
7
8
9
10
@Entity
public class Item {

    @Id
    @GeneratedValue
    private Long id;

    @NotNull
    private BigDecimal price;
}

이제 null 가격이 있는 항목을 지속해본다.

1
2
3
4
5
6
7
8
9
10
11
@SpringBootTest
public class ItemIntegrationTest {

    @Autowired
    private ItemRepository itemRepository;

    @Test
    public void shouldNotAllowToPersistNullItemsPrice() {
        itemRepository.save(new Item());
    }
}

Hibernate의 출력을 확인한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2019-11-14 12:31:15.070 ERROR 10980 --- [ main] o.h.i.ExceptionMapperStandardImpl : 
HHH000346: Error during managed flush [Validation failed for classes 
[com.baeldung.h2db.springboot.models.Item] during persist time for groups 
[javax.validation.groups.Default,] List of constraint violations:[
ConstraintViolationImpl{interpolatedMessage='must not be null', propertyPath=price, rootBeanClass=class 
com.baeldung.h2db.springboot.models.Item, 
messageTemplate='{javax.validation.constraints.NotNull.message}'}]]
 
(...)
 
Caused by: javax.validation.ConstraintViolationException: Validation failed for classes 
[com.baeldung.h2db.springboot.models.Item] during persist time for groups 
[javax.validation.groups.Default,] List of constraint violations:[
ConstraintViolationImpl{interpolatedMessage='must not be null', propertyPath=price, rootBeanClass=class 
com.baeldung.h2db.springboot.models.Item, 
messageTemplate='{javax.validation.constraints.NotNull.message}'}]

보시다시피 이 경우 시스템은 javax.validation.ConstraintViolationException을 throw했다.

Hibernate가 SQL insert 문을 트리거하지 않았다. 결과적으로 잘못된 데이터가 데이터베이스에 저장되지 않았다.

이는 사전 지속형 엔터티 수명 주기 이벤트가 데이터베이스로 쿼리를 보내기 직전에 빈 검증을 트리거했기 때문이다.

1) 스키마 생성

@NotNull 검증이 어떻게 작동하는지 확인했다.

Hibernate가 데이터베이스 스키마를 생성하게 하면 어떤 일이 일어나는지 확인한다.

application.properties 파일에 몇 가지 속성을 설정한다.

1
2
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true

이제 애플리케이션을 시작하면 DDL 문이 표시된다.

1
2
3
4
5
create table item (
   id bigint not null,
    price decimal(19,2) not null,
    primary key (id)
)

Hibernate는 자동으로 price 열 정의에 not null 제약 조건을 추가한다.

실제로 Hibernate는 엔터티에 적용된 빈 검증 주석을 DDL 스키마 메타데이터로 변환한다.

이것은 매우 편리하고 매우 합리적이다. 엔티티에 @NotNull을 적용하면 해당 데이터베이스 열도 null이 아니게 만들고 싶을 것이다.

하지만 어떤 이유로든 Hibernate 기능을 비활성화하고 싶다면 hibernate.validator.apply_to_ddl 속성을 false로 설정하기만 하면 된다.

이를 테스트하기 위해 application.properties를 업데이트해 본다.

1
2
3
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.validator.apply_to_ddl=false

애플리케이션을 실행하고 DDL 문을 살펴본다.

1
2
3
4
5
create table item (
   id bigint not null,
    price decimal(19,2),
    primary key (id)
)

예상대로 이번에 Hibernate는 price 열에 not null 제약 조건을 추가하지 않았다.

3. @Column(nullable = false) 주석

@Column 주석은 Java Persistence API 사양의 일부로 정의된다.

주로 DDL 스키마 메타데이터 생성에 사용된다. 즉, Hibernate가 데이터베이스 스키마를 자동으로 생성하게 하면 특정 데이터베이스 열에 not null 제약 조건이 적용된다.

@Column(nullable = false)로 Item 엔터티를 업데이트하고 이것이 실제로 어떻게 작동하는지 확인한다.

1
2
3
4
5
6
7
8
9
10
@Entity
public class Item {

    @Id
    @GeneratedValue
    private Long id;

    @Column(nullable = false)
    private BigDecimal price;
}

이제 null 가격 값을 유지하려고 시도할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
@SpringBootTest
public class ItemIntegrationTest {

    @Autowired
    private ItemRepository itemRepository;

    @Test
    public void shouldNotAllowToPersistNullItemsPrice() {
        itemRepository.save(new Item());
    }
}

Hibernate 출력의 일부이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Hibernate: 
    
    create table item (
       id bigint not null,
        price decimal(19,2) not null,
        primary key (id)
    )

(...)

Hibernate: 
    insert 
    into
        item
        (price, id) 
    values
        (?, ?)
2019-11-14 13:23:03.000  WARN 14580 --- [main] o.h.engine.jdbc.spi.SqlExceptionHelper   : 
SQL Error: 23502, SQLState: 23502
2019-11-14 13:23:03.000 ERROR 14580 --- [main] o.h.engine.jdbc.spi.SqlExceptionHelper   : 
NULL not allowed for column "PRICE"

예상했던 대로 Hibernate가 not null 제약 조건을 사용하여 price 열을 생성했다.

또한 SQL 삽입 쿼리를 생성하여 통과시킬 수 있었다. 결과적으로 오류를 트리거한 것은 기본 데이터베이스였다.

1) 검증

거의 모든 소스에서 @Column(nullable = false)는 스키마 DDL 생성에만 사용된다고 강조한다.

그러나 Hibernate는 해당 필드에 @Column(nullable = false) 주석이 달려 있는 경우에도 가능한 null 값에 대해 엔터티의 유효성 검사를 수행 할 수 있다.

이 Hibernate 기능을 활성화하려면 hibernate.check_nullability 속성을 명시적으로 true로 설정해야 한다.

1
2
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.check_nullability=true

이제 테스트 케이스를 다시 실행하고 출력을 확인한다.

1
2
3
4
org.springframework.dao.DataIntegrityViolationException: 
not-null property references a null or transient value : com.baeldung.h2db.springboot.models.Item.price; 
nested exception is org.hibernate.PropertyValueException: 
not-null property references a null or transient value : com.baeldung.h2db.springboot.models.Item.price

org.hibernate.PropertyValueException을 throw 했다.

이 경우 Hibernate가 데이터베이스에 삽입 SQL 쿼리를 전송하지 않았다.

[출처 및 참고]

This post is licensed under CC BY 4.0 by the author.