Search code examples
jpaspring-bootspring-data-jpaauditing

Spring Data JPA auditing fails when persisting detached entity


I've setup JPA auditing with Spring Data JPA AuditingEntityListener and AuditorAware bean. What I want is to be able to persist auditor details even on entities with predefined identifiers. The problem is that when JPA entity with predefined id is being persisted and flushed it's auditor details cannot be persisted:

object references an unsaved transient instance - save the transient instance before flushing: me.auditing.dao.AuditorDetails

The interesting part is that when an entity with a generated id is saved - everything's fine. In both cases the entities are new. I could not pinpoint the problem digging through hibernate code so I've created a sample project to demonstrate this (test class me.auditing.dao.AuditedEntityIntegrationTest) It has both entities with predefined and generated identifiers and should be audited.

The entities are:

@Entity
public class AuditedEntityWithPredefinedId extends AuditableEntity {

    @Id
    private String id;

    public String getId() {
        return id;
    }

    public AuditedEntityWithPredefinedId setId(String id) {
        this.id = id;
        return this;
    }
}

and:

@Entity
public class AuditedEntityWithGeneratedId extends AuditableEntity {

    @Id
    @GeneratedValue(generator = "uuid")
    @GenericGenerator(name = "uuid", strategy = "uuid")
    private String id;

    public String getId() {
        return id;
    }

    public AuditedEntityWithGeneratedId setId(String id) {
        this.id = id;
        return this;
    }
}

where parent class is:

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AuditableEntity implements Serializable {

    private static final long serialVersionUID = -7541732975935355789L;

    @ManyToOne(fetch = FetchType.EAGER, cascade = {CascadeType.ALL})
    @CreatedBy
    private AuditorDetails createdBy;

    @CreatedDate
    private LocalDateTime createdDate;

    @ManyToOne(fetch = FetchType.EAGER, cascade = {CascadeType.ALL})
    @LastModifiedBy
    private AuditorDetails modifiedBy;

    @LastModifiedDate
    private LocalDateTime modifiedDate;

And the auditor getter implementation is:

@Override
public AuditorDetails getCurrentAuditor() {
    return new AuditorDetails()
            .setId(null)
            .setUserId("someUserId")
            .setDivisionId("someDivisionId");
}

Edit 2016-08-08: It seems that when a new entity with predefined id is saved, it gets two different instances of createdBy and modifiedBy AuditorDetails, which is quite logical if the entity wouldn't be actually new. So, a completely new entity with generated gets both AuditorDetails of same instance, and the one with manually set id doesn't. I tested it by saving auditor details in AuditorAware bean before returning it to AuditingHandler.


Solution

  • Ok, so for now the only solution I could find is to actually persist AuditorDetails before writing it to audited entities like so:

    @Override
    @Transactional
    public AuditorDetails getCurrentAuditor() {
        AuditorDetails details = new AuditorDetails()
                .setId(null)
                .setUserId("someUserId")
                .setDivisionId("someDivisionId");
        return auditorDetailsRepository.save(details);
    }
    

    It is not the most elegant solution, but it works for now.