spring-data-jpa
spring-data-jpa copied to clipboard
@EmbeddedId breaking @CreatedDate
added an @EmbeddedId
and now I can't save
not-null property references a null or transient value : com.capitalone.e1.domain.core.ExceptionEntity.createdOn; nested exception is org.hibernate.PropertyValueException: not-null property references a null or transient value : com.capitalone.e1.domain.core.ExceptionEntity.createdOn
org.springframework.dao.DataIntegrityViolationException: not-null property references a null or transient value : com.capitalone.e1.domain.core.ExceptionEntity.createdOn; nested exception is org.hibernate.PropertyValueException: not-null property references a null or transient value : com.capitalone.e1.domain.core.ExceptionEntity.createdOn
Code that triggers the behavior*
package com.capitalone.e1.util.jpa
import com.github.f4b6a3.uuid.UuidCreator
import java.io.Serializable
import java.util.Objects
import java.util.UUID
import javax.persistence.MappedSuperclass
import javax.persistence.Transient
@MappedSuperclass
abstract class AbstractId : Serializable {
protected var id: UUID = UuidCreator.getTimeOrdered()
/**
* checks that other is the same instance of this
*/
protected abstract fun canEqual(other: AbstractId): Boolean
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other is AbstractId && other.canEqual(this) && this.id == other.id) return true
return false
}
override fun hashCode(): Int = Objects.hash(this.id)
companion object {
@Transient
private const val serialVersionUID: Long = 0
}
}
package com.capitalone.e1.domain.core
import com.capitalone.e1.util.jpa.AbstractId
import javax.persistence.Embeddable
import javax.persistence.Transient
@Embeddable
class ExceptionEntityId : AbstractId() {
override fun canEqual(other: AbstractId): Boolean = other is ExceptionEntityId
companion object {
@Transient
private const val serialVersionUID: Long = 1
}
}
package com.capitalone.e1.util.jpa
import org.apache.commons.lang3.builder.ToStringBuilder
import org.apache.commons.lang3.builder.ToStringStyle
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedDate
import java.time.OffsetDateTime
import java.util.Objects
import javax.persistence.Access
import javax.persistence.AccessType
import javax.persistence.Column
import javax.persistence.EmbeddedId
import javax.persistence.MappedSuperclass
import javax.persistence.Version
@MappedSuperclass
@Access(AccessType.PROPERTY)
abstract class AbstractBaseEntity<ID : AbstractId> {
@get:EmbeddedId
@get:Column(columnDefinition = "uuid", nullable = false, updatable = false, unique = true)
abstract var id: ID
protected set
@get:Version
protected open var version: Int = 0
@get:CreatedDate
@get:Column(name = "created_on", columnDefinition = "timestamp with time zone", nullable = false, updatable = false)
open var createdOn: OffsetDateTime? = null
protected set
@get:LastModifiedDate
@get:Column(name = "last_modified_on", columnDefinition = "timestamp with time zone", nullable = false)
open var lastModifiedOn: OffsetDateTime? = null
protected set
/**
* checks that other is the same instance of this
*/
abstract fun canEqual(other: AbstractBaseEntity<*>): Boolean
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other is AbstractBaseEntity<*> && other.canEqual(this) && this.id == other.id) return true
return false
}
override fun hashCode(): Int = Objects.hash(this.id)
override fun toString(): String {
return ToStringBuilder.reflectionToString(
this,
ToStringStyle.MULTI_LINE_STYLE
)
}
}
package com.capitalone.e1.domain.core
import com.capitalone.e1.util.jpa.AbstractBaseEntity
import javax.persistence.Column
import javax.persistence.EmbeddedId
import javax.persistence.Entity
import javax.persistence.Table
@Entity
@Table(name = "exceptions")
open class ExceptionEntity() : AbstractBaseEntity<ExceptionEntityId>() {
@Column(name = "business_division_id", columnDefinition = "text")
open var businessDivisionId: String? = null
protected set
@EmbeddedId
override var id: ExceptionEntityId = ExceptionEntityId()
constructor(businessDivisionId: String) : this() {
this.businessDivisionId = businessDivisionId
}
override fun canEqual(other: AbstractBaseEntity<*>): Boolean = other is ExceptionEntity
}
package com.capitalone.e1.domain.core
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
@DataJpaTest
internal open class ExceptionRepositoryTest {
@Test
fun save(@Autowired exceptionDao: ExceptionRepository) {
val toCreate = ExceptionEntity("someid")
val saved = exceptionDao.save(toCreate)
assertThat(saved).isInstanceOf(ExceptionEntity::class.java)
.extracting({ it.id }, { it.businessDivisionId })
.containsExactly(toCreate.id, "someid")
.doesNotContainNull()
assertThat(saved.lastModifiedOn).isSameAs(saved.createdOn)
}
}
Versions
org.springframework.boot:spring-boot-starter-data-jpa:2.7.3
org.springframework.data:spring-data-jpa:2.7.2
org.springframework.boot:spring-boot-starter-parent:2.7.3
P.S. Unrelated (as it's not in the dependency chain), if you have any time to look at this, I'm trying to inject from my own custom time provider https://github.com/spring-projects/spring-data-commons/issues/2436#issuecomment-1242268823
This issue tracker is for bug reports and feature/improvement requests for Spring Data JPA.
This question is more of a usage questions. Those should be asked at Stackoverflow and probably tagged with jpa
and hibernate
since I don't see any relation to Spring Data JPA
By using Stackoverflow, the community can assist and the questions and their answers can more easily be found using the search engine of your choice.
If you think this is a bug in Spring Data JPA please provide a reproducer demonstrating that the code works correct with plain JPA/Hibernate but fails with Spring Data JPA
This isn't a usage question. If I use @Id then the auditing works. If I remove the auditing and I use the embedded ID then that works. You want to tell me how that's not a bug.
Or are these audit attributes only provided and set within the spring data Commons? Because they are annotations only provided by spring data Commons. Thus it would be impossible for me to provide an example that worked with just hibernate. I assume, perhaps incorrectly that the actual processing of these annotations is specific to the jpa provider.
It is certain that this only happens in conjunction with these auditing metadata options in conjunction with @embeddable. So it would seem that the problem only exists when you can bind spring data Commons auditing annotations with hibernates @embedded functionality. Removing one or the other causes the code to execute just fine. Thus it seems reasonable to me that this problem can only exist when using spring data jpa.
So given that it only happens with hibernate/jpa plus spring data Commons functionality. Which project should I be bugging about this? Given that there is no problem if I do not try to use either embeddable or the auditing annotations I'm having trouble with the idea that this isn't a bug, as I obviously know how to use each independently.
Ok, if you have complete reproducer, if possible without inheritance and Kotlin, I'll take a look.
package example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
package example;
import org.springframework.data.annotation.CreatedDate;
import javax.persistence.*;
import java.time.OffsetDateTime;
@Entity
@Access(AccessType.PROPERTY)
public class MyEntity {
private MyEntityId id = new MyEntityId();
private OffsetDateTime created;
void setId(MyEntityId id) {
this.id = id;
}
@EmbeddedId
public MyEntityId getId() {
return id;
}
@CreatedDate
@Column(name = "created_on", columnDefinition = "timestamp with time zone", nullable = false, updatable = false)
public OffsetDateTime getCreated() {
return created;
}
void setCreated(OffsetDateTime created) {
this.created = created;
}
}
package example;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.io.Serializable;
import java.util.UUID;
@Embeddable
public class MyEntityId implements Serializable {
private UUID id = UUID.randomUUID();
@Column(columnDefinition = "uuid", nullable = false, updatable = false, unique = true)
UUID getId() {
return id;
}
void setId(UUID id) {
this.id = id;
}
}
package example;
import org.springframework.data.repository.CrudRepository;
public interface MyEntityRepository extends CrudRepository<MyEntity, MyEntityId> {
}
package example;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
public class MyEntityRepositoryTest {
@Test
void testSave(@Autowired MyEntityRepository repo ) {
var saved = repo.save(new MyEntity());
assertThat(saved).extracting(MyEntity::getId).isInstanceOf(MyEntityId.class);
var found = repo.findAll();
assertThat(found).extracting(MyEntity::getId).contains(saved.getId());
}
}
I think i narrowed it down farther, I may be wrong about its relationship to @EmbeddedId
(or maybe there's another behavior I've noticed). It doesn't want me to set the annotations on the methods.
this is working
@CreatedDate
@Column(name = "created_on", columnDefinition = "timestamp with time zone", nullable = false, updatable = false)
private OffsetDateTime created;
@CreatedDate
@Column(name = "created_on", columnDefinition = "timestamp with time zone", nullable = false, updatable = false)
public OffsetDateTime getCreated() {
return created;
}
but @CreatedDate
is allowed on methods, and so is Column. So that seems like an issue.
not really a full project, it's part of our monorepo, so you'd have to set up libs.versions.toml and settings.gradle.kts appropriately.
interestingly, back to kotlin, this is still failing a load test...
@field:CreatedDate
@field:Column(name = "created_on", columnDefinition = "timestamp with time zone", nullable = false, updatable = false)
open var createdOn: OffsetDateTime? = null
protected set
@field:LastModifiedDate
@field:Column(name = "last_modified_on", columnDefinition = "timestamp with time zone", nullable = false)
open var lastModifiedOn: OffsetDateTime? = null
protected set
ah, so an update of the java I shared, this fails too
@DataJpaTest
public class MyEntityRepositoryTest {
@Test
void testSave(@Autowired MyEntityRepository repo ) {
var saved = repo.save(new MyEntity());
assertThat(saved).extracting(MyEntity::getId).isInstanceOf(MyEntityId.class);
var found = repo.findAll();
assertThat(found).extracting(MyEntity::getId).contains(saved.getId());
assertThat(found).first().extracting(MyEntity::getCreated).isNotNull();
}
}
hmm... if I set the property up this way, I'm back to it not saving because of an NPE... did it ever really save? this behavior is a bit wacko
@CreatedDate
@Access(AccessType.FIELD)
@Column(name = "created_on", columnDefinition = "timestamp with time zone", nullable = false, updatable = false)
private OffsetDateTime created;
yeah, ok right now I'm not sure this has anything to do with @Embedded, this also fails
@CreatedDate
private OffsetDateTime created;
@Column(name = "created_on", columnDefinition = "timestamp with time zone", nullable = false, updatable = false)
public OffsetDateTime getCreated() {
return created;
}
for some strange reason though, this doesn't fail until the later load, allegedly it's inserting, but it's not retrieving
@CreatedDate
@Column(name = "created_on", columnDefinition = "timestamp with time zone", nullable = false, updatable = false)
private OffsetDateTime created;
public OffsetDateTime getCreated() {
return created;
}
java-sample.tar.gz btw, sorry for the comment spam, work doesn't let us edit them. I'm gonna go back to bed now. I'll see if I missed something in the morning, but either way I'm finding this behavioral interaction with hibernate strange. I do feel like I had an earlier build though that worked better, so I might have to dig a bit for that. Still... sleep again now (yay insomnia!!!)
Have a good night.
not really a full project, it's part of our monorepo, so you'd have to set up libs.versions.toml and settings.gradle.kts appropriately.
I'll need a reproducer that I can run as is. Sorry to be strict about this, but trying to fix builds eats tons of time.
That last tar I uploaded should work. Are you having problems with it? A couple of things are hard to check due to corporate proxy stuff.
not to be a nag, but any chance you've had to take a look at that jar?
- tar
@schauder sorry, I'm a jerk, I know, I'm departing this company soon though and would ideally like to have this wrapped up.
I tried to look into this. Your sample only demonstrates the failing case, not the not failing one. So I'm not sure which versions I should compare.
The build does not configure the necessary maven repositories.
The Spring Data Repository declares the wrong ID
type, although that doesn't seem to have an effect.
I also couldn't find where you activate @EnableJpaAuditing
.
Could we loose the @Access(AccessType.PROPERTY)
? Or is this relevant for the problem?
Also AbstractEntity
and MyEntityId
aren't relevant either, are they?
I'd appreciate if you could put the sample on GitHub. Different branches would be nice to demonstrate the different variants we discussing, because honestly right now, I'm quite confused about what to look at.
Huh, some of that is weird. However I was teasing a project apart that was an active development so it's entirely possible some of those things got missed...
Unfortunately issue with creating a GitHub repository is I'm not allowed to push from here... For some reason they think that that's going to keep code or something from leaking out... Basically it would be as easy for you to do it... In some ways easier. Although now that the code is uploaded anywhere in a relatively working form then it's just a matter of me pulling the tar down myself to my own personal computer.
Interesting... I wonder what I've changed... sorry it's still not a repo
in the test logs
DEBUG - insert into exceptions (created_on, last_modified_on, version, business_division_id, id) values (?, ?, ?, ?, ?) : org.hibernate.SQL
Hibernate: insert into exceptions (created_on, last_modified_on, version, business_division_id, id) values (?, ?, ?, ?, ?)
TRACE - binding parameter [1] as [TIMESTAMP] - [null] : org.hibernate.type.descriptor.sql.BasicBinder
TRACE - binding parameter [2] as [TIMESTAMP] - [null] : org.hibernate.type.descriptor.sql.BasicBinder
TRACE - binding parameter [3] as [INTEGER] - [0] : org.hibernate.type.descriptor.sql.BasicBinder
TRACE - binding parameter [4] as [VARCHAR] - [a] : org.hibernate.type.descriptor.sql.BasicBinder
TRACE - binding parameter [5] as [BINARY] - [0183ae2a-231f-7783-b922-ba65ef1a490d] : org.hibernate.type.descriptor.sql.BasicBinder
... why is it even allowing the null insert
import org.springframework.beans.factory.ObjectFactory
import org.springframework.beans.factory.config.BeanFactoryPostProcessor
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Scope
import org.springframework.data.auditing.DateTimeProvider
import org.springframework.data.jpa.repository.config.EnableJpaAuditing
import org.springframework.transaction.support.SimpleTransactionScope
import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.util.Optional
@Configuration
@EnableJpaAuditing
open class TransactionScopeTimeConfiguration {
@Bean
open fun transactionBFPP(): BeanFactoryPostProcessor {
return BeanFactoryPostProcessor {
it.registerScope("transaction", SimpleTransactionScope())
}
}
@Bean
@Scope("transaction")
open fun nowInstant(): Instant = Instant.now()
@Bean
@Scope("transaction")
open fun nowOffsetDateTime(nowInstant: Instant): OffsetDateTime = nowInstant.atOffset(ZoneOffset.UTC)
@Bean
open fun transactionDateTimeProvider(factory: ObjectFactory<OffsetDateTime>): DateTimeProvider =
DateTimeProvider {
Optional.of(factory.`object`)
}
}
import com.capitalone.e1.domain.core.exception.ExceptionAggregate
import com.capitalone.e1.domain.core.exception.ExceptionRepository
import com.capitalone.e1.infrastructure.tx.TransactionScopeTimeConfiguration
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.tuple
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.ObjectFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.ImportAutoConfiguration
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.context.annotation.Import
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Sort
import java.time.OffsetDateTime
@DataJpaTest
@Import(TransactionScopeTimeConfiguration::class)
internal open class ExceptionRepositoryTest(private val repo: ExceptionRepository) {
@Test
fun save( @Autowired timestamp: ObjectFactory<OffsetDateTime>) {
val toCreate = ExceptionAggregate("someid")
val saved = repo.save(toCreate)
assertThat(saved).isInstanceOf(ExceptionAggregate::class.java)
.extracting { it.id }
.isEqualTo(toCreate.id)
val found = repo.findAll()
val now = timestamp.`object`
assertThat(found)
.extracting({ it.id }, { it.businessDivisionId }, { it.createdOn}, {it.lastModifiedOn})
.containsExactly(tuple(toCreate.id, "someid", now, now))
.doesNotContainNull()
}
@Test
fun graphPage() {
val (a,b,c,d,e) = repo.saveAll(listOf(
ExceptionAggregate("a"),
ExceptionAggregate("b"),
ExceptionAggregate("c"),
ExceptionAggregate("d"),
ExceptionAggregate("e") ,
)).toList()
val page = repo.findAllByIdBetween(a.id, b.id, PageRequest.of(0, 2, Sort.by("lastModifiedOn")))
assertThat(page.content).isNotEmpty
}
}
package com.capitalone.e1.util.jpa
import com.github.f4b6a3.uuid.UuidCreator
import org.apache.commons.lang3.builder.ToStringBuilder
import org.apache.commons.lang3.builder.ToStringStyle
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedDate
import java.time.OffsetDateTime
import java.util.Objects
import java.util.UUID
import javax.persistence.Access
import javax.persistence.AccessType
import javax.persistence.Column
import javax.persistence.Id
import javax.persistence.MappedSuperclass
import javax.persistence.Version
@MappedSuperclass
abstract class AbstractBaseEntity {
// time ordered needed for fast database indexing on insert matters
// by the time you hit a million records because of how btree's work
// https://www.2ndquadrant.com/en/blog/on-the-impact-of-full-page-writes/
// this can be avoided by using sequential uuid's
//
// the ID here must be property accessed, this is because lazy loaded lists
// can access just the id without loading the whole object.
// https://stackoverflow.com/a/39960438/206466
@get:Id
@get:Access(AccessType.PROPERTY)
@get:Column(columnDefinition = "uuid", nullable = false, updatable = false, unique = true)
open var id: UUID = UuidCreator.getTimeOrderedEpochPlus1()
protected set
@Version
protected open var version: Int = 0
@CreatedDate
@Column(name = "created_on", columnDefinition = "timestamp with time zone", nullable = false, updatable = false)
open var createdOn: OffsetDateTime? = null
protected set
@LastModifiedDate
@Column(name = "last_modified_on", columnDefinition = "timestamp with time zone", nullable = false)
open var lastModifiedOn: OffsetDateTime? = null
protected set
/**
* checks that other is the same instance of this
*/
abstract fun canEqual(other: AbstractBaseEntity): Boolean
override fun equals(other: Any?): Boolean {
if (this === other) return true
// can equal called from other for reflexivity
if (other is AbstractBaseEntity && other.canEqual(this) && this.id == other.id) return true
return false
}
override fun hashCode(): Int = Objects.hash(this.id)
override fun toString(): String {
return ToStringBuilder.reflectionToString(
this,
ToStringStyle.MULTI_LINE_STYLE
)
}
}
adding @Transactional
to the test show's that the fetch of the offset inside the test is working
import com.capitalone.e1.domain.core.exception.ExceptionAggregate
import com.capitalone.e1.domain.core.exception.ExceptionRepository
import com.capitalone.e1.infrastructure.tx.TransactionScopeTimeConfiguration
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.tuple
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.ObjectFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.ImportAutoConfiguration
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock
import org.springframework.context.annotation.Import
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Sort
import org.springframework.test.context.TestPropertySource
import org.springframework.transaction.annotation.Transactional
import java.time.OffsetDateTime
@DataJpaTest
@Transactional
@Import(TransactionScopeTimeConfiguration::class)
internal open class ExceptionRepositoryTest(private val repo: ExceptionRepository) {
@Test
fun save( @Autowired timestamp: ObjectFactory<OffsetDateTime>) {
val toCreate = ExceptionAggregate("someid")
val saved = repo.save(toCreate)
assertThat(saved).isInstanceOf(ExceptionAggregate::class.java)
.extracting { it.id }
.isEqualTo(toCreate.id)
val found = repo.findAll()
val now = timestamp.`object`
assertThat(found)
.extracting({ it.id }, { it.businessDivisionId }, { it.createdOn}, {it.lastModifiedOn})
.containsExactly(tuple(toCreate.id, "someid", now, now))
.doesNotContainNull()
}
@Test
fun graphPage() {
val (a,b,c,d,e) = repo.saveAll(listOf(
ExceptionAggregate("a"),
ExceptionAggregate("b"),
ExceptionAggregate("c"),
ExceptionAggregate("d"),
ExceptionAggregate("e") ,
)).toList()
val page = repo.findAllByIdBetween(a.id, b.id, PageRequest.of(0, 2, Sort.by("lastModifiedOn")))
assertThat(page.content).isNotEmpty
}
}
Expecting actual:
[(0183ae38-8b57-7aec-9292-8e5401e71d6f, "someid", null, null)]
to contain exactly (and in same order):
[(0183ae38-8b57-7aec-9292-8e5401e71d6f, "someid", 2022-10-06T16:54:44.880885Z (java.time.OffsetDateTime), 2022-10-06T16:54:44.880885Z (java.time.OffsetDateTime))]
but some elements were not found:
[(0183ae38-8b57-7aec-9292-8e5401e71d6f, "someid", 2022-10-06T16:54:44.880885Z (java.time.OffsetDateTime), 2022-10-06T16:54:44.880885Z (java.time.OffsetDateTime))]
and others were not expected:
[(0183ae38-8b57-7aec-9292-8e5401e71d6f, "someid", null, null)]
DEBUG - create table exceptions (id uuid not null, created_on timestamp, last_modified_on timestamp, version integer not null, business_division_id varchar(255), primary key (id)) : org.hibernate.SQL
Sorry that you're getting all of my debugging. Here's what's going to be the last though. As it works well enough for me to leave the company with. However, seems like there's an issue. The DateTimeProvider
is never invoked
INFO - Starting ExceptionRepositoryTest using Java 17.0.4 on 5c52309d33e3 with PID 86723 (started by nqy642 in /Users/nqy642/IdeaProjects/E1-Shared-Kernel/module/domain-model-exceptions-one) : com.capitalone.e1.domain.core.ExceptionRepositoryTest
DEBUG - Running with Spring Boot v2.7.4, Spring v5.3.23 : com.capitalone.e1.domain.core.ExceptionRepositoryTest
INFO - The following 3 profiles are active: "test", "test-feign", "dev" : com.capitalone.e1.domain.core.ExceptionRepositoryTest
INFO - Bootstrapping Spring Data JPA repositories in DEFAULT mode. : org.springframework.data.repository.config.RepositoryConfigurationDelegate
INFO - Finished Spring Data repository scanning in 491 ms. Found 1 JPA repository interfaces. : org.springframework.data.repository.config.RepositoryConfigurationDelegate
INFO - Replacing 'dataSource' DataSource bean with embedded version : org.springframework.boot.test.autoconfigure.jdbc.TestDatabaseAutoConfiguration$EmbeddedDataSourceBeanFactoryPostProcessor
INFO - @Bean method TransactionScopeTimeConfiguration.transactionBFPP is non-static and returns an object assignable to Spring's BeanFactoryPostProcessor interface. This will result in a failure to process annotations such as @Autowired, @Resource and @PostConstruct within the method's declaring @Configuration class. Add the 'static' modifier to this method to avoid these container lifecycle issues; see @Bean javadoc for complete details. : org.springframework.context.annotation.ConfigurationClassEnhancer
INFO - Starting embedded database: url='jdbc:h2:mem:254465cc-1b66-4f8a-806f-3b06145f1051;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false', username='sa' : org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseFactory
INFO - HHH000204: Processing PersistenceUnitInfo [name: default] : org.hibernate.jpa.internal.util.LogHelper
INFO - HHH000412: Hibernate ORM core version 5.6.11.Final : org.hibernate.Version
INFO - HCANN000001: Hibernate Commons Annotations {5.1.2.Final} : org.hibernate.annotations.common.Version
INFO - HHH000400: Using dialect: org.hibernate.dialect.H2Dialect : org.hibernate.dialect.Dialect
DEBUG - drop table if exists exceptions CASCADE : org.hibernate.SQL
Hibernate: drop table if exists exceptions CASCADE
DEBUG - create table exceptions (id uuid not null, created_on timestamp, last_modified_on timestamp, version integer not null, business_division_id varchar(255), primary key (id)) : org.hibernate.SQL
Hibernate: create table exceptions (id uuid not null, created_on timestamp, last_modified_on timestamp, version integer not null, business_division_id varchar(255), primary key (id))
INFO - HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform] : org.hibernate.engine.transaction.jta.platform.internal.JtaPlatformInitiator
INFO - Initialized JPA EntityManagerFactory for persistence unit 'default' : org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean
INFO - Started ExceptionRepositoryTest in 6.499 seconds (JVM running for 9.899) : com.capitalone.e1.domain.core.ExceptionRepositoryTest
DEBUG - select exceptiona0_.id as id1_0_0_, exceptiona0_.created_on as created_2_0_0_, exceptiona0_.last_modified_on as last_mod3_0_0_, exceptiona0_.version as version4_0_0_, exceptiona0_.business_division_id as business5_0_0_ from exceptions exceptiona0_ where exceptiona0_.id=? : org.hibernate.SQL
Hibernate: select exceptiona0_.id as id1_0_0_, exceptiona0_.created_on as created_2_0_0_, exceptiona0_.last_modified_on as last_mod3_0_0_, exceptiona0_.version as version4_0_0_, exceptiona0_.business_division_id as business5_0_0_ from exceptions exceptiona0_ where exceptiona0_.id=?
TRACE - binding parameter [1] as [BINARY] - [0183af40-1e61-7fe7-985b-4678a0831bbd] : org.hibernate.type.descriptor.sql.BasicBinder
DEBUG - insert into exceptions (created_on, last_modified_on, version, business_division_id, id) values (?, ?, ?, ?, ?) : org.hibernate.SQL
Hibernate: insert into exceptions (created_on, last_modified_on, version, business_division_id, id) values (?, ?, ?, ?, ?)
TRACE - binding parameter [1] as [TIMESTAMP] - [2022-10-06T21:42:38.546686Z] : org.hibernate.type.descriptor.sql.BasicBinder
TRACE - binding parameter [2] as [TIMESTAMP] - [2022-10-06T21:42:38.546686Z] : org.hibernate.type.descriptor.sql.BasicBinder
TRACE - binding parameter [3] as [INTEGER] - [0] : org.hibernate.type.descriptor.sql.BasicBinder
TRACE - binding parameter [4] as [VARCHAR] - [someid] : org.hibernate.type.descriptor.sql.BasicBinder
TRACE - binding parameter [5] as [BINARY] - [0183af40-1e61-7fe7-985b-4678a0831bbd] : org.hibernate.type.descriptor.sql.BasicBinder
DEBUG - select exceptiona0_.id as id1_0_, exceptiona0_.created_on as created_2_0_, exceptiona0_.last_modified_on as last_mod3_0_, exceptiona0_.version as version4_0_, exceptiona0_.business_division_id as business5_0_ from exceptions exceptiona0_ : org.hibernate.SQL
Hibernate: select exceptiona0_.id as id1_0_, exceptiona0_.created_on as created_2_0_, exceptiona0_.last_modified_on as last_mod3_0_, exceptiona0_.version as version4_0_, exceptiona0_.business_division_id as business5_0_ from exceptions exceptiona0_
TRACE - extracted value ([id1_0_] : [BINARY]) - [0183af40-1e61-7fe7-985b-4678a0831bbd] : org.hibernate.type.descriptor.sql.BasicExtractor
org.opentest4j.AssertionFailedError:
expected: 798976000
but was: 546686000
Expected :798976000
Actual :546686000
package com.capitalone.e1.infrastructure.tx
import org.apache.logging.log4j.kotlin.logger
import org.springframework.beans.factory.ObjectFactory
import org.springframework.beans.factory.config.BeanFactoryPostProcessor
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Scope
import org.springframework.data.auditing.DateTimeProvider
import org.springframework.data.jpa.repository.config.EnableJpaAuditing
import org.springframework.transaction.support.SimpleTransactionScope
import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.util.Optional
const val TRANSACTION = "transaction"
@Configuration
@EnableJpaAuditing
open class TransactionScopeTimeConfiguration {
private val log = logger()
@Bean
open fun transactionBFPP(): BeanFactoryPostProcessor {
return BeanFactoryPostProcessor {
it.registerScope(TRANSACTION, SimpleTransactionScope())
}
}
@Bean
@Scope(TRANSACTION)
open fun nowInstant(): Instant = Instant.now()
@Bean
@Scope(TRANSACTION)
open fun nowOffsetDateTime(nowInstant: Instant): OffsetDateTime = nowInstant.atOffset(ZoneOffset.UTC)
@Bean
open fun transactionDateTimeProvider(factory: ObjectFactory<Instant>): DateTimeProvider =
DateTimeProvider {
val now = factory.`object`
log.error { "transactional now: $now" }
Optional.of(now)
}
}
package com.capitalone.e1.util.jpa
import com.github.f4b6a3.uuid.UuidCreator
import org.apache.commons.lang3.builder.ToStringBuilder
import org.apache.commons.lang3.builder.ToStringStyle
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.jpa.domain.support.AuditingEntityListener
import java.time.Instant
import java.util.Objects
import java.util.UUID
import javax.persistence.Access
import javax.persistence.AccessType
import javax.persistence.Column
import javax.persistence.EntityListeners
import javax.persistence.Id
import javax.persistence.MappedSuperclass
import javax.persistence.Version
@MappedSuperclass
@EntityListeners(AuditingEntityListener::class)
abstract class AbstractBaseEntity {
// time ordered needed for fast database indexing on insert matters
// by the time you hit a million records because of how btree's work
// https://www.2ndquadrant.com/en/blog/on-the-impact-of-full-page-writes/
// this can be avoided by using sequential uuid's
//
// the ID here must be property accessed, this is because lazy loaded lists
// can access just the id without loading the whole object.
// https://stackoverflow.com/a/39960438/206466
@get:Id
@get:Access(AccessType.PROPERTY)
@get:Column(columnDefinition = "uuid", nullable = false, updatable = false, unique = true)
open var id: UUID = UuidCreator.getTimeOrderedEpochPlus1()
protected set
@Version
protected open var version: Int = 0
@CreatedDate
@Column(name = "created_on", columnDefinition = "timestamp with time zone", nullable = false, updatable = false)
open var createdOn: Instant? = null
protected set
@LastModifiedDate
@Column(name = "last_modified_on", columnDefinition = "timestamp with time zone", nullable = false)
open var lastModifiedOn: Instant? = null
protected set
/**
* checks that other is the same instance of this
*/
abstract fun canEqual(other: AbstractBaseEntity): Boolean
override fun equals(other: Any?): Boolean {
if (this === other) return true
// can equal called from other for reflexivity
if (other is AbstractBaseEntity && other.canEqual(this) && this.id == other.id) return true
return false
}
override fun hashCode(): Int = Objects.hash(this.id)
override fun toString(): String {
return ToStringBuilder.reflectionToString(
this,
ToStringStyle.MULTI_LINE_STYLE
)
}
}
package com.capitalone.e1.domain.core
import com.capitalone.e1.domain.core.exception.ExceptionAggregate
import com.capitalone.e1.domain.core.exception.ExceptionAggregate_.LAST_MODIFIED_ON
import com.capitalone.e1.domain.core.exception.ExceptionRepository
import com.capitalone.e1.infrastructure.tx.TransactionScopeTimeConfiguration
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.tuple
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.ObjectFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.context.annotation.Import
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Sort
import org.springframework.transaction.annotation.Transactional
import java.time.Instant
@DataJpaTest
@Transactional
@Import(TransactionScopeTimeConfiguration::class)
internal open class ExceptionRepositoryTest(private val repo: ExceptionRepository) {
@Test
fun save(@Autowired timestamp: ObjectFactory<Instant>) {
val toCreate = ExceptionAggregate("someid")
val saved = repo.save(toCreate)
assertThat(saved).isInstanceOf(ExceptionAggregate::class.java)
.extracting { it.id }
.isEqualTo(toCreate.id)
val found = repo.findAll()
val now = timestamp.`object`
assertThat(found)
.extracting({ it.id }, { it.businessDivisionId })
.containsExactly(tuple(toCreate.id, "someid"))
.doesNotContainNull()
assertThat(found.first())
.extracting { it.createdOn?.nano }
.isEqualTo( now.nano )
}
as you can see the nano's are different, these should actually be the same instance. You'll note in the logs that the DateTimeProvider is never invoked.
Hello,
I am facing the same issue here, and I will try to expose this more shortly because it is a bit confusing for me to follow all the comments from the author of this issue.
I have a simple table, with just three columns:
create table customer_seller (
customer_id bigint,
seller_id bigint,
created_at timestamp,
primary key (customer_id, seller_id)
);
Here is the entity class related to this table:
@Entity
@Data
@Table(name = "customer_seller")
public class FavoriteSeller implements Serializable {
private static final long serialVersionUID = 1L;
@EmbeddedId
private FavoriteSellerId favoriteSellerId;
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private Date createdAt;
}
And here is the class to represent the composite primary key:
@Embeddable
@Data
public class FavoriteSellerId implements Serializable {
@Column(name = "customer_id")
private Long customerId;
@Column(name = "seller_id")
private Long sellerId;
}
I am getting an error when trying to save a record into this table (using the following code):
FavoriteSellerId favoriteSellerId = new FavoriteSellerId();
favoriteSellerId.setCustomerId(customerId);
favoriteSellerId.setSellerId(sellerId);
FavoriteSeller favoriteSeller = new FavoriteSeller();
favoriteSeller.setFavoriteSellerId(favoriteSellerId);
favoriteSellerRepository.save(favoriteSeller);
Here is the error:
Caused by: org.hibernate.PropertyValueException: not-null property references a null or transient value : xxx.xxx.xxx.xxx.FavoriteSeller.createdAt
I have been using the @EmbeddedId
and the @CreatedDate
annotations in many other cases separately, but when I put both in the same class, the error starts appearing. That's why I suppose a problem exists when using both in the same entity.
I still need a minimal reproducer, to clear up the confusion.
If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.
Closing due to lack of requested feedback. If you would like us to look at this issue, please provide the requested information and we will re-open the issue.