In Spring Boot I have an entity:
@Entity
@Table(name = "Product")
@EntityListeners(AuditingEntityListener.class)
public class Product extends BaseEntity {
...
@NotNull
@CreatedDate
private Date createdAt;
@NotNull
@LastModifiedDate
private Date updatedAt;
@NotNull
@CreatedBy
private String createdBy; // Employee's email
@NotNull
@LastModifiedBy
private String updatedBy; // Employee's email
@ManyToMany
@JoinTable(
name = "product_category",
joinColumns = @JoinColumn(name = "product_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "category_id", referencedColumnName = "id")
)
private Set<Category> categories = new HashSet<>();
...
public Date getCreatedAt() {
return createdAt;
}
public Date getUpdatedAt() {
return updatedAt;
}
public String getCreatedBy() {
return createdBy;
}
public String getUpdatedBy() {
return updatedBy;
}
...
}
and 2 configuration classes:
@Configuration
@EnableJpaAuditing
public class JpaConfiguration {
@Bean
public AuditorAware<String> employeeAuditorAware() {
return new EmployeeAuditorAware();
}
}
public class EmployeeAuditorAware implements AuditorAware<String> {
@Override
@NonNull
public Optional<String> getCurrentAuditor() {
return Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.map(Authentication::getName);
}
}
When I insert Product then createdAt, updatedAt, createdBy and updatedBy are initialized.
The problem occurs when I want to update Product. updatedAt is not updating. It holds old value (the same as createdAt). I don't know if updatedBy also works because for now I update it with the same user (Employee).
I tried to add a setter for updatedAt but it still doesn't work. I didn't use it manually. I added it because I thought maybe JPA needs it.
EDIT:
When I update a property which is not a relation, it works. The problem occurs when I want to update categories which is @ManyToMany relation.
This is how I save an entity:
@Service
public class ProductService {
private final CategoryRepository categoryRepository;
private final ProductRepository productRepository;
public ProductService(CategoryRepository categoryRepository, ProductRepository productRepository) {
this.categoryRepository = categoryRepository;
this.productRepository = productRepository;
}
private void insert(Product product, String name, String description, BigDecimal price, List<Long> categoryIds) {
product.setName(name);
product.setDescription(description);
product.setPrice(price);
product.getCategories().clear();
categoryIds.forEach(id -> {
Category category = categoryRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("Category with id " + id + " not found"));
category.addProduct(product);
product.addCategory(category);
});
productRepository.save(product);
}
public Page<Product> getAll(Pageable pageable) {
return productRepository.findAll(pageable);
}
@Transactional
public void save(String name, String description, BigDecimal price, List<Long> categoryIds) {
insert(new Product(), name, description, price, categoryIds);
}
@Transactional
public void update(Long id, String name, String description, BigDecimal price, List<Long> categoryIds) {
Product product = productRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("Product with id " + id + " not found"));
insert(product, name, description, price, categoryIds);
}
}
Spring Data JPAs Auditing feature is build on JPA lifecycle events. Since it works for simple properties, it seems save to assume it is setup correctly on your side.
Therefore, I assume your JPA implementation (Hibernate?) does not trigger events for the owning entity, when persisting the collection representing the m:n relationship.
IMHO this is a bug in the JPA implementation. But I guess, one can argue it is an ambiguity of the JPA specification. It says (among many similar lines):
The
PreUpdateandPostUpdatecallbacks occur before and after the database update operations to entity data respectively.
I couldn't find a proper definition of what is "entity data" exactly. If you look at it from the perspective of JPA metadata, a collection is it's own thing and might not be considered "entity data".
But if you look at it from the perspective of the mapping annotations one side of a n:m relationship "owns" that relationship, and I'd therefore argue it should be considered part of that entities data.
While this explains the behavior, it doesn't really do match in resolving it. Long term your best chance is to raise issues against the JPA specification in order to get clarification and also with your JPA implementation requesting a fix.
Short to mid term, the only fix I see is to update the auditing fields manually, or changing a dummy property, so the auditing listener gets triggered.