How can I design entity auditing in a Spring Boot application?
@Getter
@Setter
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Table(name = "user")
public class User implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;
@Column(name = "username", nullable = false)
private String username;
@Column(name = "password", nullable = false)
private String password;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private List<Role> roles = new ArrayList<>();
}
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AuditableEntity {
@CreatedDate
@Column(name = "created_on")
private LocalDateTime createdOn;
@CreatedBy
@Column(name = "created_by")
private User createdBy;
@LastModifiedDate
@Column(name = "updated_on")
private LocalDateTime updatedOn;
@LastModifiedBy
@Column(name = "updated_by")
private User updatedBy;
}
I want the following data to be stored in each entity that is subject to audit:
Is it possible to make the createdBy
and updatedBy
in AuditableEntity
have a one-to-many relationship with the User
entity, since one User
can create many records of different entities, but the entity can have only one creator? I'm not sure if this can be done in MappedSuperclass
, maybe it makes sense to just store the Id of user in AuditableEntity
?
In the getCurrentAuditor
implementation of AuditorAware
I would get the current principal
from Authentication
, find a User
entity from the database with the same id and return it:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
CustomUserDetails user = (CustomUserDetails) authentication.getPrincipal();
return userService.findUserById(user.getId());
@Data
public class CustomUserDetails implements UserDetails {
private Long id;
private String username;
private String password;
private Collection<? extends GrantedAuthority> authorities;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
private boolean enabled;
public CustomUserDetails(Long id, String username, String password, Collection<? extends GrantedAuthority> authorities) {
this.id = id;
this.username = username;
this.password = password;
this.authorities = authorities;
this.accountNonExpired = true;
this.accountNonLocked = true;
this.credentialsNonExpired = true;
this.enabled = true;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
public Long getId() {
return id;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return this.accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return this.accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return this.credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return this.enabled;
}
}
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Autowired
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
return new CustomUserDetails(
user.getId(),
user.getUsername(),
user.getPassword(),
user.getRoles().stream().map((role) -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList())
);
}
}
Then I would inherit my other entities from AuditableEntity
, for example:
@Getter
@Setter
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "patient")
public class Patient extends AuditableEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;
@Column(name = "name", nullable = false)
private String name;
@Column(name = "surname", nullable = false)
private String surname;
@Column(name = "middle_name", nullable = false)
private String middleName;
@Column(name = "address", nullable = false)
private String address;
@Column(name = "phone", nullable = false)
private String phone;
@Column(name = "messenger_contact", nullable = true)
private String messengerContact;
@Column(name = "birth_date", nullable = false)
private LocalDate birthDate;
@Column(name = "preferential_category", nullable = true)
private String preferentialCategory;
@OneToMany(mappedBy = "patient", orphanRemoval = true)
private List<Appointment> appointments = new ArrayList<>();
}
I understand that I will need to create fields
in each table of the database to be audited.
These are my thoughts, please advise me how I could have done it better and more correctly if possible
Your approach seem okay for implementing entity auditing in spring boot. However
Your AuditorAware implementation looks appropriate for fetching the current user. Ensure that it correctly handles scenarios where the user is not authenticated or the current user cannot be determined.
Using a MappedSuperclass for your AuditableEntity is a valid approach. However, you could also consider using an @Embeddable class that contains audit fields and embed it within your entities. This approach keeps your entity classes cleaner and allows for easier reuse if you have multiple auditable entities. Here is an example on how to implement it https://www.baeldung.com/spring-jpa-embedded-method-parameters#:~:text=We%20represent%20a%20composite%20primary,field%20of%20the%20%40Embeddable%20type.
it's not advisable to have a one-to-many relationship between AuditableEntity and User for the reasons you mentioned. If you must tie it to the user entitity then, Storing only the ID of the user in the AuditableEntity is a more appropriate approach.