repoistory.save() getting invoked with invalid entry when unit testing - java

I'm using java validation API to validate fields in my Note class:
#Entity
#Data
#NoArgsConstructor
#AllArgsConstructor
#Table(name = "note")
public class Note {
#Id
#Column(name = "id", nullable = false)
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "date", columnDefinition = "DATE")
private LocalDate date;
#NotBlank(message = "Enter a topic")
#Column(name = "topic")
private String topic;
#NotBlank(message = "Content can't be empty")
#Column(name = "content")
private String content;
#Column(name = "type")
private NoteType noteType;
#NotNull
#ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
#JoinColumn(name = "user_id")
#JsonIgnore
private User user;
}
NoteService:
#Service
#AllArgsConstructor
public class NoteService {
#Autowired
private NoteRepository noteRepository;
#Autowired
private UserRepository userRepository;
public void addNote(#Valid Note note) {
note.setUser(getLoggedInUser());
if (validateNote(note)) {
noteRepository.save(note);
}
}
public List<Note> getNotes() {
return getLoggedInUser().getNotes();
}
public Note editNote(Note newNote, Long id) {
noteRepository.editNoteById(newNote, id);
return newNote;
}
public List<Note> getNotesByTopic(String topic) {
List<Note> notes = noteRepository.getNotesByTopicAndUser(topic, getLoggedInUser());
return notes;
}
public boolean validateNote(Note note) {
return validateNoteType(note.getNoteType())
&& note.getDate() != null;
}
public boolean validateNoteType(NoteType type) {
return type.equals(NoteType.NOTE)
|| type.equals(NoteType.SKILL);
}
public User getLoggedInUser() {
return userRepository.findByEmail(SecurityContextHolder.getContext().getAuthentication().getName());
}
}
Test:
#ExtendWith(MockitoExtension.class)
#ExtendWith(SpringExtension.class)
class NoteServiceTest {
#Mock
private NoteRepository noteRepositoryMock;
#Mock
private UserRepository userRepositoryMock;
#Mock
SecurityContext mockSecurityContext;
#Mock
Authentication authentication;
private NoteService noteService;
#BeforeEach
void setUp() {
noteService = new NoteService(noteRepositoryMock, userRepositoryMock);
Mockito.when(mockSecurityContext.getAuthentication()).thenReturn(authentication);
SecurityContextHolder.setContext(mockSecurityContext);
}
#Test
void shouldAddNote() {
LocalDate date = LocalDate.now();
Note note = new Note(0L, date, "test", "", NoteType.NOTE, null);
noteService.addNote(note);
Mockito.verify(noteRepositoryMock).save(note);
}
}
The field user in the Note class is annotated with #NotNull and I'm passing a null user to this note but the note is still getting saved. Same thing when I pass an empty string. Any idea why that is happening? I'm new to unit testing

I'm new to unit testing - your perfectly valid question has nothing to do with unit testing.
#NotNull does nothing on it own. Its actually a contract stating the following:
A data member (or anything else annotated with #NotNull like local variables, and parameters) can't be should not be null.
For example, instead of this:
/**
* #param obj should not be null
*/
public void MyShinyMethod(Object obj)
{
// Some code goes here.
}
You can write this:
public void MyShinyMethod(#NotNull Object obj)
{
// Some code goes here.
}
P.S.
It is usually appropriate to use some kind of annotation processor at compile time, or something that processes it at runtime. But I don't really know much about annotation processing. But I am sure Google knows :-)

You need to activate validation on you service class with the #Validated annotation so the validation of parameters kicks in.
#Service
#AllArgsConstructor
#Validated
public class NoteService {
...
See Spring #Validated in service layer and Spring Boot: How to test a service in JUnit with #Validated annotation? for more details.
If for some reason you need to manually perform the validation you can always do something like this:
#Component
public class MyValidationImpl {
private final LocalValidatorFactoryBean validator;
public MyValidationImpl (LocalValidatorFactoryBean validator) {
this.validator = validator;
}
public void validate(Object o) {
Set<ConstraintViolation<Object>> set = validator.validate(o);
if (!set.isEmpty()) {
throw new IllegalArgumentException(
set.stream().map(x -> String.join(" ", x.getPropertyPath().toString(), x.getMessage())).collect(
Collectors.joining("\n\t")));
}
}
}

So your noteRepository is Mocked, so you it's not actually calling save on your repository.
Mockito.verify(noteRepositoryMock).save(note);
All you are verifying here is that a call to save is made, not that it was successful.

Related

When trying to use #RepositoryEventHandler annotatin I get 'Cannot resolve symbol 'RepositoryEventHandler' and the annotation is colored red

my problem is that when I'm trying to use #RepositoryEventHandeler annotation I get "Cannot resolve symbol 'RepositoryEventHandler'" information, as if Spring didn't recognize this annotation, I checked and it does not look like I need to add any dependencies for it to work. It's my first attempt at using it so maybe I got the whole idea behind it wrong. What am I doing wrong? Thanks in advance.
Configuration class where I create a bean from class annotated with #RepositoryEventHandler
#Configuration
public class ConfigurationBeans {
#Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public AbsenceRepositoryEventHandler absenceRepositoryEventHandler() {
return new AbsenceRepositoryEventHandler();
}
}
Repository
#Repository
public interface AbsenceRepository extends JpaRepository<Absence, Long> {
List<Absence> findAbsencesByBarberId(Long barberId);
List<Absence> findAbsencesByWorkDay_Id(Long workDayId);
}
Entity
#Getter
#Entity
#Table(name = "absences")
#Builder
#AllArgsConstructor
#NoArgsConstructor
public class Absence {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", nullable = false)
private Long id;
#ManyToOne
#NotNull
#JoinColumn(name = "barber_id")
private Barber barber;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "work_day_id")
private WorkDay workDay;
#NotNull
#Column(name = "absence_start")
private LocalTime absenceStart;
#NotNull
#Column(name = "absence_end")
private LocalTime absenceEnd;
}
Class annotated with #RepositoryEventHandler (this annotation is all red and gives Cannot resolve symbol 'RepositoryEventHandle info)
#RepositoryEventHandler(Absence.class)
public class AbsenceRepositoryEventHandler {
#HandleBeforeCreate
public void handleAbsenceBeforeCreate(Absence absence){
}
}
Controller class
#RestController
#AllArgsConstructor
#CrossOrigin(origins = "http://localhost:3000")
public class AbsenceController {
private final AbsenceServiceImpl absenceService;
private final AbsenceRepository absenceRepository;
#GetMapping("/absences")
public List<Absence> getAllAbsences() {
return absenceRepository.findAll();
}
#GetMapping("/absencesById")
public AbsenceDto getAbsencesById(#RequestParam Long id) {
return absenceService.getAbsenceById(id);
}
#GetMapping("/absencesByBarber")
public List<AbsenceDto> getAbsencesByBarber(#RequestParam Long id) {
return absenceService.getAbsenceByBarber(id);
}
#GetMapping("/absencesByWorkDay")
public List<AbsenceDto> getAbsencesByWorkDay(#RequestParam Long id) {
return absenceService.getAbsenceByWorkDay(id);
}
#PostMapping("/absence")
public AbsenceDto createAbsence(#RequestBody #Valid CreateAbsenceDto absenceDto) {
return absenceService.addAbsence(absenceDto);
}
#PutMapping("/update/absence/{id}")
public ResponseEntity<String> updateAbsence(#PathVariable("id") long id, #RequestBody #Valid AbsenceDto absence) {
absenceService.updateAbsence(id, absence);
return new ResponseEntity<>("Absence was updated.", HttpStatus.OK);
}
#DeleteMapping("/delete/absence/{id}")
public ResponseEntity<String> deleteAbsence(#PathVariable("id") long id) {
absenceService.removeAbsence(id);
return new ResponseEntity<>("Absence was deleted.", HttpStatus.OK);
}
}
Got it. Should've added dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
<version>3.0.1</version>
</dependency>
Now it works.

Facing exception HibernateException: identifier of an instance of <Class> was altered from <old> to <new> spring

I'm using spring with lombok and mapstruct and postgresSQL.
I'm facing an error on a udirectional #ManyToOne relationship on Parent / Child relationship, with my class Client
Here is my Client class :
#Getter
#Setter
#Table(name = "client")
public class Client extends AbstractEntity {
private String name;
#ManyToOne(cascade = CascadeType.ALL)
private Address address;
private boolean headOffice;
#ManyToOne(cascade= CascadeType.ALL)
#JoinColumn(name="client_parent_id")
public Client clientParent;
}
Here is my abstractEntity to generate Id and some data:
#MappedSuperclass
#Getter
#Setter
#RequiredArgsConstructor
public class AbstractEntity {
#Id
#GeneratedValue(generator = "UUID")
#GenericGenerator(
name = "UUID",
strategy = "org.hibernate.id.UUIDGenerator"
)
private String id;
#CreationTimestamp
private Timestamp createdDate;
#UpdateTimestamp
private Timestamp modifiedDate;
}
Here is my client Service :
#Service
public class ClientService {
private final ClientRepository clientRepository;
private final ClientMapper clientMapper;
ClientService(ClientRepository clientRepository, ClientMapper clientMapper) {
this.clientRepository = clientRepository;
this.clientMapper = clientMapper;
}
Client getClient(String id) throws FunctionalException {
return this.clientRepository.getById(id)
.orElseThrow(
() -> new FunctionalException("Client not found")
);
}
public ClientDto createOrUpdateClient(ClientDto clientDto, String id) throws FunctionalException {
Client client;
if (id == null) {
verifyInExistence(clientDto);
client = this.clientMapper.toEntity(clientDto);
} else {
client = this.getClient(id);
this.clientMapper.updateClientFromDto(clientDto, client);
}
verifyParent(client, clientDto.getClientParentId());
return this.clientMapper.toDto(this.clientRepository.save(client));
}
private void verifyParent(Client client, String parentId) {
if (parentId != null) {
client.setClientParent(this.getClient(parentId));
} else {
client.setClientParent(null);
}
}
private void verifyInExistence(ClientDto clientDto) throws FunctionalException {
clientRepository.findByName(clientDto.getName()).ifPresent(s -> {
throw new FunctionalException(String.format("Client '%s' already exist", clientDto.getName()));
});
}
}
And my Rest Controller :
#RestController
#RequestMapping(path = "/api/client")
public class ClientResource {
private final ClientService clientService;
ClientResource(ClientService clientService) {
this.clientService = clientService;
}
#PostMapping
ClientDto addClient(#RequestBody ClientDto clientDto) throws FunctionalException {
return this.clientService.createOrUpdateClient(clientDto, null);
}
#PutMapping(path = "/{id}")
ClientDto updateClient(#PathVariable String id, #RequestBody ClientDto clientDto) throws FunctionalException {
return this.clientService.createOrUpdateClient(clientDto, id);
}
}
When i'm posting a new client with a parent or without a parent it's okay, all goes right.
But when I try to update (by using put in clientResource) in order to remove relationship between a child and parent entity I have an Hibernate Exception like this :
HibernateException: identifier of an instance of xxxx.model.Client was altered from 7fa60bf2-e176-4b96-aae4-cbfa6461cb0e to null
I read a lot of post but i'm not in the case my Id are well generated i just don't understand why i can't set null to parent to define a chil without parent. I tried also to add childrens as OneToMany but didn't understand the need to do this.
Thanks a lot for all your reponse ! :) And sorry for the bad english.
The error message indicates that the id value of the entity Client is being changed to null. This is most likely happening in this line:
this.clientMapper.updateClientFromDto(clientDto, client);
When doing an update on an existing object, you should not change its id (see this). Either you need to make sure the clientDto does not have a null id value, or you need to exclude setting the id of client from the clientDto.
Another note: if you're using generated identifier values (#GeneratedValue(generator = "UUID")) then it may be a good idea to remove the setter on the id field, because the value will be generated automatically:
#Setter(AccessLevel.NONE)
private String id;

Trigger #OneToMany lazy fetch

I have an ExampleRequest entity that can optionally have one or more ExampleRequestYear. It's currently configured this way (unrelated fields and gettters/setters omitted for brevity, please let me know if you need anything else):
#Entity
#Table(name = "EXAMPLE_REQUEST")
#SequenceGenerator(name = "EXAMPLE_REQUEST_ID_SEQ", sequenceName = "EXAMPLE_REQUEST_ID_SEQ", allocationSize = 1)
#Cacheable(false)
public class ExampleRequest implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "EXAMPLE_REQUEST_ID_SEQ")
#Column(name="EXAMPLE_REQUEST_ID", nullable = false)
private Long exampleRequestId;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "exampleRequest")
private List<ExampleRequestYear> exampleRequestYearList;
public ExampleRequest() {
}
public List<ExampleRequestYear> getExampleRequestYearList() {
if(this.exampleRequestYearList == null){
this.exampleRequestYearList = new ArrayList<ExampleRequestYear>();
}
return this.exampleRequestYearList;
}
public void setExampleRequestYearList(List<ExampleRequestYear> exampleRequestYearList) {
this.exampleRequestYearList = exampleRequestYearList;
}
public ExampleRequestYear addExampleRequestYear(ExampleRequestYear exampleRequestYear) {
getExampleRequestYearList().add(exampleRequestYear);
exampleRequestYear.setExampleRequest(this);
return exampleRequestYear;
}
public ExampleRequestYear removeExampleRequestYear(ExampleRequestYear exampleRequestYear) {
getExampleRequestYearList().remove(exampleRequestYear);
exampleRequestYear.setExampleRequest(null);
return exampleRequestYear;
}
}
#Entity
#Table(name = "EXAMPLE_REQUEST_YEAR")
#IdClass(ExampleRequestYearPK.class)
public class ExampleRequestYear implements Serializable {
#Id
#Column(nullable = false)
private Integer year;
#Id
#ManyToOne
#JoinColumn(name = "EXAMPLE_REQUEST_ID", referencedColumnName = "EXAMPLE_REQUEST_ID")
private ExampleRequest exampleRequest;
public ExampleRequestYear() {
}
public void setExampleRequest(ExampleRequest exampleRequest) {
this.exampleRequest = exampleRequest;
}
public ExampleRequest getExampleRequest() {
return exampleRequest;
}
}
Part of the code was auto-generated by the IDE and I'm still wrapping my head around JPA so there're probably design mistakes all around.
My app works (apparently) when I create a new ExampleRequest:
ExampleRequest exampleRequest = new ExampleRequest();
ExampleRequestYear exampleRequestYear = new ExampleRequestYear(2020);
request.addExampleRequestYear(exampleRequestYear);
However, I can't figure out how to edit an existing ExampleRequest because I'm unsure on how I'm meant to retrieve the linked entities. According to articles I've read, lazy fetching should be automatic, yet when I try this:
ExampleRequest exampleRequest = employeeRequestsController.getExampleRequestById(123);
System.out.println(exampleRequest.getExampleRequestYearList().size());
... I get a null pointer exception upon .size() because the getter runs but neither initialises an empty list, nor retrieves items from DB:
public List<ExampleRequestYear> getExampleRequestYearList() {
if(this.exampleRequestYearList == null){
// Field is null and conditional is entered
this.exampleRequestYearList = new ArrayList<ExampleRequestYear>();
// After initialisation, field is still null!
}
return this.exampleRequestYearList;
}
Also, switch to FetchType.EAGER solves this particular problem entirely. What am I missing?
Further details regarding app design. The Resource classes that handle HTTP requests interact with a set of Controller classes like this:
#Stateless(name = "ISomeActionController", mappedName = "ISomeActionController")
public class SomeActionController implements ISomeActionController {
#EJB
private IFooDAO fooDao;
#EJB
private IBarDAO barDao;
#Override
public ExampleRequest getExampleRequestById(Long exampleRequestId) {
return fooDao.getEntityById(exampleRequestId);
}
}
It's in the DAO classes where EntityManager is injected an used:
#Local
public interface IGenericDAO<T> {
public T persistEntity(T o);
public T persistEntityCommit(T o);
public void removeEntity(T o);
public void removeEntity(long id);
public T mergeEntity(T o);
public List<T> getEntitiesFindAll();
public List<T> getEntitiesFindAllActive();
public T getEntityById(Object id);
}
public interface IFooDAO extends IGenericDAO<ExampleRequest> {
public void flushDAO();
public ExampleRequest getExampleRequestById(Long exampleRequestId);
}
#Stateless(name = "IFooDAO", mappedName = "IFooDAO")
public class FooDAO extends GenericDAO<ExampleRequest> implements IFooDAO {
public FooDAO() {
super(ExampleRequest.class);
}
#Override
public void flushDAO(){
em.flush();
}
#Override
public ExampleRequest getExampleRequestById(Long exampleRequestId){
String sql = "...";
Query query = em.createNativeQuery(sql, ExampleRequest.class);
//...
}
}

Calling methods in two different ReactiveMongoRepository's in a transaction using Spring Data MongoDB?

When using the reactive programming model with Spring Data MongoDB it's possible to execute transactions like this:
Mono<DeleteResult> result = template.inTransaction()
.execute(action -> action.remove(query(where("id").is("step-1")), Step.class));
But Spring Data MongoDB also has support for "reactive repositories", for example:
public interface PersonRepository extends ReactiveMongoRepository<Person, String>
Flux<Person> findByLocationNear(Point location, Distance distance);
}
and
public interface CarRepository extends ReactiveMongoRepository<Car, String>
Flux<Car> findByYear(int year);
}
My question is, given that you have ReactiveMongoRepository's, can you somehow leverage MongoDB transactions and e.g. insert both a Person and Car in the same transaction (using PersonRepository and CarRepository in the case)? If so, how do you do this?
I had also been trying hard to find solution for the Transactional support in Reactive style of Mongo DB & Spring Boot
But luckily I figured it myself. Though few of the things from google were also helpful but those were non reactive.
Important Note - For Spring boot 2.2.x it works well, but with spring boot 2.3.x it has some other issues, it has internal re-write & changes all together
You need to use ReactiveMongoTransactionManager along with ReactiveMongoDatabaseFactory, most of the details at the end, also sharing the code repo for the same
For getting the mongo db to support the Transactions we need to make sure that the DB should be running in replica mode.
Why we need that? Because you will get some error like this otherwise:-
Sessions are not supported by the MongoDB cluster to which this client is connected
The instructions for the same are below:-
run the docker-compose based mongo db server using docker-compose.yml as shared below:-
version: "3"
services:
mongo:
hostname: mongo
container_name: localmongo_docker
image: mongo
expose:
- 27017
ports:
- 27017:27017
restart: always
entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "rs0" ]
volumes:
- ./mongodata:/data/db # need to create a docker volume named as mongodata first
After the image comes up, execute the command(here localmongo_docker is the name of the container):-
docker exec -it localmongo_docker mongo
Copy and paste the command below and execute that
rs.initiate(
{
_id : 'rs0',
members: [
{ _id : 0, host : "mongo:27017" }
]
}
)
And then exit the execution by entering exit
Important - The code repo can be found here on my github - https://github.com/krnbr/mongo-spring-boot-template
Important notes for the code are as below:-
MongoConfiguration class in the config package is the important part to make the transactions working, link to the configuration class is here
Main part is the Bean
#Bean
ReactiveMongoTransactionManager transactionManager(ReactiveMongoDatabaseFactory dbFactory) {
return new ReactiveMongoTransactionManager(dbFactory);
}
For checking the working of the code's Transactional requirement you may go through the class UserService in service package here
Code shared in case the links do not work for someone:-
The Configuration and inside the Beans
#Configuration
public class MongoConfiguration extends AbstractMongoClientConfiguration {
#Autowired
private MongoProperties mongoProperties;
#Bean
ReactiveMongoTransactionManager transactionManager(ReactiveMongoDatabaseFactory dbFactory) {
return new ReactiveMongoTransactionManager(dbFactory);
}
#Override
protected String getDatabaseName() {
return mongoProperties.getDatabase();
}
#Override
public MongoClient mongoClient() {
return MongoClients.create(mongoProperties.getUri());
}
}
application.properties (related to mongo db)
spring.data.mongodb.database=mongo
spring.data.mongodb.uri=mongodb://localhost:27017/mongo?replicaSet=rs0
Document Classes
Role Class
#Getter
#Setter
#Accessors(chain = true)
#Document(collection = "roles")
#TypeAlias("role")
public class Role implements Persistable<String> {
#Id
private String id;
#Field("role_name")
#Indexed(unique = true)
private String role;
#CreatedDate
private ZonedDateTime created;
#LastModifiedDate
private ZonedDateTime updated;
private Boolean deleted;
private Boolean enabled;
#Override
#JsonIgnore
public boolean isNew() {
if(getCreated() == null)
return true;
else
return false;
}
}
User Class
#Getter
#Setter
#Accessors(chain = true)
#Document(collection = "users")
#JsonInclude(JsonInclude.Include.NON_NULL)
#TypeAlias("user")
public class User implements Persistable<String> {
#Id()
private String id;
#Field("username")
#Indexed(unique = true)
#JsonProperty("username")
private String userName;
#JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String password;
#CreatedDate
private ZonedDateTime created;
#LastModifiedDate
private ZonedDateTime updated;
private Boolean deleted;
private Boolean enabled;
#DBRef(lazy = true)
#JsonProperty("roles")
private List<Role> roles = new ArrayList();
#Override
#JsonIgnore
public boolean isNew() {
if(getCreated() == null)
return true;
else
return false;
}
}
UserProfile Class
#Getter
#Setter
#Accessors(chain = true)
#Document(collection = "user_profiles")
#JsonInclude(JsonInclude.Include.NON_NULL)
#TypeAlias("user_profile")
public class UserProfile implements Persistable<String> {
#Id
private String id;
#Indexed(unique = true)
private String mobile;
#Indexed(unique = true)
private String email;
private String address;
private String firstName;
private String lastName;
#DBRef
private User user;
#CreatedDate
private ZonedDateTime created;
#LastModifiedDate
private ZonedDateTime updated;
private Boolean deleted;
private Boolean enabled;
#Override
#JsonIgnore
public boolean isNew() {
if(getCreated() == null)
return true;
else
return false;
}
}
ReactiveMongoRepository Interface(s)
RoleRepository
public interface RoleRepository extends ReactiveMongoRepository<Role, String> {
Mono<Role> findByRole(String role);
Flux<Role> findAllByRoleIn(List<String> roles);
}
UserRepository
public interface UserRepository extends ReactiveMongoRepository<User, String> {
Mono<User> findByUserName(String userName);
}
UserProfileRepository
public interface UserProfileRepository extends ReactiveMongoRepository<UserProfile, String> {
}
The User Service Class Need to create your own RuntimeException Class here, here it is AppRuntimeException Class, I had been using
#Slf4j
#Service
public class UserService {
#Autowired
private RoleRepository roleRepository;
#Autowired
private UserRepository userRepository;
#Autowired
private UserProfileRepository userProfileRepository;
#Transactional
public Mono<UserProfile> saveUserAndItsProfile(final UserRequest userRequest) {
Mono<Role> roleMono = roleRepository.findByRole("USER");
Mono<User> userMono = roleMono.flatMap(r -> {
User user = new User()
.setUserName(userRequest.getUsername())
.setPassword(userRequest.getPassword());
user.setRoles(Arrays.asList(r));
return userRepository.save(user);
}).onErrorResume(ex -> {
log.error(ex.getMessage());
if(ex instanceof DuplicateKeyException) {
String errorMessage = "The user with the username '"+userRequest.getUsername()+"' already exists";
log.error(errorMessage);
return Mono.error(new AppRuntimeException(errorMessage, ErrorCodes.CONFLICT, ex));
}
return Mono.error(new AppRuntimeException(ex.getMessage(), ErrorCodes.INTERNAL_SERVER_ERROR, ex));
});
Mono<UserProfile> userProfileMono = userMono.flatMap(u -> {
UserProfile userProfile = new UserProfile()
.setAddress(userRequest.getAddress())
.setEmail(userRequest.getEmail())
.setMobile(userRequest.getMobile())
.setUser(u);
return userProfileRepository.save(userProfile);
}).onErrorResume(ex -> {
log.error(ex.getMessage());
if(ex instanceof DuplicateKeyException) {
String errorMessage = "The user with the profile mobile'"+userRequest.getMobile()+"' and/or - email '"+userRequest.getEmail()+"' already exists";
log.error(errorMessage);
return Mono.error(new AppRuntimeException(errorMessage, ErrorCodes.CONFLICT, ex));
}
return Mono.error(new AppRuntimeException(ex.getMessage(), ErrorCodes.INTERNAL_SERVER_ERROR, ex));
});
return userProfileMono;
}
}
Controller and the Model Class
UserRequest Model Class
#Getter
#Setter
#Accessors(chain = true)
#Slf4j
#JsonInclude(JsonInclude.Include.NON_NULL)
public class UserRequest {
private String username;
private String password;
private String mobile;
private String email;
private String address;
private String firstName;
private String lastName;
}
UserProfileApisController class
#Slf4j
#RestController
#RequestMapping("/apis/user/profile")
public class UserProfileApisController {
#Autowired
private UserService userService;
#PostMapping
public Mono<UserProfile> saveUserProfile(final #RequestBody UserRequest userRequest) {
return userService.saveUserAndItsProfile(userRequest);
}
}
Just an addition to the accepted answer regarding MongoDB replica set initialization.
If one needs a non-fixed port single replica set for testing they might use the Testcontainers’ MongoDB Module that encapsulates such initialization:
final MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.2.8");
We can start it via ‘mongoDBContainer.start()’ and stop it via try-with-resources or ‘mongoDBContainer.stop()’. See more details on this module and Spring Data MongoDB here.
If one needs a non-fixed port multi-node replica set for testing complex production issues, they might use this project, for example:
try (
//create a PSA mongoDbReplicaSet and auto-close it afterwards
final MongoDbReplicaSet mongoDbReplicaSet = MongoDbReplicaSet.builder()
//with 2 working nodes
.replicaSetNumber(2)
//with an arbiter node
.addArbiter(true)
//create a proxy for each node to simulate network partitioning
.addToxiproxy(true)
.build()
) {
//start it
mongoDbReplicaSet.start();
assertNotNull(mongoDbReplicaSet.getReplicaSetUrl());
//do some testing
}

Spring Async breaks JPA - detached entity passed to persist

Using H2 and JPA my REST app worked well before Ansyc, but after implementation breaks the JPA persistence model.
Here is the case:
My repository has a method JpaRepository.save() but when called from a separate thread, it throws InvalidDataAccessApiUsageException error.
My Controller calls the Service which calls the Repository to insert a new object, and I get the following error:
InvalidDataAccessApiUsageException: detached entity passed to persist: TransactionalEntity; nested exception is org.hibernate.PersistentObjectException: detached entity passed to persist: TransactionalEntity]
CONTROLLER:
#Autowired
#Qualifier("depositIntentService")
private TransactionIntentService depositIntentService;
#PostMapping("/services/transactions/deposit")
public CompletableFuture<ResponseEntity<TransactionIntent>> deposit(#Valid #RequestBody TransactionClientRequest request) {
CompletableFuture<TransactionIntent> depositIntentFuture =
transactionIntentFactory.createDepositIntent(
request.entity.id,
Money.of(CurrencyUnit.of(request.money.currency), request.money.amount));
return depositIntentFuture.thenApply(intent -> {
TransactionIntent publishedIntent = depositIntentService.attemptPublish(intent); //<-- causes error
ResponseEntity.ok(publishedIntent)
});
}
SERVICE:
#Component
#Repository
public abstract class TransactionIntentServiceImpl implements TransactionIntentService{
#Autowired
private TransactionIntentRepository transactionIntentRepo;
#Transactional
public TransactionIntent attemptPublish(TransactionIntent intent){
transactionIntentRepo.save(intent); //<-- Throws error: ...detached entity passed to persist
}
}
REPOSITORY
#Repository
public interface TransactionIntentRepository extends JpaRepository<TransactionIntent, Long>{
}
Any ideas how to support JPA persistance in an Async environment?
Thanks!
Update1
FACTORY
#Component
public class TransactionIntentFactory {
#Autowired
private UserService userService;
#Async("asyncExecutor")
public CompletableFuture<TransactionIntent> createDepositIntent(long beneficiaryId, Money money) {
CompletableFuture<User> bank = userService.findByUsername("bankItself#bank.com");
CompletableFuture<User> user = userService.find(beneficiaryId);
CompletableFuture<Void> allUserFutures = CompletableFuture.allOf(bank, user);
return allUserFutures.thenApply(it -> {
User userSource = bank.join();
User userBeneficiary = user.join();
TransactionIntent intent = new TransactionIntentBuilder()
.status(new TransactionIntentStatus(TRANSFER_STATUS.CREATED, "Deposit"))
.beneficiary(userBeneficiary)
.source(userSource)
.amount(money)
.build();
return intent;
});
}
}
ENTITY
#Entity
public class TransactionIntent {
#Id
#GeneratedValue
public long id;
public final Money amount;
public final Date createdAt;
#OneToOne(fetch = FetchType.EAGER, cascade=CascadeType.ALL)
public final TransactionIntentStatus status;
#OneToOne(fetch = FetchType.EAGER, cascade=CascadeType.ALL)
public final TransactionalEntity beneficiary; //to
#OneToOne(fetch = FetchType.EAGER, cascade=CascadeType.ALL)
public final TransactionalEntity source; //from
TransactionIntent(){
this.amount= null;
this.createdAt = null;
this.status = null;
this.beneficiary = null;
this.source = null;
}
public TransactionIntent(TransactionIntentBuilder builder) {
this.amount = builder.amount;
this.createdAt = new Date();
this.status = builder.status;
this.beneficiary = builder.beneficiary;
this.source = builder.source;
}
}

Categories