«1. Введение

На первый взгляд может показаться, что аннотации @NotNull и @Column(nullable = false) служат одной и той же цели и могут использоваться взаимозаменяемо. Однако, как мы вскоре увидим, это не совсем так.

Несмотря на то, что при использовании на объекте JPA оба они по существу предотвращают сохранение нулевых значений в базовой базе данных, между этими двумя подходами есть существенные различия.

В этом кратком руководстве мы сравним ограничения @NotNull и @Column(nullable = false).

2. Зависимости

Во всех представленных примерах мы будем использовать простое приложение Spring Boot.

Вот соответствующий раздел файла pom.xml, в котором показаны необходимые зависимости:

<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>

2.1. Образец объекта

Давайте также определим очень простой объект, который мы будем использовать в этом руководстве:

@Entity
public class Item {

    @Id
    @GeneratedValue
    private Long id;

    private BigDecimal price;
}

3. Аннотация @NotNull

Аннотация @NotNull определена в спецификации Bean Validation. Это означает, что его использование не ограничивается только сущностями. Напротив, мы можем использовать @NotNull и для любого другого компонента.

Давайте вернемся к нашему варианту использования и добавим аннотацию @NotNull в поле цены товара:

@Entity
public class Item {

    @Id
    @GeneratedValue
    private Long id;

    @NotNull
    private BigDecimal price;
}

Теперь попробуем сохранить товар с нулевой ценой:

@SpringBootTest
public class ItemIntegrationTest {

    @Autowired
    private ItemRepository itemRepository;

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

И давайте посмотрим на вывод Hibernate:

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.

Важно отметить, что Hibernate не запускает оператор вставки SQL. Следовательно, неверные данные не были сохранены в базе данных.

Это связано с тем, что событие жизненного цикла объекта pre-persist инициировало проверку компонента непосредственно перед отправкой запроса в базу данных.

3.1. Генерация схемы

В предыдущем разделе мы представили, как работает проверка @NotNull.

Давайте теперь выясним, что произойдет, если мы позволим Hibernate сгенерировать для нас схему базы данных.

По этой причине мы установим несколько свойств в нашем файле application.properties:

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

Если мы сейчас запустим наше приложение, мы увидим оператор DDL:

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

Удивительно , Hibernate автоматически добавляет ограничение not null к определению столбца цены.

Как это возможно?

Как оказалось, Hibernate по умолчанию переводит аннотации проверки bean-компонентов, применяемые к сущностям, в метаданные схемы DDL.

Это довольно удобно и имеет большой смысл. Если мы применим @NotNull к объекту, мы, скорее всего, захотим сделать соответствующий столбец базы данных также ненулевым.

Однако, если по какой-либо причине мы хотим отключить эту функцию Hibernate, все, что нам нужно сделать, это установить для свойства hibernate.validator.apply_to_ddl значение false.

Чтобы проверить это, давайте обновим наши application.properties:

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

Давайте запустим приложение и посмотрим оператор DDL:

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

Как и ожидалось, на этот раз Hibernate не добавил ненулевое значение ограничение на столбец цены.

4. Аннотация @Column(nullable = false)

Аннотация @Column определена как часть спецификации Java Persistence API.

Он используется в основном при генерации метаданных схемы DDL. Это означает, что если мы позволим Hibernate автоматически генерировать схему базы данных, он применит ограничение not null к конкретному столбцу базы данных.

Давайте обновим нашу сущность Item с помощью @Column(nullable = false) и посмотрим, как это работает в действии:

@Entity
public class Item {

    @Id
    @GeneratedValue
    private Long id;

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

Теперь мы можем попытаться сохранить нулевое значение цены:

@SpringBootTest
public class ItemIntegrationTest {

    @Autowired
    private ItemRepository itemRepository;

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

Вот фрагмент вывода Hibernate:

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 сгенерировал столбец цены с ненулевым ограничением, как мы и ожидали.

Кроме того, он смог создать запрос на вставку SQL и передать его. В результате основная база данных вызвала ошибку.

4.1. Валидация

Почти все источники подчеркивают, что @Column(nullable = false) используется только для генерации схемы DDL.

Hibernate, однако, может выполнять проверку объекта на возможные нулевые значения, даже если соответствующее поле аннотировано только @Column(nullable = false).

«Чтобы активировать эту функцию Hibernate, нам нужно явно установить для свойства hibernate.check_nullability значение true:

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

Давайте теперь снова выполним наш тестовый пример и изучим вывод:

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.

Важно отметить, что в этом случае Hibernate не отправлял SQL-запрос на вставку в базу данных.

5. Резюме

В этой статье мы описали, как работают аннотации @NotNull и @Column(nullable – false).

Несмотря на то, что оба они не позволяют нам хранить нулевые значения в базе данных, они используют разные подходы.

Как правило, мы должны предпочесть аннотацию @NotNull аннотации @Column(nullable = false). Таким образом, мы гарантируем, что проверка будет выполнена до того, как Hibernate отправит SQL-запросы на вставку или обновление в базу данных.

Кроме того, обычно лучше полагаться на стандартные правила, определенные в Bean Validation, вместо того, чтобы позволить базе данных обрабатывать логику проверки.

Но даже если мы позволим Hibernate сгенерировать схему базы данных, он преобразует аннотацию @NotNull в ограничения базы данных. Затем мы должны только убедиться, что для свойства hibernate.validator.apply_to_ddl установлено значение true.

Как обычно, все примеры кода доступны на GitHub.