Skip to content

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

Closed
@jipipi

Description

@jipipi

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

Activity

changed the title [-]Using audit annotation in an embedded java record not wort[/-] [+]Using audit annotation in an embedded java record not work[/+] on Dec 12, 2023
schauder

schauder commented on Jan 3, 2024

@schauder
Contributor

Is this specific to the embeded entity being a record?

self-assigned this
on Jan 3, 2024
jipipi

jipipi commented on Jan 4, 2024

@jipipi
Author

Is this specific to the embeded entity being a record?

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

grzegorzt

grzegorzt commented on Jan 8, 2024

@grzegorzt

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

changed the title [-]Using audit annotation in an embedded java record not work[/-] [+]Using audit annotation in an embedded java object not work[/+] on Jan 27, 2024
jipipi

jipipi commented on Feb 8, 2024

@jipipi
Author

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

dennishendriksen commented on Mar 1, 2024

@dennishendriksen

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

DreamStar92 commented on Jun 26, 2024

@DreamStar92

@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

schauder commented on Jun 26, 2024

@schauder
Contributor

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.

changed the title [-]Using audit annotation in an embedded java object not work[/-] [+]Using audit annotation in an embedded Java record fails with `Cannot set property`[/+] on Jun 27, 2024
added 3 commits that reference this issue on Jun 27, 2024
5c76ddc
3e5ab63
6f74c0f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

    Development

    No branches or pull requests

      Participants

      @schauder@dennishendriksen@grzegorzt@jipipi@spring-projects-issues

      Issue actions

        Using audit annotation in an embedded Java record fails with `Cannot set property` · Issue #1694 · spring-projects/spring-data-relational