spring-data-jpa icon indicating copy to clipboard operation
spring-data-jpa copied to clipboard

@EmbeddedId breaking @CreatedDate

Open xenoterracide opened this issue 2 years ago • 21 comments

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

xenoterracide avatar Sep 14 '22 19:09 xenoterracide

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

schauder avatar Sep 15 '22 06:09 schauder

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.

xenoterracide avatar Sep 15 '22 06:09 xenoterracide

Ok, if you have complete reproducer, if possible without inheritance and Kotlin, I'll take a look.

schauder avatar Sep 15 '22 07:09 schauder

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());
  }
}

xenoterracide avatar Sep 15 '22 08:09 xenoterracide

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.

xenoterracide avatar Sep 15 '22 08:09 xenoterracide

iw-pm-backend.tar.gz

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.

xenoterracide avatar Sep 15 '22 08:09 xenoterracide

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

xenoterracide avatar Sep 15 '22 09:09 xenoterracide

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();
  }
}

xenoterracide avatar Sep 15 '22 09:09 xenoterracide

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;

xenoterracide avatar Sep 15 '22 09:09 xenoterracide

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

xenoterracide avatar Sep 15 '22 10:09 xenoterracide

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

xenoterracide avatar Sep 15 '22 10:09 xenoterracide

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.

schauder avatar Sep 15 '22 11:09 schauder

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.

xenoterracide avatar Sep 15 '22 14:09 xenoterracide

not to be a nag, but any chance you've had to take a look at that jar?

xenoterracide avatar Sep 16 '22 12:09 xenoterracide

  • tar

xenoterracide avatar Sep 16 '22 12:09 xenoterracide

@schauder sorry, I'm a jerk, I know, I'm departing this company soon though and would ideally like to have this wrapped up.

xenoterracide avatar Sep 22 '22 10:09 xenoterracide

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.

schauder avatar Oct 04 '22 13:10 schauder

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.

xenoterracide avatar Oct 04 '22 22:10 xenoterracide

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

xenoterracide avatar Oct 06 '22 16:10 xenoterracide

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

xenoterracide avatar Oct 06 '22 17:10 xenoterracide

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 

xenoterracide avatar Oct 06 '22 17:10 xenoterracide

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.

xenoterracide avatar Oct 06 '22 21:10 xenoterracide

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.

marcelom56 avatar Mar 09 '23 13:03 marcelom56

I still need a minimal reproducer, to clear up the confusion.

schauder avatar Mar 09 '23 13:03 schauder

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.

spring-projects-issues avatar Mar 16 '23 13:03 spring-projects-issues

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.

spring-projects-issues avatar Mar 23 '23 13:03 spring-projects-issues