How to design "delete user flow" in monolith application with Spring Boot? - spring

I have a monolith spring boot application and I have a user for the program. I want to make an endpoint to delete user, but my problem is there are so many entities and Jpa repository interfaces related to the user. When I want to delete a user, I need to inject so many repository interface into the related deletion service like:
#Service
public class DeletionService {
private static final int S3_DELETION_KEY_LIMIT = 1000;
private final OfficeRepository officeRepository;
private final AdvertRepository advertRepository;
private final UserRepository userRepository;
private final AdvertSearchRepository advertSearchRepository;
private final AdvertPricesRepository advertPricesRepository;
private final AdvertFacilityRepository advertFacilityRepository;
private final AdvertVirtualPackagesRepository advertVirtualPackagesRepository;
private final BookingRepository bookingRepository;
private final AdvertMediasRepository advertMediasRepository;
private final MediaRepository mediaRepository;
private final AmazonS3Service amazonS3Service;
private final OfficeBuildingSecurityRepository officeBuildingSecurityRepository;
private final OfficeCyberSecurityRepository officeCyberSecurityRepository;
private final OfficeFacilityRepository officeFacilityRepository;
private final OfficeMediasRepository officeMediasRepository;
private final OfficeNotificationsRepository officeNotificationsRepository;
private final OfficePropertiesRepository officePropertiesRepository;
private final OfficeRoomsRepository officeRoomsRepository;
private final OfficeViewsRepository officeViewsRepository;
private final OwnerCompanyRepository ownerCompanyRepository;
private final PushNotificationTokenRepository pushNotificationTokenRepository;
... and so many other repositories and
//CONSTRUCTOR etc.
Everything in the program is related to the user and If I delete the user then everything goes off. I am not sure it is correct style or flow, how can I do this with a better approach if there is any other option in monolith app? Do I have to inject all related repository interfaces into the current service ?
Note: I am not using any queue service like Kafka, Sqs, RabbitMq etc.

That does not really matter what is you architecture, monolithic or microservices - patterns remain the same.
Basic idea: decompose that large delete operation into individual steps, for example:
public interface DeleteStep<E> {
void delete(E entity);
}
#Component
public class DeleteFiles implements DeleteStep<User> {
#Autowired
AmazonS3Service amazonS3Service
#Override
public void delete(User user) {
amazonS3Service.deleteUserFiles(user);
}
}
#Component
public class DeleteNotifications implements DeleteStep<User> {
#Autowired
OfficeNotificationsRepository officeNotificationsRepository;
#Override
public void delete(DmUser user) {
officeNotificationsRepository.deleteUserNotifications(user);
}
}
Now the body of our DeletionService would look like:
#Autowired
ObjectProvider<DeleteStep<User>> userDeletionSteps;
public void deleteUser(User user) {
userDeletionSteps.forEach(s -> s.delete(user));
}
Now we may actually improve that basic idea via taking advantage of application events, for example:
public class UserDeleteEvent {
private final User user;
public UserDeleteEvent(User user) {
this.user = user;
}
public User getUser() {
return user;
}
}
interface OfficeNotificationsRepository extends ... {
#EventListener(UserDeleteEvent.class)
default void onUserDeleted(UserDeleteEvent event) {
deleteUserNotifications(event.getUser());
}
}
class AmazonS3Service ... {
#EventListener(UserDeleteEvent.class)
void onUserDeleted(UserDeleteEvent event) {
deteteUserFiles(event.getUser());
}
}
and now our DeletionService turns into:
#Autowired
ApplicationEventPublisher eventPublisher;
public void deleteUser(User user) {
eventPublisher.publishEvent(new UserDeleteEvent(user));
}
Moreover, we may take advantage of #DomainEvents and get rid of DeletionService - UserRepository may dispatch application events.

Related

Quarkus: How to run StartupEvent only for non-PROD profiles?

#Singleton
public class Startup {
private static final String ADMIN_ROLES = String.join(",", Api.adminuser.role, Api.apiuser.role);
#Inject
UserRepo repo;
#Transactional
public void loadUsersForTest(#Observes StartupEvent evt) {//TODO Need to load only for non-PROD profiles
repo.deleteAll();// reset and load all test users
repo.addTestUserOnly(Api.adminuser.testuser, Api.adminuser.testpassword, ADMIN_ROLES);
repo.addTestUserOnly(Api.apiuser.testuser, Api.apiuser.testpassword, Api.apiuser.role);
}
}
There are a coupld of ways you can do this.
The first is to use io.quarkus.runtime.configuration.ProfileManage like so:
#Singleton
public class Startup {
private static final String ADMIN_ROLES = String.join(",", Api.adminuser.role, Api.apiuser.role);
#Inject
UserRepo repo;
#Transactional
public void loadUsersForTest(#Observes StartupEvent evt) {
if ("prod".equals(io.quarkus.runtime.configuration.ProfileManager.getActiveProfile())) {
return;
}
repo.deleteAll();// reset and load all test users
repo.addTestUserOnly(Api.adminuser.testuser, Api.adminuser.testpassword, ADMIN_ROLES);
repo.addTestUserOnly(Api.apiuser.testuser, Api.apiuser.testpassword, Api.apiuser.role);
}
}
The second is to use io.quarkus.arc.profile.UnlessBuildProfile like so:
#UnlessBuildProfile("prod")
#Singleton
public class Startup {
private static final String ADMIN_ROLES = String.join(",", Api.adminuser.role, Api.apiuser.role);
#Inject
UserRepo repo;
#Transactional
public void loadUsersForTest(#Observes StartupEvent evt) {
repo.deleteAll();// reset and load all test users
repo.addTestUserOnly(Api.adminuser.testuser, Api.adminuser.testpassword, ADMIN_ROLES);
repo.addTestUserOnly(Api.apiuser.testuser, Api.apiuser.testpassword, Api.apiuser.role);
}
}
The second way is better since it will result in Startup never becoming a CDI bean when you build the production application.

Object in bean constructor empty

I have exception-messages written down in the application.yml. They are pure text, which is later reformatted using java.text.MessageFormat.
I have got the following custom RuntimeException my service throws when login failed:
#Component
public class AccountLoginFailedException extends RuntimeException {
#Autowired
public AccountLoginFailedException(#Value("#(${authservice.exception-messages.login-failed})") final String message, #Qualifier(value = "Credentials") final Credentials credentials) {
super(MessageFormat.format(message, credentials.getUsername()));
}
}
My test, which solely tests the AccountController and mocks away the service behind it:
#RunWith(SpringRunner.class)
#ContextConfiguration(classes = AuthServiceTestConfiguration.class)
#WebMvcTest(AccountController.class)
public class AccountControllerTest {
#Autowired
private BeanFactory beanFactory;
#Autowired
private ObjectMapper objectMapper;
#Autowired
private TestHelper helper;
#MockBean
private AccountService accountService;
#Autowired
private JwtService jwtService;
#Autowired
private MockMvc mvc;
#Test
public void test_LoginFailed_AccountDoesNotExist() throws Exception {
// Given
final Credentials credentials = helper.testCredentials();
final String credentialsJson = objectMapper.writeValueAsString(credentials);
final AccountLoginFailedException loginFailedException = beanFactory.getBean(AccountLoginFailedException.class, credentials);
// When
given(accountService.login(credentials)).willThrow(loginFailedException);
// Then
mvc
.perform(
post("/login")
.accept(MediaType.APPLICATION_JSON_UTF8_VALUE)
.contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)
.content(credentialsJson))
.andExpect(status().isUnprocessableEntity())
.andExpect(jsonPath("$.data").value(equalTo(loginFailedException.getMessage())));
}
}
message contains the correct String. However: credentials contains just an empty object (not null) instead of the one created using helper.testCredentials().
Here is a slightly simplified TestHelper class I am using:
#TestComponent
public class TestHelper {
public static final String USERNAME = "SomeUsername";
public static final String PASSWORD = "SomePassword";
#Autowired
private BeanFactory beanFactory;
public Credentials testCredentials() {
final Credentials credentials = beanFactory.getBean(Credentials.class.getSimpleName(), Credentials.class);
credentials.setUsername(USERNAME);
credentials.setPassword(PASSWORD);
return credentials;
}
}
These custom exceptions are thrown by my application only and are always expected to contain the credentials (username) responsible for it. I also have a AccountExceptionsControllerAdvice-class, which just wraps these custom exceptions in a generic JSON response, exposing the error in a preferred manner.
How can I ensure that this particular instance of Credentials is inserted into the particular instance of AccountLoginFailedException? Or should I not be autowiring exceptions at all?
You could mock your Credentials component in your tests as follows:
#MockBean
private Credentials credentials;
#Before
public void before() {
when(credentials.getUsername()).thenReturn(USERNAME);
when(credentials.getPassword()).thenReturn(PASSWORD);
}

Spring MVC Repository Factory

Approach 1:
Below are the two service classes which are using same 2 repositories.
#org.springframework.stereotype.Service(value = "userService")
public class UserServiceImpl implements UserService {
#Autowired
private CounterRepository counterRepository;
#Autowired
private SessionRepository sessionRepository;
}
#org.springframework.stereotype.Service(value = "projectService")
public class UserServiceImpl implements UserService {
#Autowired
private CounterRepository counterRepository;
#Autowired
private SessionRepository sessionRepository;
}
So in above classes, as you see that CounterRepository & SessionRepository are using two times each in UserServiceImpl & ProjectServiceImpl services.
Is this is correct approach or I can make One Factory Class and use it to get required repo.
Approach 2:
class RepoFactory{
#Autowired
private CounterRepository counterRepository;
#Autowired
private SessionRepository sessionRepository;
public <T> T getRepo(Class<T> entityClass) {
if (entityClass == CounterRepository .class) {
return (T) appMessageRepository;
} else if (entityClass == SessionRepository.class) {
return (T) auditTrailRepository;
}
}
And I use like below
#org.springframework.stereotype.Service(value = "userService")
public class UserServiceImpl implements UserService {
#Autowired
private RepoFactory repoFactory;
public void someMethod(){
repoFactory.getRepo(CounterRepository.class);
.....
}
public void someMethod2(){
repoFactory.getRepo(SessionRepository.class);
.....
}
}
#org.springframework.stereotype.Service(value = "projectService")
public class ProjectServiceImpl implements ProjectService {
#Autowired
private RepoFactory repoFactory;
public void someMethod(){
repoFactory.getRepo(CounterRepository.class);
.....
}
public void someMethod2(){
repoFactory.getRepo(SessionRepository.class);
.....
}
}
Could you please help me out which approach is better according to performance and memory consumption.
If you don't configure it explictly, all spring beans are singleton, it will not affect memory at all! http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#beans-factory-scopes
The first approach is easy to go, recommended!!
The second approach is totally unnecessary, if you want to, you can always autowire applicationContext and use applicationContext.getBean(Mybean.class)
If you have to save some memory on application start, you can have a look at #Lazy, but from my point of view, it is also not necessary.

Static field injection in Spring Unit test

I am using JUnit4 in my application where I tried to test a UserService, the current test case is simple:
Create a user named 'admin' and save it to the database.
The other test case will rely on this user.
So I use the BeforeClass to insert the record, however I have to use the UserService to save the user, but spring does not support to inject static fields.
When I tried to create the UserService manually, I found that I have to fill the dependencies myself, I wonder if there is an alternative? Or there is something wrong with how I use JUnit?
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(locations = {"classpath:spring/application-config.xml"})
public class UserServiceTest {
#Rule
public final ExpectedException exception = ExpectedException.none();
#Autowired
private UserService userService;
//#Autowired // sprint does not suport this
private static UserService us;
private static User u;
#BeforeClass
public static void before() {
us = new UserServiceImpl(); // this UserService instance can not be used, since I have to fill the dependencies manually
us.clear();
u = new User();
u.setUsername("admin");
u.setPassword("pass");
us.save(u);
}
#AfterClass
public static void after() {
userService.delete(u.getId());
}
#Test
public void testQuery() {
List<User> list = userService.find();
assertEquals(1, list.size());
}
#Test
public void testChangePassword() {
userService.changePassword(u.getId(), u.getPassword(), "newpass");
assertEquals("newpass", userService.findOne(u.getId()).getPassword());
exception.expect(ResourceNotFoundException.class);
userService.changePassword("1", "32", "ss");
exception.expect(ResourceNotFoundException.class);
userService.changePassword(u.getId(), "32", "ss");
}
}

Why can't i create a neo4j relationship with spring data for neo?

i'm fairly new to spring data for neo (though i have experience with neo4j itself). i tried following the 'official' guide on spring data for neo, specifically the chapter on creating relationships.
But it seems i cannot get it to work. Spring is giving me an
java.lang.IllegalStateException: This index (Index[__rel_types__,Relationship]) has been marked as deleted in this transaction
Let me stress, that i am NOT removing any nodes or relationships. These are the relevant classes of my domain model:
#NodeEntity
public class User {
#GraphId
private Long nodeid;
#Indexed(unique = true)
private String uuid;
....
}
#NodeEntity
public class Website {
#GraphId
private Long nodeid;
#Indexed(unique = true)
private String uuid;
....
}
#RelationshipEntity(type = RelTypes.REL_USER_INTERESTED_IN)
public class UserInterest {
#GraphId
private Long nodeid;
#StartNode
private User user;
#EndNode
private Website site;
...
}
And this is my basic test which i can't get to turn green ..
(note that i omitted large portions of the code, the basic setup of the spring context etc. is working fine)
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration
#Transactional
public class BaseTest {
#Autowired
protected Neo4jTemplate template;
#Autowired
protected GraphDatabaseService graphDatabaseService;
protected Transaction tx;
#Configuration
#EnableNeo4jRepositories
static class TestConfig extends Neo4jConfiguration {
TestConfig() throws ClassNotFoundException {
setBasePackage("me.bcfh.model");
}
#Bean
GraphDatabaseService graphDatabaseService() {
return new TestGraphDatabaseFactory().newImpermanentDatabase();
}
}
public void before() {
// provide implementation if necessary
}
public void after() {
// provide implementation if necessary
}
#Before
public void setup() throws Exception {
Neo4jHelper.cleanDb(graphDatabaseService, false);
before();
}
#After
public void tearDown() throws Exception {
after();
if (tx != null) {
tx.success();
tx.close();
tx = null;
}
}
}
public class BasicGraphTest extends BaseTest {
User user;
Website website;
UserInterest interest;
#Override
public void before() {
user = new User();
website = new Website();
website = template.save(website);
user = template.save(user);
}
#Test
#Transactional
public void dbShouldContainData() throws Exception {
UserInterest interest = new UserInterest();
interest.setSite(website);
interest.setUser(user);
template.save(interest);
// some assertions
...
}
}
The IllegalStateException is being thrown when I try persisting the UserInterest instance, which I do not understand because I am not removing anything anywhere.
The ways to create a relationship mentioned in the spring guide did not work for me either, here I got the same exception ..
Can anyone spot what I'm doing wrong here?
I am using Spring Version 4.1.4.RELEASE and Spring Data For Neo Version 3.2.1.RELEASE. Neo4j has version 2.1.6
Note: I also tried copying the domain model classes from the cineasts example into my project and borrowed a few lines of the DomainTest class but this too gives me the IllegalStateException, maybe there is something wrong with my setup?
I think you are getting your IllegalStateException because you are calling cleanDb in your setup method.
You may not need to clean the database. Since your tests are makred #Transactional anything you do in your tests gets rolled back at the end of the test.
Looks like the transaction is trying to rollback and can't find the relationship it expects.

Resources