Using mockito to test bigger services in Spring - java

I started learning mockito to test my classes. I know how to do this with small classes with one, maybe 2 mocks, but I have a problem when my service is much bigger. For example, I have service
public class ShoppingListService {
Map<Ingredient, Long> shoppingList = new HashMap<>();
List<MealInfo> meals = new ArrayList<>();
UserInfoService userInfoService;
DietMealsService dietMealsService;
UserRepository userRepository;
User user;
#Autowired
public ShoppingListService(UserInfoService userInfoService, DietMealsService dietMealsService,UserRepository userRepository) {
this.userInfoService = userInfoService;
this.dietMealsService = dietMealsService;
this.userRepository = userRepository;
}
public Map<Ingredient,Long> createShoppingList(){
user = userRepository.findByLoginAndPassword(userInfoService.getUser().getLogin(),userInfoService.getUser().getPassword()).get();
shoppingList.clear();
meals.clear();
meals = user.getDiet().getMeals();
meals=dietMealsService.adjustIngredients(meals);
for (MealInfo meal : meals) {
meal.getMeal().getIngredients().forEach(s -> {
if(shoppingList.containsKey(s.getIngredient()))
shoppingList.put(s.getIngredient(), s.getWeight()+shoppingList.get(s.getIngredient()));
else
shoppingList.put(s.getIngredient(),s.getWeight());
});
}
return shoppingList;
}
}
and I want to test method createShoppingList.
Should I create few instances and mock every field except shoppingList and meals and then create 1 or 2 instances of ingredients, meals and after use when->then like this?
#Test
public void createShoppingList() {
//GIVEN
Ingredient pineapple = new Ingredient().builder().name("Pineapple").caloriesPer100g(54F).carbohydratePer100g(13.6F).fatPer100g(0.2F).proteinPer100g(0.8F).build();
Ingredient watermelon = new Ingredient().builder().name("Watermelon").caloriesPer100g(36F).carbohydratePer100g(8.4F).fatPer100g(0.1F).proteinPer100g(0.6F).build();
IngredientWeight pineappleWithWeight...
//after this create Meal, MealInfo, Diet...
}
Below other classes:
public class MealInfo implements Comparable<MealInfo>{
#Id
#GeneratedValue
private Long id;
private LocalDate date;
#ManyToOne(cascade = CascadeType.PERSIST)
#JoinColumn(name = "meal_id")
private Meal meal;
private String name;
#ManyToMany(cascade = CascadeType.REMOVE)
#JoinTable(name = "diet_meal_info", joinColumns = #JoinColumn(name = "meal_info_id"),
inverseJoinColumns = #JoinColumn(name = "diet_id"))
private List<Diet> diet;
public MealInfo(LocalDate date, String description, Meal meal) {
this.date = date;
this.name = description;
this.meal = meal;
}
#Override
public int compareTo(MealInfo o) {
return getName().compareTo(o.getName());
}
}
public class Meal {
#Id
#GeneratedValue
private Long id;
private String name;
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "meal_ingredient", joinColumns = #JoinColumn(name = "meal_id"),
inverseJoinColumns = #JoinColumn(name = "ingredient_id"))
private List<IngredientWeight> ingredients;
#Column(length = 1000)
private String description;
private String imageUrl;
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(name = "meal_category", joinColumns = #JoinColumn(name = "meal_id"),
inverseJoinColumns = #JoinColumn(name = "category_id"))
private Set<Category> category;
#OneToMany(mappedBy = "meal", cascade = CascadeType.ALL, orphanRemoval = true)
private List<MealInfo> mealInfo;
private Integer calories;
public Meal(MealForm mealForm) {
this.name = mealForm.getName();
this.description = mealForm.getDescription();
this.imageUrl = mealForm.getImageUrl();
this.category = mealForm.getCategory();
}
}
public class IngredientWeight {
#Id
#GeneratedValue
private Long id;
#ManyToOne
#JoinColumn(name = "ingredient_weight_id")
private Ingredient ingredient;
private Long weight;
#ManyToMany
#JoinTable(name = "meal_ingredient", joinColumns = #JoinColumn(name = "ingredient_id"),
inverseJoinColumns = #JoinColumn(name = "meal_id"))
private Set<Meal> meals;
}
public class Ingredient {
#Id
#GeneratedValue
private Long id;
private String name;
#Column(name = "calories")
private Float caloriesPer100g;
#Column(name = "proteins")
private Float proteinPer100g;
#Column(name = "carbohydrates")
private Float carbohydratePer100g;
#Column(name = "fat")
private Float fatPer100g;
#OneToMany(mappedBy = "ingredient", cascade = {CascadeType.DETACH, CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.MERGE},
fetch = FetchType.EAGER)
private List<IngredientWeight> ingredientWeights;
}
Could you write how to test this method or test implementation? Or maybe do you have any public repositories with tests bigger methods like this?

As mentioned, you probably don't want the fields user, shoppingList and meals in your service. These fields make the service unsafe to use in a multi-threaded environment, like a web app or web service (which can be accessed by multiple clients, so multiple threads, at the same time). For example the shoppingList you are working on might be cleared halfway through the process if another thread enters createShoppingList. Instead, make these fields local variables inside the createShoppingList method for now. If the logic becomes too complex and your service too large, you could extract it into a separate service or a helper class which is instantiated at the start of the method call and discarded at the end of it.
I always write unit tests as white-box tests for a single class. I try to cover every branch in the code if I can. You can check this by running the tests with coverage in IntelliJ. Note that black-box tests are also very useful, they focus on 'the contract' of a component. In my opinion unit tests are usually not suited for this, since the contract of a single class is normally not very interesting for the component's functionality as a whole and can easily change if the code is refactored. I write integration (or end-to-end) tests as black box tests. This requires setting up a stubbed application environment with for example an in-memory database and maybe some external services via WireMock. I you are interested in this, look into Google's contract testing or RestAssured framework.
Some remarks about your code:
public Map<Ingredient,Long> createShoppingList() {
// if any of the chained methods below return null, a NullPointerException occurs
// You could extract a method which takes the userInfoService user as an argument, see `findUser` below.
user = userRepository.findByLoginAndPassword(userInfoService.getUser().getLogin(),userInfoService.getUser().getPassword()).get();
// the above would then become:
User user = findUser(userInfoService.getUser()).orElseThrow(new ShoppingServiceException("User not found");
// instead of clearing these field, just initialize them as local variables:
shoppingList.clear();
meals.clear();
meals = user.getDiet().getMeals();
// I would change adjustIngredients so it doesn't return the meals but void
// it's expected that such a method modifies the meals without making a copy
meals = dietMealsService.adjustIngredients(meals);
// I would extract the below iteration into a separate method for clarity
for (MealInfo meal : meals) {
// I would also extract the processing of a single meal into a separate method
// the `meal.getIngredients` actually doesn't return Ingredients but IngredientWeights
// this is very confusing, I would rename the field to `ingredientWeights`
meal.getMeal().getIngredients().forEach(s -> {
// I would replace the four calls to s.getIngredient() with one call and a local variable
// and probably extract another method here
// You are using Ingredient as the key of a Map so you must implement
// `equals` and // `hashCode`. Otherwise you will be in for nasty
// surprises later when Java doesn't see your identical ingredients as
// equal. The simplest would be to use the database ID to determine equality.
if(shoppingList.containsKey(s.getIngredient()))
shoppingList.put(s.getIngredient(), s.getWeight()+shoppingList.get(s.getIngredient()));
else
shoppingList.put(s.getIngredient(),s.getWeight());
});
}
return shoppingList;
}
private Optional<User> findUser(my.service.User user) {
if (user != null) {
return userRepository.findByLoginAndPassword(user.getLogin(), user.getPassword());
}
else {
return Optional.empty();
}
}
private void processMeals(List<MealInfo> meals, Map<Ingredient, Long> shoppingList) {
for (MealInfo mealInfo : meals) {
processIngredientWeights(mealInfo.getMeal().getIngredients(), shoppingList);
}
}
private void processIngredientWeights(List<IngredientWeight> ingredientWeights, Map<Ingredient, Long> shoppingList) {
for (IngredientWeight ingredientWeight: ingredientWeights) {
processIngredientWeight(ingredientWeight, shoppingList);
}
}
private void processIngredientWeight(IngredientWeight ingredientWeight, Map<Ingredient, Long> shoppingList) {
Ingredient ingredient = ingredientWeight.getIngredient();
Long weight = shoppingList.getOrDefault(ingredient, 0L);
weight += ingredientWeight.getWeight();
shoppingList.put(ingredient, weight);
}
EDIT: I looked at your code and domain again and made some changes, see my example code here: https://github.com/akoster/x-converter/blob/master/src/main/java/xcon/stackoverflow/shopping
The domain model was a bit confusing because of the 'Info' classes. I renamed them as follows:
MealInfo -> Meal
Meal -> Recipe (with a list of Ingredients)
IngredientInfo -> Ingredient (represents a certain amount of a FoodItem)
Ingredient -> FoodItem (e.g. 'broccoli')
I realized that the service took no arguments! That's a bit odd. It makes sense to get the user separately (e.g. depending on the currently logged in/selected user) and pass it into the service, as you see above. The ShoppingListService now looks like this:
public class ShoppingListService {
private DietMealsService dietMealsService;
public ShoppingListService(DietMealsService dietMealsService) {
this.dietMealsService = dietMealsService;
}
public ShoppingList createShoppingList(User user) {
List<Meal> meals = getMeals(user);
dietMealsService.adjustIngredients(meals);
return createShoppingList(meals);
}
private List<Meal> getMeals(User user) {
Diet diet = user.getDiet();
if (diet == null || diet.getMeals() == null || diet.getMeals().isEmpty()) {
throw new ShoppingServiceException("User doesn't have diet");
}
return diet.getMeals();
}
private ShoppingList createShoppingList(List<Meal> meals) {
ShoppingList shoppingList = new ShoppingList();
for (Meal meal : meals) {
processIngredientWeights(meal.getRecipe().getIngredients(), shoppingList);
}
return shoppingList;
}
private void processIngredientWeights(List<Ingredient> ingredients, ShoppingList shoppingList) {
for (Ingredient ingredient : ingredients) {
shoppingList.addWeight(ingredient);
}
}
}
I also introduced a 'ShoppingList' class because passing a Map around is a code smell and now I could move the logic to add an ingredient to the shopping list into that class.
import lombok.Data;
#Data
public class ShoppingList {
private final Map<FoodItem, Long> ingredientWeights = new HashMap<>();
public void addWeight(Ingredient ingredient) {
FoodItem foodItem = ingredient.getFoodItem();
Long weight = ingredientWeights.getOrDefault(foodItem, 0L);
weight += ingredient.getWeight();
ingredientWeights.put(foodItem, weight);
}
}
The unit test for this service now looks like this:
#RunWith(MockitoJUnitRunner.class)
public class ShoppingListServiceTest {
#InjectMocks
private ShoppingListService instanceUnderTest;
#Mock
private DietMealsService dietMealsService;
#Mock
private User user;
#Mock
private Diet diet;
#Mock
private Meal meal;
#Test(expected = ShoppingServiceException.class)
public void testCreateShoppingListUserDietNull() {
// SETUP
User user = mock(User.class);
when(user.getDiet()).thenReturn(null);
// CALL
instanceUnderTest.createShoppingList(user);
}
#Test(expected = ShoppingServiceException.class)
public void testCreateShoppingListUserDietMealsNull() {
// SETUP
when(user.getDiet()).thenReturn(diet);
when(diet.getMeals()).thenReturn(null);
// CALL
instanceUnderTest.createShoppingList(user);
}
#Test(expected = ShoppingServiceException.class)
public void testCreateShoppingListUserDietMealsEmpty() {
// SETUP
when(user.getDiet()).thenReturn(diet);
List<Meal> meals = new ArrayList<>();
when(diet.getMeals()).thenReturn(meals);
// CALL
instanceUnderTest.createShoppingList(user);
}
#Test
public void testCreateShoppingListAdjustsIngredients() {
// SETUP
when(user.getDiet()).thenReturn(diet);
List<Meal> meals = Collections.singletonList(meal);
when(diet.getMeals()).thenReturn(meals);
// CALL
instanceUnderTest.createShoppingList(user);
// VERIFY
verify(dietMealsService).adjustIngredients(meals);
}
#Test
public void testCreateShoppingListAddsWeights() {
// SETUP
when(user.getDiet()).thenReturn(diet);
when(diet.getMeals()).thenReturn(Collections.singletonList(meal));
Recipe recipe = mock(Recipe.class);
when(meal.getRecipe()).thenReturn(recipe);
Ingredient ingredient1 = mock(Ingredient.class);
Ingredient ingredient2 = mock(Ingredient.class);
when(recipe.getIngredients()).thenReturn(Arrays.asList(ingredient1, ingredient2));
FoodItem foodItem = mock(FoodItem.class);
when(ingredient1.getFoodItem()).thenReturn(foodItem);
when(ingredient2.getFoodItem()).thenReturn(foodItem);
Long weight1 = 42L;
Long weight2 = 1337L;
when(ingredient1.getWeight()).thenReturn(weight1);
when(ingredient2.getWeight()).thenReturn(weight2);
// CALL
ShoppingList shoppingList = instanceUnderTest.createShoppingList(user);
// VERIFY
Long expectedWeight = weight1 + weight2;
Long actualWeight = shoppingList.getIngredientWeights().get(foodItem);
assertEquals(expectedWeight, actualWeight);
}
}
I hope that's pretty self-explanatory.
BTW remember that a unit test should only test the class under test. Try to minimize any assumptions about behavior of other classes and make that explicit by mocking them, as shown above. For the same reason I always try to avoid using 'realistic' test data in unit tests because it suggests that the values matter to the test - they don't.

I fixed my design like Adrian said, and created a test to this method. Regarding to my code below, I have a few questions:
What do you think about my tests? The first section in setUp method and above is necessary or can I replace it somehow better? Maybe can I create an example entity in the database only for a test?
What more cases should I test?
Is it ok to extract user.getDiet() to separate method checkDiet() and using try-catch inside?
Why I get ShoppingServiceException("User not found") when I remove login and password fields from my variable user though I mock behavior of methods here when(userInfoService.getUser()).thenReturn(user);
when(userRepository.findByLoginAndPassword(anyString(),anyString())).thenReturn(Optional.of(user));
My refactored ShoppingServiceClass:
#Service
#Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
#Data
#NoArgsConstructor
public class ShoppingListService {
UserInfoService userInfoService;
DietMealsService dietMealsService;
UserRepository userRepository;
#Autowired
public ShoppingListService(UserInfoService userInfoService, DietMealsService dietMealsService,UserRepository userRepository) {
this.userInfoService = userInfoService;
this.dietMealsService = dietMealsService;
this.userRepository = userRepository;
}
public Map<Ingredient,Long> createShoppingList() throws ShoppingServiceException {
User user = findUser(userInfoService.getUser()).orElseThrow(() -> new ShoppingServiceException("User not found"));
List<MealInfo> meals = checkDiet(user).getMeals();
dietMealsService.adjustMealsIngredients(meals);
Map<Ingredient, Long> shoppingList = new HashMap<>();
processMeals(meals, shoppingList);
return shoppingList;
}
private Optional<User> findUser(User user) {
if (user != null) {
return userRepository.findByLoginAndPassword(user.getLogin(), user.getPassword());
}
else {
return Optional.empty();
}
}
private Diet checkDiet(User user){
try{
user.getDiet().getMeals();
} catch(NullPointerException e){
throw new ShoppingServiceException("User doesn't have diet");
}
return user.getDiet();
}
private void processMeals(List<MealInfo> meals, Map<Ingredient, Long> shoppingList) {
for (MealInfo mealInfo : meals) {
processIngredientWeights(mealInfo.getMeal().getIngredientWeights(), shoppingList);
}
}
private void processIngredientWeights(List<IngredientWeight> ingredientWeights, Map<Ingredient, Long> shoppingList) {
for (IngredientWeight ingredientWeight: ingredientWeights) {
processIngredientWeight(ingredientWeight, shoppingList);
}
}
private void processIngredientWeight(IngredientWeight ingredientWeight, Map<Ingredient, Long> shoppingList) {
Ingredient ingredient = ingredientWeight.getIngredient();
Long weight = shoppingList.getOrDefault(ingredient, 0L);
weight += ingredientWeight.getWeight();
shoppingList.put(ingredient, weight);
}
}
And ShoppingServiceTest class:
#RunWith(MockitoJUnitRunner.class)
public class ShoppingListServiceTest {
#InjectMocks
ShoppingListService shoppingListService;
#Mock
UserInfoService userInfoService;
#Mock
DietMealsService dietMealsService;
#Mock
UserRepository userRepository;
private Ingredient pineapple;
private Ingredient bread;
private Ingredient butter;
private IngredientWeight pineappleWeight;
private IngredientWeight bread1Weight;
private IngredientWeight bread2Weight;
private IngredientWeight butterWeight;
private Meal meal1;
private Meal meal2;
private Meal meal3;
private MealInfo mealInfo1;
private MealInfo mealInfo2;
private MealInfo mealInfo3;
private Diet diet;
private User user;
private User user2;
#Before
public void setUp() {
//Ingredient
pineapple = new Ingredient();
pineapple.setName("Pineapple");
bread = new Ingredient();
bread.setName("Bread");
butter = new Ingredient();
butter.setName("Butter");
//IngredientWeight
pineappleWeight = new IngredientWeight();
pineappleWeight.setIngredient(pineapple);
pineappleWeight.setWeight(200L);
bread1Weight = new IngredientWeight();
bread1Weight.setIngredient(bread);
bread1Weight.setWeight(300L);
bread2Weight = new IngredientWeight();
bread2Weight.setIngredient(bread);
bread2Weight.setWeight(200L);
butterWeight = new IngredientWeight();
butterWeight.setIngredient(butter);
butterWeight.setWeight(50L);
//Meal
meal1 = new Meal();
meal1.setIngredientWeights(Arrays.asList(bread1Weight,butterWeight));
meal2 = new Meal();
meal2.setIngredientWeights(Arrays.asList(pineappleWeight,bread2Weight));
meal3 = new Meal();
meal3.setIngredientWeights(Arrays.asList(butterWeight,bread2Weight));
//MealInfo
mealInfo1 = new MealInfo();
mealInfo1.setMeal(meal1);
mealInfo1.setName("Posiłek 1"); //Meal 1
mealInfo2 = new MealInfo();
mealInfo2.setMeal(meal2);
mealInfo2.setName("Posiłek 2"); //Meal 2
mealInfo3 = new MealInfo();
mealInfo3.setMeal(meal3);
mealInfo3.setName("Posiłek 3"); //Meal 3
//Diet
diet = new Diet();
diet.setMeals(Arrays.asList(mealInfo1,mealInfo2,mealInfo3));
//User
user = new User();
user.setDiet(diet);
user.setLogin("123");
user.setPassword("123");
//User
user2 = new User();
user2.setLogin("123");
user2.setPassword("123");
}
#Test(expected = ShoppingServiceException.class)
public void shouldThrownShoppingServiceExceptionWhenUserNotFound() throws ShoppingServiceException {
shoppingListService.createShoppingList();
}
#Test
public void shouldReturnShoppingListWhenUserHasDiet(){
when(userInfoService.getUser()).thenReturn(user);
when(userRepository.findByLoginAndPassword(anyString(),anyString())).thenReturn(Optional.of(user));
doNothing().when(dietMealsService).adjustMealsIngredients(anyList());
Map<Ingredient,Long> expectedResult = new HashMap<>();
expectedResult.put(pineapple, 200L);
expectedResult.put(bread, 700L);
expectedResult.put(butter,100L);
Map<Ingredient,Long> actualResult = shoppingListService.createShoppingList();
assertEquals(actualResult,expectedResult);
}
#Test(expected = ShoppingServiceException.class)
public void shouldReturnShoppingServiceExceptionWhenUserDoesntHaveDiet(){
when(userInfoService.getUser()).thenReturn(user2);
when(userRepository.findByLoginAndPassword(anyString(),anyString())).thenReturn(Optional.of(user2));
doNothing().when(dietMealsService).adjustMealsIngredients(anyList());
Map<Ingredient,Long> expectedResult = new HashMap<>();
Map<Ingredient,Long> actualResult = shoppingListService.createShoppingList();
assertEquals(actualResult,expectedResult);
}
}

Related

Why isn´t it possible to change a reference object of an entity with JPA?

I have a ticket object which should be edited. In my ticket object are attributes which reference to object(see below).
...
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "project_id", referencedColumnName="id")
private Project project;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "costCenter_id", referencedColumnName="id")
private CostCenter costCenter;
...
But when I try to update the entity I always get an error:
Could not commit JPA transaction; nested exception is javax.persistence.RollbackException: Error while committing the transaction
#PutMapping("/tickets/{id}")
#ResponseStatus(HttpStatus.OK)
public Ticket updateTicket(#RequestBody Ticket ticket) throws Exception{
Optional<Ticket> o = this.ticketRepo.findById(ticket.getId());
o.ifPresent(element -> {
if(ticket.getCostCenter() != null) {
Optional<CostCenter> c = this.costCenterRepo.findById(ticket.getCostCenter().getId());
c.ifPresent( costCenter -> {
element.setCostCenter(costCenter);
});
}
if(ticket.getProject() != null) {
Optional<Project> p = this.projectRepo.findById(ticket.getProject().getId());
p.ifPresent(project -> {
element.setProject(project);
});
}
this.ticketRepo.save(element);
});
return o.orElseThrow(() -> new NotFoundException(ticket.getId()));
}
PS: When I trigger the update without changes everything works fine.
Stacktrace: https://textsaver.flap.tv/lists/2vm5
class AuditorAwareImpl implements AuditorAware<Long> {
#Override
public Optional<Long> getCurrentAuditor() {
PersonRepository personRepo = ApplicationContextProvider.getApplicationContext().getBean(PersonRepository.class);
if(SecurityContextHolder.getContext().getAuthentication() != null) {
Person p = personRepo.findByUserPrincipalName(SecurityContextHolder.getContext().getAuthentication().getName() + "#email.com");
return Optional.of(p.getId());
} else {
Person p = personRepo.findByUserPrincipalName("SYSTEM");
return Optional.of(p.getId());
}
}
}
#Component(value = "applicationContextProvider")
class ApplicationContextProvider implements ApplicationContextAware {
private static class AplicationContextHolder {
private static final InnerContextResource CONTEXT_PROV = new InnerContextResource();
}
private static final class InnerContextResource {
private ApplicationContext context;
private void setContext(ApplicationContext context) {
this.context = context;
}
}
public static ApplicationContext getApplicationContext() {
return AplicationContextHolder.CONTEXT_PROV.context;
}
#Override
public void setApplicationContext(ApplicationContext ac) {
AplicationContextHolder.CONTEXT_PROV.setContext(ac);
}
}
#Data
#Getter
#Setter
#MappedSuperclass
#EntityListeners(AuditingEntityListener.class)
#Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class BaseEntity implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", updatable = false, nullable = false)
protected Long id;
#CreatedDate
private Date createdAt;
#CreatedBy
private Long createdBy;
#LastModifiedDate
private Date updatedAt;
#LastModifiedBy
private Long updatedBy;
#PrePersist
protected void prePersist() {
if (this.createdAt == null) createdAt = new Date();
if (this.updatedAt == null) updatedAt = new Date();
}
#PreUpdate
protected void preUpdate() {
this.updatedAt = new Date();
}
#PreRemove
protected void preRemove() {
this.updatedAt = new Date();
}
}
You have a StackOverflowError which strongly suggest you've got some infinite recursion somewhere (or at least a very deep one):
Caused by: java.lang.RuntimeException: java.lang.StackOverflowError
The fact that com.mycompany.test.config.AuditorAwareImpl.getCurrentAuditor shows up repeatedly in your very long stack trace suggests that it is somehow involved in your infinite recursion and I'd wager that it comes from this class somehow triggering whatever triggered it in the first place possibly org.springframework.data.auditing.AuditingHandler. So check your AuditorAwareImpl code and/or the auditing configuration.

Postman request doesn't work - Rest APi implementation

I have a problem to create a POST or a GET request in postman. I have an controller that suppose to save a new project or to receive all projects from DB.
If I try to save a project in a class that implements CommandLineRunner in database it's everything ok. For more extra details I put my code below:
This is Project class:
#Entity
#Table(name = "project")
public class Project {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "proj_id")
private int projectId;
#Column(name = "project_name")
private String projectName;
#Column(name = "dg_number")
private int dgNumber;
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(
name = "variant_gate_relation",
joinColumns = {#JoinColumn(name = "proj_id")},
inverseJoinColumns = {#JoinColumn(name = "gate_id")}
)
private Set<Gate> gates = new HashSet<>();
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "variant_threshold_relation",
joinColumns = #JoinColumn(name = "proj_id"),
inverseJoinColumns = #JoinColumn(name = "threshold_id"))
private Set<Threshold> thresholds = new HashSet<>();
public Project() {
}
public Project(String projectName, int dgNumber, Set<Gate> gates, Set<Threshold> thresholds){
this.projectName = projectName;
this.dgNumber = dgNumber;
this.gates = gates;
this.thresholds = thresholds;
}
Controller
#RestController
#RequestMapping(ProjectController.PROJECT_URL)
public class ProjectController {
public static final String PROJECT_URL = "/cidashboard/projects";
#Autowired
private final ProjectService projectService;
public ProjectController(ProjectService projectService) {
this.projectService = projectService;
}
#GetMapping
public List<Project> getAllProjects(){
return projectService.findAllProjects();
}
#GetMapping("/{id}")
public Project getProjectById(#PathVariable int id) {
return projectService.findProjectById(id);
}
#PostMapping
// #Consumes(MediaType.APPLICATION_JSON_VALUE)
public Project saveProject(#RequestBody Project newProj) {
return projectService.saveProject(newProj);
}
}
This is my CommandLineRunner
#Component
public class Test implements CommandLineRunner {
private final ProjectRepository projectRepository;
private final GateRepository gateRepository;
private final ThresholdRepository thresholdRepository;
public Test(ProjectRepository projectRepository, GateRepository gateRepository, ThresholdRepository thresholdRepository) {
this.projectRepository = projectRepository;
this.gateRepository = gateRepository;
this.thresholdRepository = thresholdRepository;
}
#Override
public void run(String... args) throws Exception {
Gate gate1 = new Gate("gate5", 23);
gateRepository.save(gate1);
Threshold threshold1 = new Threshold(101, "threshold5");
thresholdRepository.save(threshold1);
Set<Gate> gates = new HashSet<>();
gates.add(gate1);
Set<Threshold> thresholds = new HashSet<>();
thresholds.add(threshold1);
Project project1 = new Project("project1", 20, gates, thresholds);
projectRepository.save(project1);
List<Project> allProjectsFromDatabase = projectRepository.findAll();
System.out.println("List of all projects from database : ");
for (Project project : allProjectsFromDatabase) {
System.out.println(project.toString());
}
System.out.println("---------------------------------------------");
List<Gate> allGatesFromDatabase = gateRepository.findAll();
for (Gate gate : allGatesFromDatabase) {
System.out.println(gate);
}
}
}
My output from console is :
1 project1 201 gate5 23.0 threshold5 101
I try to do this request from Postman:
{
"projectName": "project2",
"dgnumber": 1,
"gates": {
"gateType" : "gate2",
"gateValue" : 13
},
"thresholds": {
"thresholdType" : "threshold2",
"thresholdValue" : 22
}
}
And I receive the following output :
{
"projectId": 3,
"projectName": "project2",
"dgnumber": 1
}
And in DB only in project table the data was save, in gate table, threshold table, variant_gate_relation and variant_threshold_relation didn't save nothing
In your CommandLineRunner, you do this before saving the project.
Gate gate1 = new Gate("gate5", 23);
gateRepository.save(gate1);
Threshold threshold1 = new Threshold(101, "threshold5");
thresholdRepository.save(threshold1);
Set<Gate> gates = new HashSet<>();
gates.add(gate1);
Set<Threshold> thresholds = new HashSet<>();
thresholds.add(threshold1);
You should replicate that when saving from endpoint as well (the gateRepository.save() and thresholdRepository.save() are relevant here).

Why filter for Spring Data JPA Specification doesn't work?

I try select data from the table by a filter with Spring Data JPA Specification I think what my implementation is correct, But it doesn't work. Help me please understand my mistake and fix my example.
I have very strange SQL query in log :
select phone0_.id as id1_0_, phone0_.note as note2_0_, phone0_.number as number3_0_, phone0_.operator_login as operator4_0_, phone0_.operator_pass as operator5_0_, phone0_.operator_name as operator6_0_, phone0_.operator_url as operator7_0_, phone0_.reg_date as reg_date8_0_, phone0_.status as status9_0_ from phone phone0_ where 0=1 limit ?
In the end: where 0=1 it's crash my mind. Where did that come from?
Here I fill CriteriaBuilder if filter field not null. I expect to get correctly built Specification object and send it to findAll(Specifications.where(specification), Pageable p) method. But something incorrect.
My repo and specification impl:
public interface PhoneRepository extends CrudRepository<Phone, Integer>, JpaRepository<Phone, Integer>, JpaSpecificationExecutor<Phone> {
class PhoneSpecification implements Specification<Phone> {
private final #NonNull PhoneService.PhoneFilter filter;
public PhoneSpecification(#NonNull PhoneService.PhoneFilter filter) {
this.filter = filter;
}
#Override
public Predicate toPredicate(Root<Phone> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Predicate predicate = cb.disjunction();
if (nonNull(filter.getId())) {
cb.disjunction().getExpressions().add(cb.equal(root.get("id"), filter.getId()));
}
if (nonNull(filter.getNote())) {
cb.disjunction().getExpressions().add(cb.like(root.get("note"), filter.getNote()));
}
if (nonNull(filter.getNumber())) {
cb.disjunction().getExpressions().add(cb.like(root.get("number"), filter.getNumber()));
}
if (nonNull(filter.getStatus())) {
cb.disjunction().getExpressions().add(cb.like(root.get("status"), filter.getStatus()));
}
if (nonNull(filter.getOpName())) {
cb.disjunction().getExpressions().add(cb.like(root.get("operatorName"), filter.getOpName()));
}
if (nonNull(filter.getOpLogin())) {
cb.disjunction().getExpressions().add(cb.like(root.get("operatorAccLogin"), filter.getOpLogin()));
}
if (nonNull(filter.getOpPassword())) {
cb.disjunction().getExpressions().add(cb.like(root.get("operatorAccPassword"), filter.getOpPassword()));
}
if (nonNull(filter.getRegFrom()) && nonNull(filter.getRegTo())) {
cb.disjunction().getExpressions().add(cb.between(root.get("regDate"), filter.getRegFrom(), filter.getRegTo()));
}
return predicate;
}
}
}
This is service level:
#Service
public class PhoneService {
#Autowired
private PhoneRepository phoneRepository;
public Phone get(int id) {
Phone phone = phoneRepository.findOne(id);
return nonNull(phone) ? phone : new Phone();
}
public Page<Phone> list(#NonNull PhoneFilter filter) {
PhoneSpecification specification = new PhoneSpecification(filter);
return phoneRepository.findAll(Specifications.where(specification), filter.getPageable());
}
#Data
public static class PhoneFilter {
private Pageable pageable;
private Integer id;
private Timestamp regFrom;
private Timestamp regTo;
private String number;
private String opLogin;
private String opPassword;
private String opName;
private String status;
private String note;
}
}
And entity
#Entity
#Data
#NoArgsConstructor
#AllArgsConstructor
#Table(name = "phone")
#ToString(exclude = {"accounts"})
#EqualsAndHashCode(exclude = {"accounts"})
public class Phone {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
#OneToMany(mappedBy = "phone", cascade = CascadeType.DETACH)
private Collection<SocialAccount> accounts;
#Column(name = "reg_date")
private Timestamp regDate;
#Column(name = "number")
private String number;
#Column(name = "operator_url")
private String operatorUrl;
#Column(name = "operator_login")
private String operatorAccLogin;
#Column(name = "operator_pass")
private String operatorAccPassword;
#Column(name = "operator_name")
private String operatorName;
#Column(name = "status")
private String status;
#Column(name = "note")
private String note;
}
I find the mistake.
Method CriteriaBuilder.disjunction() this is factory and each time when I call him I got new Predicate object.
This implementation CriteriaBuilderImpl:
public Predicate disjunction() {
return new CompoundPredicate(this, BooleanOperator.OR);
}
Be careful with it.

Mapping hibernate entity with Jackson annotation

I'm working with Spring, hibernate and MySql but I have some problem with seralization of query result.
First in my entity I added #JsonManagedReference on Set structure (#OneToMany side) and #JsonBackReference on single object reference (#ManyToOne side) and it works but I wasn't be able to retrieve all needed information (for example #ManyToOne reference).
So i swapping #JsonBackReference on set structure and #JsonManagedReference on single object but I retrieve
No serializer found for class org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) ) (through reference chain: com.model.tablesField.TableUI["data"]->java.util.ArrayList[0]->com.domain.Car["carType"]->com.domain.CarType_$$_jvst744_f["handler"])
I tried also with #JsonIgnore on Set structure but it doesn't work for the same issues.
This is my spring configuration
private Properties getHibernateProperties() {
Properties properties = new Properties();
properties.put(PROPERTY_NAME_HIBERNATE_DIALECT, env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_DIALECT));
// properties.put(PROPERTY_NAME_HIBERNATE_SHOW_SQL, env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_SHOW_SQL));
properties.put(PROPERTY_NAME_HIBERNATE_FORMAT_SQL, env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_FORMAT_SQL));
properties.put("hibernate.enable_lazy_load_no_trans",true);
return properties;
and this is part of one of my several entities:
/**
* Car generated by hbm2java
*/
#Entity
#Table(name = "car", catalog = "ATS")
public class Car implements java.io.Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
private Integer idCar;
#JsonManagedReference
private CarType carType;
#JsonManagedReference
private Fleet fleet;
private String id;
private int initialKm;
private String carChassis;
private String note;
#JsonBackReference
private Set<Acquisition> acquisitions = new HashSet<Acquisition>(0);
public Car() {
}
public Car(CarType carType, Fleet fleet, int initialKm, String carChassis) {
this.carType = carType;
this.fleet = fleet;
this.initialKm = initialKm;
this.carChassis = carChassis;
}
public Car(CarType carType, Fleet fleet, String id, int initialKm, String carChassis, String note,
Set<Acquisition> acquisitions) {
this.carType = carType;
this.fleet = fleet;
this.id = id;
this.initialKm = initialKm;
this.carChassis = carChassis;
this.note = note;
this.acquisitions = acquisitions;
}
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "id_car", unique = true, nullable = false)
public Integer getIdCar() {
return this.idCar;
}
public void setIdCar(Integer idCar) {
this.idCar = idCar;
}
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "id_carType", nullable = false)
public CarType getCarType() {
return this.carType;
}
public void setCarType(CarType carType) {
this.carType = carType;
}
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "id_fleet", nullable = false)
public Fleet getFleet() {
return this.fleet;
}
public void setFleet(Fleet fleet) {
this.fleet = fleet;
}
#Column(name = "id", length = 5)
public String getId() {
return this.id;
}
public void setId(String id) {
this.id = id;
}
#Column(name = "initialKm", nullable = false)
public int getInitialKm() {
return this.initialKm;
}
public void setInitialKm(int initialKm) {
this.initialKm = initialKm;
}
#Column(name = "carChassis", nullable = false, length = 20)
public String getCarChassis() {
return this.carChassis;
}
public void setCarChassis(String carChassis) {
this.carChassis = carChassis;
}
#Column(name = "note", length = 100)
public String getNote() {
return this.note;
}
public void setNote(String note) {
this.note = note;
}
#OneToMany(fetch = FetchType.LAZY, mappedBy = "car")
public Set<Acquisition> getAcquisitions() {
return this.acquisitions;
}
public void setAcquisitions(Set<Acquisition> acquisitions) {
this.acquisitions = acquisitions;
}
}
one method that uses the query:
#Override
#RequestMapping(value = { "/cars/{idFleet}"}, method = RequestMethod.GET)
public #ResponseBody TableUI getCars(#PathVariable int idFleet) {
TableUI ajaxCall=new TableUI();
try {
ajaxCall.setData(fleetAndCarService.findCarsByIdFleet(idFleet));
return ajaxCall;
} catch (QueryException e) {
ErrorResponse errorResponse= ErrorResponseBuilder.buildErrorResponse(e);
LOG.error("Threw exception in FleetAndCarControllerImpl::addCar :" + errorResponse.getStacktrace());
return ajaxCall;
}
}
two class for the query:
public interface DefRdiRepository extends JpaRepository<DefRdi, Integer>{
//#Query("SELECT CASE WHEN COUNT(c) > 0 THEN true ELSE false END FROM DefRdi c WHERE c.parName = ?1 AND c.description= ?2")
//Boolean existsByParNameAndDescription(String parName, String description);
//Query method of spring, I put findBy and then the key of research
DefRdi findByParNameAndDescription(String parName, String description);
}
public interface CarRepository extends JpaRepository<Car, Integer>, CarRepositoryCustom {
//Query method of spring, I put findBy and then the key of research
List<Car> findByFleetIdFleet(int idFleet);
}
Where is my error? I don't want Set object but only the single reference. The problem is only when I serialize. Thanks
UPDATE:
I use #JSonIgnore on all set collectionts and Eager instead lazy ad all works fine, but is there a way to retrieve all the information only when I want, for example having two different query?
So it doesn't work
#Override
#Transactional
public List<Car> findByFleetIdFleet(int idFleet) {
List<Car> carList= carRepository.findByFleetIdFleet(idFleet);
for (Car car:carList){
Hibernate.initialize(car.getCarType());
Hibernate.initialize(car.getFleet());
}
return carList;
// return carRepository.findByFleetIdFleet(idFleet);
}
All collections need to be fetched eagerly when loading them from data base, in order to get serialized by Spring. Make sure you fetch them eagerly (e.g. FetchMode.JOIN). You could also swap #JsonManagedReference from wanted fields with #JsonIgnore to black listed fields, Spring automatically serialises every field without annotation.
Update:
Changing the data repository to something like that should work, I am not sure it compiles, but I think you will get the point:
#EntityGraph(value = "some.entity.graph", type = EntityGraph.EntityGraphType.FETCH)
#Query(
value = "SELECT c FROM Car c INNER JOIN FETCH c.acquisitions WHERE c.id = :idFleet"
)
public interface CarRepository extends JpaRepository<Car, Integer>, CarRepositoryCustom {
//Query method of spring, I put findBy and then the key of research
List<Car> findByFleetIdFleet(int idFleet);
}
For more information look at this post and read the official documentation.
Workaround:
There seems to be a workaround, however fetching those collections eager like shown above should have a positive performance impact, since there is no need for loading proxies afterwards. Also no open transactions are needed at controller level.

Spring MVC CrudRepository findByIn

I have a CrudRepository that is supposed to make a query with an array (findByIn). In my repository tests it works, but when I try to use the query in my service, it doesn't work. Could someone explain why it doesn't work? Here is my setup (excluding some code irrelevant to the question)
Database model:
#Entity
#Table(name="Place")
public class Place implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "placeId", nullable = false)
private Long placeId;
#Column(name = "owner", nullable = false)
private String owner;
public Long getPlaceId() {
return placeId;
}
public void setPlaceId(Long placeId) {
this.placeId = placeId;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
}
Repository:
#Repository
public interface PlaceRepository extends CrudRepository<Place, Long> {
List<Place> findByPlaceIdIn(Long[] placeId);
}
Service (this is the part not working):
#Service
public class PlaceService {
#Autowired
private PlaceRepository placeRepository;
public List<Place> getPlaces(Long[] placeIds) {
return placeRepository.findByPlaceIdIn(placeIds);
}
}
The problem is that in my service placeRepository.findByPlaceIdIn(placeIds) returns 0 objects if placeIds contains more than one item. If placeIds contains just one item, the query works fine. I tried replacing return placeRepository.findByPlaceIdIn(placeIds) with this piece of code that does the query for every array item one by one (this actually works, but I'd like to get the query work as it should):
ArrayList<Place> places = new ArrayList<Place>();
for (Long placeId : placeIds) {
Long[] id = {placeId};
places.addAll(placeRepository.findByPlaceIdIn(id));
}
return places;
I know that the repository should work, because I have a working test for it:
public class PlaceRepositoryTest {
#Autowired
private PlaceRepository repository;
private static Place place;
private static Place place2;
private static Place otherUsersPlace;
#Test
public void testPlacesfindByPlaceIdIn() {
place = new Place();
place.setOwner(USER_ID);
place2 = new Place();
place2.setOwner(USER_ID);
place = repository.save(place);
place2 = repository.save(place2);
Long[] ids = {place.getPlaceId(), place2.getPlaceId()};
assertEquals(repository.findByPlaceIdIn(ids).size(), 2);
}
}
I also have another repository for other model, which also uses findByIn and it works fine. I can't see any relevant difference between the repositories. I thought it might offer some more details to show the working repository, so I included it below:
Database model:
#Entity
#Table(name="LocalDatabaseRow")
#JsonIgnoreProperties(ignoreUnknown=false)
public class LocalDatabaseRow implements Serializable {
public LocalDatabaseRow() {}
public LocalDatabaseRow(RowType rowType) {
this.rowType = rowType;
}
public enum RowType {
TYPE1,
TYPE2
};
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id")
#JsonProperty("id")
private Long id;
#JsonProperty("rowType")
#Column(name = "rowType")
private RowType rowType;
public Long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public RowType getRowType() {
return rowType;
}
public void setRowType(RowType rowType) {
this.rowType = rowType;
}
}
Repository:
#Repository
public interface LocalDatabaseRowRepository extends CrudRepository<LocalDatabaseRow, Long> {
List<LocalDatabaseRow> findByRowTypeAndUserIdIn(RowType type, String[] userId);
}
try using a list instead :
findByPlaceIdIn(List placeIdList);
You have a typo in your code (the repository declaration in the service):
#Autowired
private placeRepository placeRepository;
Should be:
#Autowired
private PlaceRepository placeRepository;

Categories

Resources