Matthias Baumgart
Matthias Baumgart

Reputation: 965

How to use Mongo Auditing and a UUID as id with Spring Boot 2.2.x?

I would like to have Documents stored with an UUID id and createdAt / updatedAt fields. My solution was working with Spring Boot 2.1.x. After I upgraded from Spring Boot 2.1.11.RELEASE to 2.2.0.RELEASE my test for MongoAuditing failed with createdAt = null. What do I need to do to get the createdAt field filled again?

This is not just a testproblem. I ran the application and it has the same behaviour as my test. All auditing fields stay null.

I have a Configuration to enable MongoAuditing and UUID generation:

@Configuration
@EnableMongoAuditing
public class MongoConfiguration {
    @Bean
    public GenerateUUIDListener generateUUIDListener() {
        return new GenerateUUIDListener();
    }
}

The listner hooks into the onBeforeConvert - I guess thats where the trouble starts.

public class GenerateUUIDListener extends AbstractMongoEventListener<IdentifiableEntity> {
    @Override
    public void onBeforeConvert(BeforeConvertEvent<IdentifiableEntity> event) {
        IdentifiableEntity entity = event.getSource();
        if (entity.isNew()) {
            entity.setId(UUID.randomUUID());
        }
    }
}

The document itself (I dropped the getter and setters):

@Document
public class MyDocument extends InsertableEntity {
    private String name;
}


public abstract class InsertableEntity extends IdentifiableEntity {
    @CreatedDate
    @JsonIgnore
    private Instant createdAt;
}

public abstract class IdentifiableEntity implements Persistable<UUID> {
    @Id
    private UUID id;

    @JsonIgnore
    public boolean isNew() {
        return getId() == null;
    }
}

A complete minimal example can be find here (including a test) https://github.com/mab/auditable With 2.1.11.RELEASE the test succeeds with 2.2.0.RELEASE it fails.

Upvotes: 4

Views: 5267

Answers (2)

Matthias Baumgart
Matthias Baumgart

Reputation: 965

For me the best solution was to switch from event UUID generation to a callback based one. With the implementation of Ordered we can set the new callback to be executed after the AuditingEntityCallback.

public class IdEntityCallback implements BeforeConvertCallback<IdentifiableEntity>, Ordered {
    @Override
    public IdentifiableEntity onBeforeConvert(IdentifiableEntity entity, String collection) {
      if (entity.isNew()) {
        entity.setId(UUID.randomUUID());
      }
      return entity;
    }

    @Override
    public int getOrder() {
      return 101;
    }
}

I registered the callback with the MongoConfiguration. For a more general solution you might want to take a look at the registration of the AuditingEntityCallback with the `MongoAuditingBeanDefinitionParser.

@Configuration
@EnableMongoAuditing
public class MongoConfiguration {
  @Bean
  public IdEntityCallback registerCallback() {
    return new IdEntityCallback();
  }
}

Upvotes: 4

shahaf
shahaf

Reputation: 4983

MongoTemplate works in the following way on doInsert()

  • this.maybeEmitEvent - emit an event (onBeforeConvert, onBeforeSave and such) so any AbstractMappingEventListener can catch and act upon like you did with GenerateUUIDListener
  • this.maybeCallBeforeConvert - call before convert callbacks like mongo auditing

like you can see in source code of MongoTemplate.class src (831-832)

protected <T> T doInsert(String collectionName, T objectToSave, MongoWriter<T> writer) {
        BeforeConvertEvent<T> event = new BeforeConvertEvent(objectToSave, collectionName);
        T toConvert = ((BeforeConvertEvent)this.maybeEmitEvent(event)).getSource(); //emit event
        toConvert = this.maybeCallBeforeConvert(toConvert, collectionName); //call some before convert handlers
        ...
}

MongoAudit marks createdAt only to new entities by checking if entity.isNew() == true

because your code (UUID) already set the Id the createdAt is not populated (the entity is not considered new)

you can do the following (order by best to worst):

  • forget about the UUID and use String for your id, let the mongo itself create and manage it's entities ids (this how MongoTemplate actually works lines 811-812)
  • keep the UUID at the code level, convert from/to String when inserting and retrieving from the db
  • create a custom repository like in this post
  • stay with 2.1.11.RELEASE
  • set the updateAt by GenerateUUIDListener as well as id (rename it NewEntityListener or smth), basically implement the audit
  • implement a new isNew() logic that don't depends only on the entity id

in version 2.1.11.RELEASE the order of the methods was flipped (MongoTemplate.class 804-805) so your code worked fine

as an abstract approach, the nature of event is to be sort of send-and-forget (async compatible), so it's a very bad practice to change the object itself, there is NO grantee for order of computation, if any

this is why the audit build on callbacks and not events, and that's why Pivotal don't (need to) keep order between versions

Upvotes: 1

Related Questions