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.foreach.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.foreach.h2db.springboot.models.Item,
messageTemplate='{javax.validation.constraints.NotNull.message}'}]]
(...)
Caused by: javax.validation.ConstraintViolationException: Validation failed for classes
[com.foreach.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.foreach.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 не добавил ограничение not null
в столбец цены
.
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.foreach.h2db.springboot.models.Item.price;
nested exception is org.hibernate.PropertyValueException:
not-null property references a null or transient value : com.foreach.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 .