Discovering a strange behavior during SpringBoot JPA testing Please advise. What I want is a column that has a default value and is not null. I created it like this
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Entity
@DynamicInsert
public class Board {
@GeneratedValue(strategy = GenerationType.AUTO)
@Id
private Long seq;
@Column(length = 320, nullable = false)
@ColumnDefault("'system'")
/* i don't won't it this way */
// @Builder.Default
// private String usrId = "system";
private String usrId;
}
@SpringBootApplication
public class SpringBootJpaTestApplication {
private static final Logger log = LoggerFactory.getLogger(SpringBootJpaTestApplication.class);
public static void main(String[] args) {
SpringApplication.run(SpringBootJpaTestApplication.class, args);
}
@Bean
CommandLineRunner demo(TestRepository repository) {
return (args) -> {
for(int i = 0; i < 1; i++) {
// Test test = Test.builder().build();
Test test = new Test();
test.setTitle("test title " + i);
test.setContent("test content " + i);
System.out.println(test.toString());
repository.save(test);
}
repository.findAll().forEach(test -> {
log.info(test.toString());
});
repository.findByTitle("test").forEach(bbs -> {
log.info(bbs.toString());
});
};
}
}
This seemed to work fine. However, if you comment out the following, an error occurs
<!--
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
-->
Error description
//--------------------------------------------------------------------------------
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
[2m2024-03-14T23:09:48.901+09:00[0;39m [31mERROR[0;39m [35m2776[0;39m [2m---[0;39m [2m[SpringBootJpaTest] [ main][0;39m [2m[0;39m[36mo.s.boot.SpringApplication [0;
39m [2m:[0;39m Application run failed
org.springframework.dao.DataIntegrityViolationException: not-null property references a null or transient value : com.demo.SpringBootJpa.Test.usrId
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:307) ~[spring-orm-6.1.4.jar:6.1.4]
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:241) ~[spring-orm-6.1.4.jar:6.1.4]
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:550) ~[spring-orm-6.1.4.jar:6.1.4]
at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61) ~[spring-tx-6.1.4.jar:6.1.4]
at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:335) ~[spring-tx-6.1.4.jar:6.1.4]
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:152) ~[spring-tx-6.1.4.jar:6.1.4]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.4.jar:6.1.4]
at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:164) ~[spring-data-jpa-3.2
.3.jar:3.2.3]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.4.jar:6.1.4]
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97) ~[spring-aop-6.1.4.jar:6.1.4]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.4.jar:6.1.4]
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:220) ~[spring-aop-6.1.4.jar:6.1.4]
at jdk.proxy2/jdk.proxy2.$Proxy94.save(Unknown Source) ~[na:na]
at com.skywhalelab.SpringBootJpa.SpringBootJpaTestApplication.lambda$0(SpringBootJpaTestApplication.java:31) ~[classes/:na]
at org.springframework.boot.SpringApplication.lambda$callRunner$5(SpringApplication.java:790) ~[spring-boot-3.2.3.jar:3.2.3]
at org.springframework.util.function.ThrowingConsumer$1.acceptWithException(ThrowingConsumer.java:83) ~[spring-core-6.1.4.jar:6.1.4]
at org.springframework.util.function.ThrowingConsumer.accept(ThrowingConsumer.java:60) ~[spring-core-6.1.4.jar:6.1.4]
at org.springframework.util.function.ThrowingConsumer$1.accept(ThrowingConsumer.java:88) ~[spring-core-6.1.4.jar:6.1.4]
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:798) ~[spring-boot-3.2.3.jar:3.2.3]
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:789) ~[spring-boot-3.2.3.jar:3.2.3]
at org.springframework.boot.SpringApplication.lambda$callRunners$3(SpringApplication.java:774) ~[spring-boot-3.2.3.jar:3.2.3]
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183) ~[na:na]
at java.base/java.util.stream.SortedOps$SizedRefSortingSink.end(SortedOps.java:357) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:510) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) ~[na:na]
at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150) ~[na:na]
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[na:na]
at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596) ~[na:na]
at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:774) ~[spring-boot-3.2.3.jar:3.2.3]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:341) ~[spring-boot-3.2.3.jar:3.2.3]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1354) ~[spring-boot-3.2.3.jar:3.2.3]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1343) ~[spring-boot-3.2.3.jar:3.2.3]
at com.skywhalelab.SpringBootJpa.SpringBootJpaTestApplication.main(SpringBootJpaTestApplication.java:16) ~[classes/:na]
//--------------------------------------------------------------------------------
Can you tell me why this is happening? Of course, I know that if I use Builder.Default and give it a default value, it works fine. But that's not the direction I want to go. I want to handle it so that if usrId goes to null, I don't get an error and the DB puts in the Default value.
Thank you in advance.
Thank you for your answer.
Let me clarify my question further.
In JPA, if the @DynamicInsert annotation is present and the Entity has a field with a null value, it will not be included when inserting.
It works like this
Hibernate:
create table test (
del_yn varchar(1) default 'N' not null,
dth integer default 0,
rd_cnt integer default 0 not null,
bbs_cd varchar(8) default 'NONE',
cre_dt datetime(6) default sysdate() not null,
mod_dt datetime(6),
seq bigint not null,
upr_seq bigint,
title varchar(128),
usr_id varchar(320) default 'system' not null,
content tinytext,
primary key (seq)
) engine=InnoDB
[2m2024-03-16T07:51:16.144+09:00[0;39m [32m INFO[0;39m [35m6540[0;39m [2m---[0;39m [2m[SpringBootJpaTest] [ main][0;39m [2m[0;39m[36mj.LocalContainerEntityManagerFactoryBean[0;
39m [2m:[0;39m Initialized JPA EntityManagerFactory for persistence unit 'default'
[2m2024-03-16T07:51:16.470+09:00[0;39m [32m INFO[0;39m [35m6540[0;39m [2m---[0;39m [2m[SpringBootJpaTest] [ main][0;39m [2m[0;39m[36mc.s.S.SpringBootJpaTestApplication [0;
39m [2m:[0;39m Started SpringBootJpaTestApplication in 7.923 seconds (process running for 8.568)
Test(seq=null, bbsCd=null, title=test title 0, content=test content 0, uprSeq=null, rdCnt=null, dth=null, delYn=null, usrId=null, creDt=null, modDt=null)
Hibernate:
select
next value for test_seq
Hibernate:
insert
into
test
(content, title, seq)
values
(?, ?, ?)
This works perfectly fine.
In other words, if usrId is Null, it is not included in the Insert syntax.
This is because of the @DynamicInsert annotation.
However, if we remove the spring-boot-starter-validation dependency, we get an error.
Here is what is commonly known
if [nullable = false] in the @Column annotation, make the table column [not null].
if entity have the @DynamicInsert annotation, if the value of a particular field is Null, remove that field from the Insert syntax.
The difference between having spring-boot-starter-validation in pom.xml and not having it is as follows
The prerequisites are
In this case, the following happens
For example, this column behaves as follows when null.
@Column(nullable = false)
private String testColumn;
without spring-boot-starter-validation, it will be checked for not-null and an error will be thrown. Same behaviour as above.
If spring-boot-starter-validation is present, it is not included in the Insert syntax due to the @DynamicInsert annotation.
However, the table column is not assigned Default, so an error is thrown.
org.springframework.orm.jpa.JpaSystemException: could not execute statement [(conn=1624) Field 'test_column' doesn't have a default value] [insert into test (content,title,seq) values (?,?,?)]
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:341) ~[spring-orm-6.1.4.jar:6.1.4]
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:241) ~[spring-orm-6.1.4.jar:6.1.4]
at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:565) ~[spring-orm-6.1.4.jar:6.1.4]
To summarize, my question is this.
the entity has the @DynamicInsert annotation declared.
a certain field has @Column(nullable = false) declared on it.
the specific field value is null.
If the spring-boot-starter-validation dependency is not present in the pom.xml, the null check will result in an error.
If there is a spring-boot-starter-validation dependency in the pom.xml, it does not check null.
Any advice would be greatly appreciated.
I've uploaded the source I tested to git.
If you could take a look and give me some advice, it would be greatly appreciated :)
git: github.com/jykim31337/SpringBootJpaTest.git
I also didn't quite understand the intent of the problem. thank you :)
The cause of the problem lies in the hibernate.check_nullability check method.
@Column(nullable=false) is a Hibernate annotation.
I verified it with the example below. :)
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// implementation 'org.springframework.boot:spring-boot-starter-validation'
}
@Getter
@NoArgsConstructor
@Entity
@DynamicInsert
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long seq;
@Column(length = 320, nullable = false)
@ColumnDefault("'system'")
private String usrId;
}
Create a custom Hibernate configuration class, change the check_nullability property to false, and run it.
@Configuration
public class HibernateConfig {
@Bean
public HibernatePropertiesCustomizer hibernatePropertiesCustomizer() {
return hibernateProperties -> hibernateProperties.put(Environment.CHECK_NULLABILITY, false);
}
}
//skip logic...
Board board = new Board();
boardRepository.save(board);
//...
Hibernate:
select
next value for board_seq
Hibernate:
insert
into
board
(seq)
values
(?)
Hibernate:
select
b1_0.seq,
b1_0.usr_id
from
board b1_0
I hope it will be of help.
Let's briefly summarize the flow with and without the NULL constraint below.
However, there is something that can be a bit confusing.
INSERT INTO BOARD(SEQ) VALUES(1); //this case insert default value in USR_ID
INSERT INTO BOARD(SEQ, USR_ID) VALUES(4, null); //NULL not allowed for column
In conclusion, JPA does not work as the questioner intended.
@Getter
@NoArgsConstructor
@Entity
@DynamicInsert
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long seq;
@Column(length = 320, nullable = false)
@ColumnDefault("'system'")
private String usrId;
public Board(String userId) {
if (userId == null || userId.isEmpty()) {
this.usrId = "system";
return;
}
this.usrId = userId;
}
}