spring-projects / spring-data-relational

Spring Data Relational. Home of Spring Data JDBC and Spring Data R2DBC.
https://spring.io/projects/spring-data-jdbc
Apache License 2.0
753 stars 345 forks source link

Using audit annotation in an embedded Java record fails with `Cannot set property` #1694

Closed jipipi closed 2 months ago

jipipi commented 9 months ago

If you declare audit annotation like @CreatedBy in an embedded java record then an exception is thrown : IllegalState Cannot set property createdUser because no setter, no wither and it's not part of the persistence constructor public springdata.jdbc.bug.UserAudit(java.lang.String,java.time.Instant,java.lang.String,java.time.LocalDateTime)

This issue also happens for other audit annotation @LastModifiedBy, @LastModifiedDate and @CreatedDate If the annotation are not in an embedded object it's work.

Exemple of code to reproduce issue

public record Car (
        @Id Long id,
        String name,
        @Embedded.Empty() 
        UserAudit userAudit
){}
public record UserAudit(@LastModifiedBy String modifiedUser,
                        @LastModifiedDate Instant modifiedDate,
                        @CreatedBy String createdUser,
                        @CreatedDate LocalDateTime createdDate) {}
CREATE TABLE IF NOT EXISTS Car (
    id INTEGER IDENTITY PRIMARY KEY,
     name VARCHAR(100),
     -- UserAudit attributes
    modified_user  varchar(255),
    modified_date  timestamp,
    created_user   varchar(255),
    created_date   timestamp
);

Here a project to reproduce the bug (the test failed) : https://github.com/jipipi/spring-data-jdbc-bug1

spring-data-jdbc : 3.2.0 java 17

schauder commented 8 months ago

Is this specific to the embeded entity being a record?

jipipi commented 8 months ago

Is this specific to the embeded entity being a record?

If i remember well, I reproduced the issue with classes instead of record

grzegorzt commented 8 months ago

The problem isn't specific to records, I've encountered it with regular java class - I've attached example project.

This is embedded class:

@Value
@Builder
@Immutable
public class Audit {
  private static final Audit EMPTY = builder().build();

  @CreatedDate
  Instant createdAt;
  @CreatedBy
  String createdBy;
  @LastModifiedDate
  Instant updatedAt;
  @LastModifiedBy
  String updatedBy;

  public static Audit empty() {
    return EMPTY;
  }
}

and entity class:

@Value
@Builder(toBuilder = true)
@Immutable
@Table
public class TestEntity {
  @Id
  String id;
  @Version
  int version;
  @NonNull
  String name;
  @NonNull
  @Embedded.Empty
  @Builder.Default
  Audit audit = Audit.empty();
}

When spring boot version 3.1.7 is used (you can easilty change it in build.gradle), everything works. In 3.2.1 the exception is thrown: java.lang.IllegalStateException: Cannot set property createdBy because no setter, no wither and it's not part of the persistence constructor ....

Full stacktrace:

java.lang.IllegalStateException: Cannot set property createdBy because no setter, no wither and it's not part of the persistence constructor com.bug.audit.model.Audit(java.time.Instant,java.lang.String,java.time.Instant,java.lang.String)
    at org.springframework.data.mapping.model.InstantiationAwarePropertyAccessor.setProperty(InstantiationAwarePropertyAccessor.java:93)
    at org.springframework.data.mapping.model.SimplePersistentPropertyPathAccessor.setValue(SimplePersistentPropertyPathAccessor.java:233)
    at org.springframework.data.mapping.model.SimplePersistentPropertyPathAccessor.setProperty(SimplePersistentPropertyPathAccessor.java:187)
    at org.springframework.data.auditing.MappingAuditableBeanWrapperFactory$MappingMetadataAuditableBeanWrapper.lambda$setProperty$0(MappingAuditableBeanWrapperFactory.java:232)
    at java.base/java.lang.Iterable.forEach(Iterable.java:75)
    at org.springframework.data.auditing.MappingAuditableBeanWrapperFactory$MappingMetadataAuditableBeanWrapper.setProperty(MappingAuditableBeanWrapperFactory.java:232)
    at org.springframework.data.auditing.MappingAuditableBeanWrapperFactory$MappingMetadataAuditableBeanWrapper.setCreatedBy(MappingAuditableBeanWrapperFactory.java:197)
    at org.springframework.data.auditing.AuditingHandlerSupport.touchAuditor(AuditingHandlerSupport.java:169)
    at org.springframework.data.auditing.AuditingHandlerSupport.lambda$touch$0(AuditingHandlerSupport.java:136)
    at java.base/java.util.Optional.map(Optional.java:260)
    at org.springframework.data.auditing.AuditingHandlerSupport.touch(AuditingHandlerSupport.java:134)
    at org.springframework.data.auditing.AuditingHandlerSupport.markCreated(AuditingHandlerSupport.java:114)
    at org.springframework.data.auditing.AuditingHandler.markCreated(AuditingHandler.java:86)
    at org.springframework.data.auditing.IsNewAwareAuditingHandler.markAudited(IsNewAwareAuditingHandler.java:78)
    at org.springframework.data.relational.auditing.RelationalAuditingCallback.onBeforeConvert(RelationalAuditingCallback.java:61)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at org.springframework.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:281)
    at org.springframework.data.mapping.callback.EntityCallbackDiscoverer.lambda$computeCallbackInvokerFunction$1(EntityCallbackDiscoverer.java:258)
    at org.springframework.data.mapping.callback.DefaultEntityCallbacks$SimpleEntityCallbackInvoker.invokeCallback(DefaultEntityCallbacks.java:106)
    at org.springframework.data.mapping.callback.DefaultEntityCallbacks.callback(DefaultEntityCallbacks.java:87)
    at org.springframework.data.jdbc.core.JdbcAggregateTemplate.triggerBeforeConvert(JdbcAggregateTemplate.java:622)
    at org.springframework.data.jdbc.core.JdbcAggregateTemplate.beforeExecute(JdbcAggregateTemplate.java:462)
    at org.springframework.data.jdbc.core.JdbcAggregateTemplate.performSave(JdbcAggregateTemplate.java:489)
    at org.springframework.data.jdbc.core.JdbcAggregateTemplate.save(JdbcAggregateTemplate.java:168)
    at org.springframework.data.jdbc.repository.support.SimpleJdbcRepository.save(SimpleJdbcRepository.java:68)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:352)
    at org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new$0(RepositoryMethodInvoker.java:277)
    at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:170)
    at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:158)
    at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:516)
    at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:285)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:628)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
    at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:168)
    at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:143)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
    at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:70)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:385)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
    at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:249)
    at jdk.proxy3/jdk.proxy3.$Proxy78.save(Unknown Source)
    at com.bug.audit.EmbeddedAuditTest.shouldPersistEntity(EmbeddedAuditTest.java:49)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:728)
    at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
    at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:218)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:214)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:139)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:69)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
    at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
    at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:119)
    at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:94)
    at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:89)
    at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:62)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
    at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
    at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
    at jdk.proxy1/jdk.proxy1.$Proxy2.stop(Unknown Source)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker$3.run(TestWorker.java:193)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60)
    at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
    at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:113)
    at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:65)
    at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
    at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)

bug.tar.gz

jipipi commented 7 months ago

If the embedded entity containing the audit annotation is a regular class with setter then it's work. I updated the test case : bug1.zip

FYI, on version 2.4.11, the record was supported.

@schauder When I debug it's failing on InstantiationAwarePropertyAccessor :

if (!creator.isCreatorParameter(property)) {
            throw new IllegalStateException(String.format(NO_CONSTRUCTOR_PARAMETER, property.getName(), creator));
}

because it's searching an EmbeddedRelationalPersistencePoperty and BasicJdbcPersistentProperty is provided: Screenshot 2024-02-08 at 17 53 27

dennishendriksen commented 6 months ago

Ran into the same issue as reported by @jipipi, but instead of getting an error, no error occurs and no auditing information if persisted to the database.

Works:

public record MyEntity(
    @Id Integer id,
    @CreatedBy AggregateReference<User, Integer> createdBy,
    @CreatedDate Instant creationDate,
    @LastModifiedBy AggregateReference<User, Integer> lastModifiedBy,
    @LastModifiedDate Instant lastModifiedDate) {}

Fails:

public record MyEntity(
    @Id Integer id,
    @Embedded.Empty AuditMetadata auditMetadata) {}

public record AuditMetadata(
    @CreatedBy AggregateReference<User, Integer> createdBy,
    @CreatedDate Instant creationDate,
    @LastModifiedBy AggregateReference<User, Integer> lastModifiedBy,
    @LastModifiedDate Instant lastModifiedDate){}

My use case is reusing the same embedded AuditMetadata record in multiple entities.

DreamStar92 commented 2 months ago

@schauder

The owner (RelationalPersistentEntity) returned by EmbeddedRelationalPersistentProperty is the owner of its delegate. The creator of the owner gets the creation value from its own properties. The property of RelationalPersistentEntity is BasicRelationalPersistentProperty, which means isCreatorParameter->doGetIsCreatorParameter->maps->property.equals(referencedProperty), property is EmbeddedRelationalPersistentProperty and referencedProperty is BasicRelationalPersistentProperty, causing false to be returned. image

image

schauder commented 2 months ago

Ran into the same issue [...], but instead of getting an error, no error occurs and no auditing information if persisted to the database.

This is the expected behaviour when the embedded entity is null. We are not instantiating such entities on the fly.