spring-projects / spring-data-mongodb

Provides support to increase developer productivity in Java when using MongoDB. Uses familiar Spring concepts such as a template classes for core API usage and lightweight repository style data access.
https://spring.io/projects/spring-data-mongodb/
Apache License 2.0
1.62k stars 1.09k forks source link

Auditing metadata does not work on embedded object #4198

Closed bwgjoseph closed 2 years ago

bwgjoseph commented 2 years ago

Based on the docs for auditing, it states that

Auditing metadata does not necessarily need to live in the root level entity but can be added to an embedded one (depending on the actual store in use)

I tested it out, but it doesn't seem to work as described. Otherwise, it could be my interpretation is wrong of how it should work


So I have this simple configuration setup to test

@SpringBootApplication
@EnableMongoAuditing
public class SpringMongoAuditingApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringMongoAuditingApplication.class, args);
    }

}

@Component
public class CustomAuditor implements AuditorAware<String> {

    @Override
    public Optional<String> getCurrentAuditor() {
        return Optional.of("Joseph");
    }

}

@Repository
public interface CustomerRepository extends MongoRepository<Customer, String> {}

And I tested with both type; root based, and embedded based

Root based

Given this entity

@ToString
@NoArgsConstructor
@Builder
@Document("customer")
public class Customer {
    @Id
    private String id;
    private String name;
    @CreatedBy
    private String createdBy;
    private LocalDateTime createdDateTime;
}

If I run a test with the following

@SpringBootTest
class CustomerRepositoryTests {
    @Autowired
    private CustomerRepository personRepository;

    @Test
    void test() {
        Customer p = Customer.builder().name("Shane").build();
        Customer saved = personRepository.insert(p);
    }
}

The output of saved is

Customer(id=63441b90c1bc61294d876dfb, name=Shane, createdBy=Joseph, createdDateTime=null)

So this works as expected, but if I were to switch to use the embedded one, then createdBy wouldn't be populated at all

Embedded based

For this, I made some small adjustment to the class

@ToString
@NoArgsConstructor
@SuperBuilder
@Document("customer")
public class Customer {
    @Id
    private String id;
    private String name;
    private AuditMetadata auditMetadata;
}

@Builder
@ToString
public class AuditMetadata {
    @CreatedBy
    private String createdBy;
    private LocalDateTime createdDateTime;
}

With the same test, the output is

Customer(id=63441d808e052e5fffc2352f, name=Shane, auditMetadata=null)

So I tried out something which is to set the timestamp myself, just to initialize the class, and to see if that will work, and it did. I tweaked the test a little to

@Test
void test() {
    AuditMetadata auditMetadata = AuditMetadata.builder().createdDateTime(LocalDateTime.now()).build();
    Customer p = Customer.builder().name("Shane").auditMetadata(auditMetadata).build();
    Customer saved = personRepository.insert(p);
}

To ensure that the object is not null to begin with. And the output was

Customer(id=63441ec51e4f867c7f9d19b0, name=Shane, auditMetadata=AuditMetadata(createdBy=Joseph, createdDateTime=2022-10-10T21:31:49.162247500))

Notice that the name is now filled up automatically. I just had to ensure the object is not null for Spring to fill up the @CreatedBy field.


So my question is, is this behavior expected? If it does, then I think the documentations may have to be more explicit, if it's not, then what I do wrong? And documentations may have to be more explicit as well

Thanks!

mp911de commented 2 years ago

Yes, it is expected that you have an instance of the subdocument. Spring Data cannot fill in auditing values into a property that is null. We also cannot reason whether to create or not create object instances because the data is yours, not ours.

bwgjoseph commented 2 years ago

In that case, if I define the document as

@Builder
@ToString
public class AuditMetadata {
    @CreatedBy
    private String createdBy;
    @CreatedDate
    private LocalDateTime createdDateTime;
}

Which I expect Spring to help filled up all the fields. In that case, I have to create an empty object for this to work?

@Test
void test() {
    Customer p = Customer.builder().name("Shane").auditMetadata(AuditMetadata.builder().build()).build();
    Customer saved = personRepository.insert(p);
}

That seem to be too verbose.

Is there any way to do this to tell Spring?

public class Customer {
    @Id
    private String id;
    private String name;
    @EmbeddedAuditMetadata
    private AuditMetadata auditMetadata;
}

And it can init the class, and set the values accordingly?

Thanks

mp911de commented 2 years ago

In that case, I have to create an empty object for this to work?

Yes, you need to create that object. We cannot reason about whether to create a subdocument or not because presence of subdocuments is subject to your application.