2

I have a Couchbase document for which I'd like to enable auditing:

@Document(expiry = 0, expiryUnit = TimeUnit.DAYS, touchOnRead = true)
public class Entity {
    @Id
    @GeneratedValue(strategy = GenerationStrategy.USE_ATTRIBUTES, delimiter = ":")
    private String id;

    @IdAttribute(order = 0)
    private String type;

    @IdAttribute(order = 1)
    private String entityGuid;

    @Version
    private long version;
    
    private String firstName;
    
    private String lastName;
    
    private LocalDate dateOfBirth;
    
    @CreatedDate
    private LocalDateTime createTimeStamp;
    
    @LastModifiedDate
    private LocalDateTime lastUpdateTimeStamp;
    
    @CreatedBy
    private String createdBy;
    
    @LastModifiedBy
    private String lastUpdatedBy;

    ...

My configuration:

@Data
@Configuration
@EnableCouchbaseAuditing
@EnableConfigurationProperties(CouchbaseProperties.class)
public class EntityCouchConfig extends AbstractCouchbaseConfiguration {

    ...

    @Bean
    public AuditorAware<String> couchAuditing() {
        return () -> Optional.of("my-entity-service");
    }
}

It was my expectation that when performing update operations through the Couchbase template like replaceById() and upsertById(), spring-data would preserve the document's @CreatedDate and @CreatedBy fields, only updating the @LastModifiedDate and @LastModifiedBy.

This, however, seems not to be the case. When I perform an update, the document's @Created fields are updated as well. This seems counterintuitive, and would suggest that I first need to pull the document, transfer the @Created fields, and then save that, explicitly making two calls.

I have read the spring-data-couchbase documentation on auditing but it's pretty sparse on the expected behavior here.

Is retrieving the document to capture the creation info and then updating the only way to do this, or am I implementing auditing wrong?

FerdTurgusen
  • 320
  • 5
  • 13

2 Answers2

0

So my question was based on a flawed premise, because performing Couchbase template operations like upsert() and replaceById() will never perform any auditing logic.

To get auditing to work, it must be used in conjunction with a Couchbase Repository's save() method.

The other important bit is that auditing won't do away with two calls to the database if your caller wants to update an entity and doesn't have the current version number and the current auditing data.

In order for spring-data auditing to work on an update operation, you must supply an entity with all audit fields (@CreatedDate/By, @LastModifiedDate/By) and the current version number. That means if your caller doesn't have this information, you must conduct a find call to retrieve it followed by a save once you have added that data to your entity. Only then will the auditing intelligently preserve the original @Createdxx fields while updating the @Lastxx fields.

FerdTurgusen
  • 320
  • 5
  • 13
0

"So my question was based on a flawed premise, because performing Couchbase template operations like upsert() and replaceById() will never perform any auditing logic.

To get auditing to work, it must be used in conjunction with a Couchbase Repository's save() method."

The repository.save() calls the template.save() which in turn calls one of insertById, replaceById or upsertById (depending on the existence of an @Version field, and if it is populated or not).

All the couchbase modification operations insert/upsert/replace are called in spring data couchbase by - ReplaceByIdOperationSupport.one(), InsertByIdOperationSupport().one, UpsertByIdOperationSupport.one() convert the entity with ReactiveCouchbaseTemplateSupport.encodeEntity() which does the audit in onBeforeConvert(). Set logging of org.springframework.data.couchbase.core.mapping.event=TRACE to see.

"if your caller wants to update an entity and doesn't have the current version number and the current auditing data."

All of insert/replace/upsert act on the whole document. So when you upsert, you overwrite everything, and if you don't already have the current auditing data, you overwrite them. If you don't have the current version number, but your entity has an @Version field, you can leave it null or 0, and repository.save() will call template.save() which will call template.upsertById.

The markedAudit() method is below. isNew() is true if the entity has no @Version or has a @Version field is null or zero.

  public Mono<Object> markAudited(Object object) {
    Assert.notNull(object, "Source object must not be null");
    if (!this.isAuditable(object)) {
      return Mono.just(object);
    } else {
      PersistentEntity<?, ? extends PersistentProperty<?>> entity = this.entities.getRequiredPersistentEntity(object.getClass());
      return entity.isNew(object) ? this.markCreated(object) : this.markModified(object);
    }
  }

"you must conduct a find call to retrieve it followed by a save once you have added that data to your entity. Only then will the auditing intelligently preserve the original @Createdxx fields while updating the @Lastxx fields."

So the final assessment is pretty much correct, although it should work equally well through repository.save(), template.insertById() and template.replaceById() and template.upsertById(). (but if you already have fetched the document, your entity it will have the @Version and there's no need to use upsert() instead of replace().

The only funky behavior is that if you have an entity without an @Version, that has an @Createdxxxx, it will always treat it as new - as it has no @Version to indicated otherwise.

Michael Reiche
  • 375
  • 1
  • 7