JpaRepository findBy boolean - spring-boot

I am implementing an API to return all flagged users (boolean true). I am novice on Spring Boot and I am wondering what would be the best way to implement that method on the UserDAO
Does that DAO method makes sense, I want to return a LIST a Users with FLAG set to true.
User
#Entity
#Table(name = "user", schema = "public")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "userid", nullable = false)
private Long userId;
#Column(name = "flag")
private Boolean flag;
public Boolean getFlag() {
return flag;
}
public void setFlag(Boolean flag) {
this.flag = flag;
}
DAO
#Repository
public interface UserDao extends JpaRepository<User, Long> {
List<User> findByRoleId(Integer roleId);
Service
public Execution<User> getFlaggedUsers() {
Execution<User> res;
try {
List<User> users = userDao.findUsersByFlag();
res = new Execution<>(ResultEnum.SUCCESS, users);
} catch (Exception e) {
res = new Execution<>(ResultEnum.INNER_ERROR);
}
return res;
}
Controller
#GetMapping("/flagged_users")
public Map<String, Object> getFlaggedUsers() throws InternalException {
Map<String, Object> resultMap = new HashMap<>();
try {
Execution<User> res = userService.getFlaggedUsers();
resultMap.put(Constants.USER.getStatusCode(), res.getObjects());
} catch (Exception e) {
throw new InternalException(e.getMessage());
}
return resultMap;
}

Your DAO method should be
List<User> findByFlag(boolean flag) where you can pass required flag.
Or
List<User> findByFlagTrue() which will returns all the users where flag = true.

Related

Failing to get entity id on ManyToMany relationship

I have two entities, Agency and Client mapped as below in a ManyToMany relationship. I am persisting the Client entity on the agencyrepository side and I cannot get the client id after persisting the client. Below are the entity definitions
#Entity
#AllArgsConstructor
#NoArgsConstructor
#Data
public class Client extends AbstractAuditingEntity{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToMany(mappedBy = "clients")
private Set<Agency> agencys = new HashSet<>();
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Client tag = (Client) o;
return Objects.equals(name, tag.name);
}
#Override
public int hashCode() {
return Objects.hash(name);
}
}
and Agency:
#Entity
#AllArgsConstructor
#NoArgsConstructor
#Data
public class Agency extends AbstractAuditingEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToMany(cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
})
#JoinTable(name = "agency_client",
joinColumns = #JoinColumn(name = "agency_id"),
inverseJoinColumns = #JoinColumn(name = "client_id")
)
private Set<Client> clients = new HashSet<>();
public void addClient(Client client) {
clients.add(client);
client.getAgencys().add(this);
}
public void removeClient(Client client) {
clients.remove(client);
client.getAgencys().remove(this);
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Agency)) return false;
return id != null && id.equals(((Agency) o).getId());
}
#Override
public int hashCode() {
return getClass().hashCode();
}
}
MEthod to create Client:
#Override
#Transactional
public Long save(ClientCreateDto clientCreateDto) {
try {
Client client = new Client();
client.setName(clientCreateDto.getName());
Address address = new Address();
address.setFirstLine(clientCreateDto.getAddress().getFirstLine());
address.setTown(clientCreateDto.getAddress().getTown());
address.setPostcode(clientCreateDto.getAddress().getPostcode());
client.setAddress(address);
client.setEmail(clientCreateDto.getEmail());
client.setBillingEmail(clientCreateDto.getBillingEmail());
client.setTelephone(clientCreateDto.getTelephone());
client.setLogo(clientCreateDto.getLogo());
client.setCreatedBy("System");
client.setStatus(Status.ACTIVE);
client.setService(service.getOne(clientCreateDto.getServiceId()));
Agency agency = agencyService.getOne(clientCreateDto.getAgencyId());
agency.addClient(client);
agencyRepository.saveAndFlush(agency);
log.info("################### Client {}", client.toString());
return client.getId();
} catch (Exception exception) {
throw new BusinessValidationException(exception.getMessage());
}
}
I am getting null, instead of the id allocated to the client.
You must persist the client object first. Do this,
#Override
#Transactional
public Long save(ClientCreateDto clientCreateDto) {
try {
Client client = new Client();
client.setName(clientCreateDto.getName());
Address address = new Address();
address.setFirstLine(clientCreateDto.getAddress().getFirstLine());
address.setTown(clientCreateDto.getAddress().getTown());
address.setPostcode(clientCreateDto.getAddress().getPostcode());
client.setAddress(address);
client.setEmail(clientCreateDto.getEmail());
client.setBillingEmail(clientCreateDto.getBillingEmail());
client.setTelephone(clientCreateDto.getTelephone());
client.setLogo(clientCreateDto.getLogo());
client.setCreatedBy("System");
client.setStatus(Status.ACTIVE);
client.setService(service.getOne(clientCreateDto.getServiceId()));
Agency agency = agencyService.getOne(clientCreateDto.getAgencyId());
//save client
Client savedClient = clientRepository.save(client);
agency.addClient(savedClient);
agencyRepository.saveAndFlush(agency);
log.info("################### Client {}", savedClient.toString());
return savedClient.getId();
} catch (Exception exception) {
throw new BusinessValidationException(exception.getMessage());
}
}

move validation to the JPQL query level

I am looking for a way to move the validation method from Service to Repository
One Picture has one PictureData.
This is the method:
// TODO move validation to the JPQL query level
.filter(pic -> pic.getPictureData().getFileName() != null)
This is my Service
#Service
#ConditionalOnProperty(name = "picture.storage.type", havingValue = "file")
public class PictureServiceFileImpl implements PictureService {
private static final Logger logger = LoggerFactory.getLogger(PictureServiceFileImpl.class);
#Value("${picture.storage.path}")
private String storagePath;
private final PictureRepository repository;
#Autowired
public PictureServiceFileImpl(PictureRepository repository) {
this.repository = repository;
}
#Override
public Optional<String> getPictureContentTypeById(long id) {
return repository.findById(id)
// TODO move validation to the JPQL query level
.filter(pic -> pic.getPictureData().getFileName() != null)
.map(Picture::getContentType);
}
#Override
public Optional<byte[]> getPictureDataById(long id) {
return repository.findById(id)
// TODO move validation to the JPQL query level
.filter(pic -> pic.getPictureData().getFileName() != null)
.map(pic -> Path.of(storagePath, pic.getPictureData().getFileName()))
.filter(Files::exists)
.map(path -> {
try {
return Files.readAllBytes(path);
} catch (IOException ex) {
logger.error("Can't open picture file", ex);
throw new RuntimeException(ex);
}
});
}
#Override
public PictureData createPictureData(byte[] picture) {
String fileName = UUID.randomUUID().toString();
try (OutputStream os = Files.newOutputStream(Path.of(storagePath, fileName))) {
os.write(picture);
} catch (IOException ex) {
logger.error("Can't create picture file", ex);
throw new RuntimeException(ex);
}
return new PictureData(fileName);
}
}
The Entities
#Entity
#Table(name = "pictures")
public class Picture {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#Column(name = "name", nullable = false)
private String name;
#Column(name = "content_type", nullable = false)
private String contentType;
#OneToOne(fetch = FetchType.LAZY, cascade= CascadeType.ALL, optional = false, orphanRemoval = true)
#JoinColumn(name="picture_data_id")
private PictureData pictureData;
#ManyToOne
private Product product;
public Picture() {
}
public Picture(String name, String contentType, PictureData pictureData, Product product) {
this.name = name;
this.contentType = contentType;
this.pictureData = pictureData;
this.product = product;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getContentType() {
return contentType;
}
public void setContentType(String contentType) {
this.contentType = contentType;
}
public PictureData getPictureData() {
return pictureData;
}
public void setPictureData(PictureData pictureData) {
this.pictureData = pictureData;
}
public Product getProduct() {
return product;
}
public void setProduct(Product product) {
this.product = product;
}
}
#Entity
#Table(name = "pictures_data")
public class PictureData {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#Lob
#Type(type="org.hibernate.type.BinaryType") // для правильной работы PostgreSQL
#Column(name = "data", length = 33554430) // для правильной hibernate-валидации в MySQL
private byte[] data;
#Column(name = "file_name")
private String fileName;
public PictureData() {
}
public PictureData(byte[] data) {
this.data = data;
}
public PictureData(String fileName) {
this.fileName = fileName;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public byte[] getData() {
return data;
}
public void setData(byte[] data) {
this.data = data;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
}
I am struggling to get a query working in JPQL.
public interface PictureRepository extends JpaRepository<Picture, Long> {
#Query ("SELECT p FROM Picture p JOIN p.pictureData d WHERE d.data IS NOT NULL ")
Picture filterPictureWherePictureDataIsNotNull ();
}
Since you already have entity level join, you can directly use below method
public interface PictureRepository extends JpaRepository<Picture, Long>
{
#Query ("SELECT p FROM Picture p WHERE p.pictureData.data IS NOT NULL ")
Picture filterPictureWherePictureDataIsNotNull ();
}
Another observation as well,
You repo method might return list of Picture and not a one picture.So, return type should ideally be
#Query ("SELECT p FROM Picture p WHERE p.pictureData.data IS NOT NULL ")
List<Picture> filterPictureWherePictureDataIsNotNull ();

Handling Authentication Failure with Springboot & Spring security

In a Rest appplication developped with Spring, I use POJO classes, DTO and entity for users management. Here is an abstract of my entity class.
#Entity
#Table(name="users")
#Getter #Setter
#AllArgsConstructor #NoArgsConstructor
public class UserEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false, unique = true)
private String userKeyId;
#Column(nullable = false, length = 50)
private String firstName;
#Column(nullable = false, length = 50)
private String lastName;
#Column(nullable = false, length = 120, unique = true)
private String email;
#Column(nullable = false)
private String encryptedPassword;
#Column
private String emailVerificationToken;
#Column(name = "email_verification_status", columnDefinition = "BOOLEAN NOT NULL DEFAULT FALSE")
private Boolean emailVerificationStatus = false;
#Column(name="is_account_non_expired")
private Boolean isAccountNonExpired;
#Column(name="is_account_non_locked")
private Boolean isAccountNonLocked;
#Column(name="is_credentials_non_expired")
private Boolean isCredentialsNonExpired;
#Column(name="is_enabled")
private Boolean isEnabled;
#Column(name="is_logged_in")
private Boolean isLoggedIn;
#ManyToMany(cascade= { CascadeType.PERSIST }, fetch = FetchType.EAGER )
#JoinTable(
name = "user_role",
joinColumns = #JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns=#JoinColumn(name = "role_id", referencedColumnName = "id"))
private Collection<RoleEntity> roles;
#CreationTimestamp
#Temporal(TemporalType.DATE)
#Column(name="created_at")
private Date createdAt;
#UpdateTimestamp
#Temporal(TemporalType.DATE)
#Column(name="updated_at")
private Date updatedAt;
}
I have a UserServiceImpl class that implements UserDetails
I do have then to implement loadUserByUsername
#Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
UserEntity userEntity = userRepository.findByEmail(email);
if( userEntity == null) {
throw new UsernameNotFoundException("User email is not in the database");
} else {
validateLoginAttempt(userEntity);
log.info("Returning User : " + userEntity.getFirstName() + " " + userEntity.getLastName());
userEntity.setLastLoginDateDisplay(userEntity.getLastLoginDate());
userEntity.setLastLoginDate(new Date());
userRepository.save(userEntity);
return new UserPrincipal(userEntity);
}
}
If user exists I call a method to validate authentication.
private void validateLoginAttempt(UserEntity user) {
if(user.getIsAccountNonLocked()) {
if(loginAttemptService.hasExceededMaxAttempts(user.getEmail())) {
user.setIsAccountNonLocked(Boolean.FALSE);
} else {
user.setIsAccountNonLocked(Boolean.TRUE);
}
} else {
loginAttemptService.evictUserFromLoginAttemptCache(user.getEmail());
}
}
This method allows me to check if the user account is locked or not and if user tried to connect too many times.
My LoginAttemptServiceImpl is the following:
#Service
public class LoginAttemptServiceImpl implements LoginAttemptService {
public static final int MAXIMUM_AUTH_ATTEMPT = 5;
public static final int AUTH_ATTEMPT_INCREMENT = 1;
private LoadingCache<String, Integer> loginAttemptCache;
private String username;
public LoginAttemptServiceImpl() {
super();
loginAttemptCache = CacheBuilder.newBuilder()
.expireAfterWrite(15, TimeUnit.MINUTES)
.maximumSize(10000)
.build(new CacheLoader<>() {
#Override
public Integer load(String key) {
return 0;
}
});
}
#Override
public void evictUserFromLoginAttemptCache(String username) {
loginAttemptCache.invalidate(username);
}
#Override
public void addUserToLoginAttemptCache(String username) {
int attempts = 0;
try {
attempts = AUTH_ATTEMPT_INCREMENT + loginAttemptCache.get(username);
loginAttemptCache.put(username, attempts);
} catch (ExecutionException e) {
e.printStackTrace();
}
}
#Override
public boolean hasExceededMaxAttempts(String username) {
try {
return loginAttemptCache.get(username) >= MAXIMUM_AUTH_ATTEMPT;
} catch (ExecutionException e) {
e.printStackTrace();
}
return false;
}
#Override
public int getLoginAttempts(String username) throws ExecutionException {
return loginAttemptCache.get(username);
}
}
I also implemented an event listener for authentication failure:
#Component
public class AuthenticationFailureListener {
private final LoginAttemptService loginAttemptService;
#Autowired
public AuthenticationFailureListener(LoginAttemptService loginAttemptService) {
this.loginAttemptService = loginAttemptService;
}
#EventListener
public void onAuthenticationFailure(AuthenticationFailureBadCredentialsEvent event) {
Object principal = event.getAuthentication().getPrincipal();
if (principal instanceof String) {
String username = (String) event.getAuthentication().getPrincipal();
loginAttemptService.addUserToLoginAttemptCache(username);
}
}
}
And finally my AuthenticationFilter allows me to manage successful and unsuccessful response:
#Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
String userName = ((UserPrincipal)authResult.getPrincipal()).getUsername();
// built the token
String token = Jwts.builder()
.setSubject(userName)
.setExpiration(new Date(System.currentTimeMillis() + SecurityConstants.EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SecurityConstants.getTokenSecret())
.compact();
UserService userService = (UserService) SpringApplicationContext.getBean("userServiceImpl");
UserDto userDto = userService.getUser(userName);
response.addHeader(SecurityConstants.HEADER_STRING_USERID, userDto.getUserKeyId());
response.addHeader(SecurityConstants.HEADER_STRING, SecurityConstants.TOKEN_PREFIX + token);
}
#SneakyThrows
#Override
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
// super.unsuccessfulAuthentication(request, response, failed);
int attempts;
if(loginAttemptService.hasExceededMaxAttempts(this.username)) {
attempts = loginAttemptService.getLoginAttempts(this.username);
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Attempt number " + attempts + ": Account is locked for 15 minutes");
} else {
attempts = loginAttemptService.getLoginAttempts(this.username);
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Attempt number " + attempts + ": " + (SecurityConstants.MAX_AUTH_ATTEMPTS - attempts) + " - before account is blocked");
}
}
Authentication works when it's successful... My issue concerns failure and i have 3 issues:
I would like to return an object in case of failure. the response.sendError should do the job but it doesn't. I also tried to return a Json response : https://www.baeldung.com/servlet-json-response
I use Guava cache but I also update database at the same time by setting isAccountNonLocked to false. I'd like to set the value to True once the cache is cleared.
I do not update the count of attempt in unsuccessfulAuthentication method. My response is always : Attempt number 0: 5 - before account is blocked
Thanks for help and for reading the whole text!
Regarding issue number 1, you can use a similar approach as the one mentioned in the link you posted, but use response.getWriter().write(String) and Jackson's ObjectMapper, like this:
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
ObjectMapper mapper = new ObjectMapper();
response.getWriter().write(mapper.writeValueAsString( /*Your custom POJO here */ ));
For issue 2: I found a trick that solves it. Instead of updating the database at the same time i clear the cache, I make the update at login validation...
private void validateLoginAttempt(UserEntity user) {
if(user.getIsAccountNonLocked()) {
if(loginAttemptService.hasExceededMaxAttempts(user.getEmail())) {
user.setIsAccountNonLocked(Boolean.FALSE);
} else {
user.setIsAccountNonLocked(Boolean.TRUE);
}
} else {
if(!loginAttemptService.hasExceededMaxAttempts(user.getEmail())) {
user.setIsAccountNonLocked(Boolean.TRUE);
}
loginAttemptService.evictUserFromLoginAttemptCache(user.getEmail());
}
}
For issue 3:
In my WebSecurity class which extends WebSecurityConfigurerAdapter, I implemented a bean in order to inject it in my AuthenticationFilter.
Here is my bean:
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
Here is my AuthenticationFilter class. I initially added this class as component (bad idea which generated error messages).
// #Component
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final LoginAttemptService loginAttemptService;
private String username;
public AuthenticationFilter(AuthenticationManager authenticationManager, LoginAttemptService loginAttemptService) {
this.authenticationManager = authenticationManager;
this.loginAttemptService = loginAttemptService;
}
....

Spring data jpa specification and pageable in #manytomany using join table repository

I have a use case to filter and paginate the record with #manytomany relation using a separate join table.
Below are the relation and entities
public class User {
private Long userId;
private String userName
#OneToMany(mappedBy = "user")
private List<UserRole> userRole;
}
public class Role {
private Long roleId;
private String roleName
#OneToMany(mappedBy = "role")
private List<UserRole> userRole;
}
public class UserRole{
private Long id;
private Integer active
#ManyToOne
#MapsId("userId")
private User user;
#ManyToOne
#MapsId("roleId")
private Role role;
}
#Repository
public interface UserRoleRepository extends
JpaRepository<UserRole, String>,
JpaSpecificationExecutor<UserRole> {
}
public class UserRoleSpecification implements Specification<UserRole>
{
private SearchCriteria criteria;
public RuleEntitySpecification(SearchCriteria criteria ) {
this.criteria = criteria;
}
#Override
public Predicate toPredicate(Root<UserRole> root,
CriteriaQuery<?> query,
CriteriaBuilder criteriaBuilder) {
if(criteria.getOperation().equalsIgnoreCase("eq")) {
if(root.get(criteria.getKey()).getJavaType() == String.class)
{
return criteriaBuilder.like(root.get(criteria.getKey()),
"%" + criteria.getValue() + "%");
} else {
return criteriaBuilder.equal(root.get(criteria.getKey()),
criteria.getValue());
}
}
return null;
}
}
public class SearchCriteria implements Serializable {
private String key;
private String operation;
private Object value;
}
UserRoleSpecificationBuilder specBuilder = new UserRoleSpecificationBuilder();
specBuilder.with("active", "eq" , 1); // giving us proper result
Specification<UserRole> spec = specBuilder.build();
Pageable paging = PageRequest.of(0, 5, Sort.by("user.userId"));
Page<UserRole> pagedResult = userRoleRepository.findAll(spec,paging);
But when we try to filter based on Rule/User table properties like userName/roleName specBuilder.with("user.userName", "eq" , "xyz");, I am getting following exception:
org.springframework.dao.InvalidDataAccessApiUsageException:
Unable to locate Attribute with the the given name
[user.userName] on this ManagedType
Kindly suggest if there is any way to achieve the filter using UserRole Join Table repository and specification
Pagination is also required hence using repository of Type UserRole JoinTable.
#Override
public Predicate toPredicate(Root<UserRole> root,
CriteriaQuery<?> query,
CriteriaBuilder criteriaBuilder) {
if (criteria.getOperation().equalsIgnoreCase("eq")) {
String key = criteria.getKey();
Path path;
if (key.contains(".")) {
String attributeName1 = key.split("\\.")[0];
String attributeName2 = key.split("\\.")[1];
path = root.get(attributeName1).get(attributeName2);
} else {
path = root.get(key);
}
if (path.getJavaType() == String.class) {
return criteriaBuilder.like(path, "%" + criteria.getValue() + "%");
} else {
return criteriaBuilder.equal(root.get(key), criteria.getValue());
}
}
return null;
}

Spring JPA Transaction ID

I have added an attribute to all my entities - transaction id - which is a sequence generated value that I bump up once in each transaction.
I also store the transaction id with user and start/end times so I have an audit trail for every change in the database.
What is the best way to handle storing a complete graph, where I basically only want to apply the transaction id to those entities that are actually dirty?
I can put a #PrePersist and #PreUpdate on the transaction id column, but how do I retrieve the value for the current transaction id? Is there a way to store and retrieve a value on the transaction object or other JPA controller? Do I need to use a ThreadLocal solution?
Ok, here is what I did. It seems to work in all of the use cases, though I have not done any performance testing, etc. If anyone sees anything that may be non-optimal or may fail in certain situations, please point it out.
Here is the base service class that all #Service implementations must extend:
public class BaseService
{
private final ActivityService activityService;
private final ApplicationEventPublisher applicationEventPublisher;
public static ThreadLocal<Activity> transaction = new ThreadLocal<>();
public BaseService(ActivityService activityService, ApplicationEventPublisher applicationEventPublisher)
{
this.activityService = activityService;
this.applicationEventPublisher = applicationEventPublisher;
}
Object executeWithinActivity(Updater updater)
{
boolean startedLocally = false;
try
{
if (transaction.get() == null)
{
startedLocally = true;
Activity activity = activityService.startTransaction();
transaction.set(activity);
}
return updater.execute(transaction.get());
}
finally
{
if (startedLocally)
{
applicationEventPublisher.publishEvent(new TransactionEvent());
Activity activity = transaction.get();
activityService.endTransaction(activity);
}
}
}
protected interface Updater
{
Object execute (Activity activity);
}
static class TransactionEvent
{
}
}
Activity is the entity that represents the stored transaction id:
#Entity
#Getter #Setter
#Table(name = "transactions", schema = "public", catalog = "euamdb")
public class Activity
{
#Id
#Column(name = "transaction_id", nullable = false)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "tx_generator")
#SequenceGenerator(name = "tx_generator", sequenceName = "transaction_seq", allocationSize = 1)
private long transactionId;
#Basic
#Column(name = "user_id", length = 24)
private String userId;
#Basic
#Column(name = "transaction_start")
#CreationTimestamp
private Date transactionStart;
#Basic
#Column(name = "transaction_end")
#UpdateTimestamp
private Date transactionEnd;
#Override
public boolean equals(Object o)
{
if (this == o) return true;
if (!(o instanceof Activity)) return false;
Activity that = (Activity) o;
return transactionId == that.transactionId;
}
#Override
public int hashCode()
{
return Long.hashCode(transactionId);
}
}
ActivityService (which does not extend BaseService):
#Service
public class ActivityService
{
private final ActivityRepository activityRepository;
private final AuthUserService authService;
#Autowired
public ActivityService(ActivityRepository activityRepository, AuthUserService authService)
{
this.activityRepository = activityRepository;
this.authService = authService;
}
#Transactional
public Activity startTransaction()
{
Activity activity = new Activity();
activity.setTransactionStart(new Date());
activity.setUserId(authService.getAuthenticatedUserId());
activityRepository.save(activity);
return activity;
}
#Transactional
public void endTransaction(Activity activity)
{
activity.setTransactionEnd(new Date());
activityRepository.save(activity);
}
}
The base entity class for all entities (excepting Activity):
#MappedSuperclass
#Getter #Setter
public class BaseEntity
{
#Basic
#Column(name = "transaction_id")
private Long transactionId;
#PrePersist
#PreUpdate
public void setupTransaction ()
{
ThreadLocal<Activity> transaction = BaseService.transaction;
Activity activity = transaction.get();
long transactionId = activity.getTransactionId();
setTransactionId(transactionId);
}
}
An example of a service:
#Service
public class OrganizationService extends BaseService
{
private final OrgUserRepository orgUserRepository;
private final UserService userService;
#Autowired
public OrganizationService(ActivityService activityService,
OrgUserRepository orgUserRepository,
UserService userService,
ApplicationEventPublisher applicationEventPublisher)
{
super(activityService, applicationEventPublisher);
this.orgUserRepository = orgUserRepository;
this.userService = userService;
}
#Transactional
public OrgUser save(User user, OrgUser orgUser)
{
return (OrgUser) executeWithinActivity(activity ->
{
orgUser.setUser(userService.save(user));
return orgUserRepository.save(orgUser);
});
}
}
UserService also will extend BaseService and the save(OrgUser) method will also executeWithinActivity.
Finally, the commit listener:
#Component
public class AfterCommitListener
{
#TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)
public void doAfterTxComplete(BaseService.TransactionEvent event)
{
BaseService.transaction.remove();
}
}

Resources