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).
Related
I develop an application that allows to manage the candidates, the application contains two tables (candidate and techno) joined with a #ManyToMany join table, I'm looking for how to fill both tables with the same #PostMapping, as my code indicates. I'm using an Angular application witch send a candidat with all the informations and a table of techno (that the candidat have to select, he can not add a new techno). I would like to join the new candidat with some techno. This is what the controller will receive:
{prenom: "Pname", nom: "Name", pseudo: "Pnamename", ecole: "school", mail: "email#email.com", …}
ecole: "school"
mail: "email#email.com"
nom: "Name"
numTel: "0123456789"
prenom: "Pname"
pseudo: "Pnamename"
roleCible: "poste"
secteurActivites: "sector"
techno: Array(3)
0: "android"
1: "drupal"
2: "html"
length: 3
__proto__: Array(0)
typeContrat: "CDI"
villeRecherchee: "Paris"
__proto__: Object
1- Candidat.java
#Entity
#Table(name = "Candidats")
public class Candidat {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
private String nom;
private String prenom;
private String ecole;
private String numTel;
private String mail;
private String pseudo;
private String roleCible;
private String typeContrat;
private String villeRecherchee;
#Temporal(TemporalType.DATE)
private Date dateCurrent = new Date();
#ManyToMany(fetch = FetchType.LAZY, cascade = { CascadeType.PERSIST, CascadeType.MERGE })
#JoinTable(name = "candidat_techno", joinColumns = { #JoinColumn(name = "candidat_id") },
inverseJoinColumns = {
#JoinColumn(name = "techno_id") })
private Set<Techno> techno = new HashSet<>();
public Candidat() {
}
#SuppressWarnings("unchecked")
public Candidat(String nom, String prenom, String ecole, String numTel, String mail, String pseudo,
String roleCible, String typeContrat, String villeRecherchee, List<Techno> techno, Date dateCurrent,) {
super();
this.nom = nom;
this.prenom = prenom;
this.ecole = ecole;
this.numTel = numTel;
this.mail = mail;
this.pseudo = pseudo;
this.roleCible = roleCible;
this.typeContrat = typeContrat;
this.villeRecherchee = villeRecherchee;
this.techno = (Set<Techno>) techno;
this.dateCurrent = new Date();
//getters ans setters
2- CandidatController
#CrossOrigin(origins = "http://localhost:4200")
#RestController
#RequestMapping("/avatar")
public class CandidatController {
#Autowired
CandidatDao candidatdao;
#Autowired
TechnoDao technoDao;
#PostMapping(value = "/add-candidat")
public Candidat addCandidate(#RequestBody Candidat Candidat) {
Candidat candidatAdded = candidatdao.save(Candidat);
return candidatAdded;
technodao.save(Candidat.getTechno());
}
}
3- CandidatDAO
#Repository
public interface CandidatDao extends JpaRepository<Candidat, String> {
}
4-Techno.java
#Entity
#Table(name = "techno")
public class Techno {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
private String nomTechno;
#ManyToMany(fetch = FetchType.LAZY, cascade = { CascadeType.PERSIST, CascadeType.MERGE }, mappedBy = "techno")
private Set<Candidat> candidat = new HashSet<Candidat>();
public Techno() {
}
#SuppressWarnings("unchecked")
public Techno(String nomTechno, Candidat candidat) {
super();
this.nomTechno = nomTechno;
this.candidat = (Set<Candidat>) candidat;
}
public String getNomTechno() {
return nomTechno;
}
public void setNomTechno(String nomTechno) {
this.nomTechno = nomTechno;
}
#Override
public String toString() {
return "Techno [nomTechno=" + nomTechno + ", candidat=" + candidat + "]";
}
//getters ans setters
5- TechnoController
#CrossOrigin(origins = "http://localhost:4200")
#RestController
#RequestMapping("/avatar")
public class TechnoController {
#Autowired
TechnoDao technodao;
#PostMapping(value = "/add-techno")
public Techno addCandidate(#RequestBody Techno Techno) {
Techno technoAdded = technodao.save(Techno);
return technoAdded;
}
}
6- TechnoDao
#Repository
public interface TechnoDao extends JpaRepository<Techno, String> {
Techno save(Set<Techno> techno);
}
for now I can fill both tables, but with two different post mapping.
how to fill both tables (techno and candidate) at the same time with a single #post mapping ?? like this:
{
id: 1,
nom: "smith",
prenom: "john",
ecole: "usa",
numTel: "11111",
mail: "j#smith",
pseudo: "JS",
roleCible: "usa",
typeContrat: "usa",
villeRecherchee: "paris",
dateCurrent: "2019-10-02",
techno: [
{
id: 1,
nomTechno: "springBoot"
},
{
id: 2,
nomTechno: "java"
}
]
}
In your CandidateController, Add this:
#Autowired
TechnoDao technoDao;
Inside post mapping use this:
technoDao.save(candidat.getTechno());
This has to help you.
I wish to have subquery, which provides me filtering actors by name.
I have a rest controller's method, which returns list of actors as JSON from movie base on movieId. I try to add filters as specification, but I have no idea how to write proper query. Base on "Spring Data JPA Specification for a ManyToMany Unidirectional Relationship" I found solution for subquery, which returns me all actors to proper movie base on movieId. Now I try to write this query.
Actor entity
#Data
#NoArgsConstructor
#Entity
#Table(name = "actors")
public class Actor implements Serializable {
private static final long serialVersionUID = 6460140826650392604L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "actor_id")
private Long actorId;
#Column(name = "first_name")
private String firstName;
#ManyToMany(mappedBy = "actors")
#ToString.Exclude
private List<Movie> movie = new ArrayList<>();
#JsonIgnore
public List<Movie> getMovie() {
return this.movie;
}
}
Movie entity
#Data
#Entity
#NoArgsConstructor
#Table(name = "movies")
public class Movie implements Serializable {
private static final long serialVersionUID = 3683778473783051508L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "movie_id")
private Long movieId;
private String title;
#ManyToMany(cascade = { CascadeType.ALL })
#JoinTable(name = "movies_actors"
, joinColumns = { #JoinColumn(name = "movie_id") }
, inverseJoinColumns = { #JoinColumn(name = "actor_id") })
private List<Actor> actors = new ArrayList<>();
#JsonIgnore
public List<Actor> getActors() {
return this.actors;
}
}
//Rest Controller
#CrossOrigin(origins = "http://localhost:3000")
#RestController
#RequestScope
#RequestMapping("/rest")
public class ActorRestController {
private ActorService actorService;
private MovieService movieService;
#Autowired
public ActorRestController(ActorService actorService, MovieService movieService) {
this.actorService = actorService;
this.movieService = movieService;
}
.
.
.
#GetMapping("movies/{movieId}/actors")
public ResponseEntity<Page<Actor>> getAllActorsFromMovieByIdMovie(#PathVariable(name = "movieId") Long movieId, Pageable pageable) {
Optional<Movie> movieFromDataBase = movieService.findMovieById(movieId);
if (movieFromDataBase.isPresent()) {
return new ResponseEntity<>(actorService.findAllActors(ActorSpec.query(movieId), pageable), HttpStatus.OK);
} else {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
.
.
}
// Specification for actor
#NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ActorSpec {
public static Specification<Actor> query(final Long movieId) {
return (root, query, cb) -> {
query.distinct(true);
Subquery<Movie> movieSubQuery = query.subquery(Movie.class);
Root<Movie> movie = movieSubQuery.from(Movie.class);
Expression<List<Actor>> actors = movie.get("actors");
movieSubQuery.select(movie);
movieSubQuery.where(cb.equal(movie.get("movieId"), movieId), cb.isMember(root, actors));
return cb.exists(movieSubQuery);
};
}
}
I would like, my code will return filtered actors by name ex.:
http://localhost:8080/rest/movies/48/actors?name=Collin
will return me
{ "actorId": 159,
"firstName": "Collin",
"lastName": "Konopelski",
"age": 21
},
but in case I do not sent any request param (http://localhost:8080/rest/movies/48/actors), let program return me all actors. I don't want to create new endpoint only for #Requestparam cause, this one is used by UI created in React.
Thanks!
Ok I found,
My solution:
RestController
#GetMapping("movies/{movieId}/actors")
public ResponseEntity<Page<Actor>> getAllActorsFromMovieByIdMovie(#PathVariable(name = "movieId") Long movieId,
#RequestParam(name = "name", required = false) String name,
Pageable pageable) {
Optional<Movie> movieFromDataBase = movieService.findMovieById(movieId);
if (movieFromDataBase.isPresent()) {
return new ResponseEntity<>(actorService.findAllActors(ActorSpec.query(movieId ,name), pageable), HttpStatus.OK);
} else {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
Specification
#NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ActorSpec {
public static Specification<Actor> query(final Long movieId, String name) {
return (root, query, cb) -> {
Predicate predicateMovieID = getPredicateByMovieId(movieId, root, query, cb);
if (Strings.isNotBlank(name)) {
Predicate a = cb.and(predicateMovieID, cb.equal(root.get("firstName"), name));
Predicate b = cb.and(predicateMovieID, cb.equal(root.get("lastName"), name));
return cb.or(a,b);
}
return cb.and(predicateMovieID);
};
}
private static Predicate getPredicateByMovieId(Long movieId, Root<Actor> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
query.distinct(true);
Subquery<Movie> movieSubQuery = query.subquery(Movie.class);
Root<Movie> movie = movieSubQuery.from(Movie.class);
Expression<List<Actor>> actors = movie.get("actors");
movieSubQuery.select(movie);
movieSubQuery.where(cb.equal(movie.get("movieId"), movieId), cb.isMember(root, actors));
return cb.exists(movieSubQuery);
}
}
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);
}
}
My Java application has become very slow due to the BLOB field that each entity has. This field is usually used to store a PDF file and whenever i have to list all the objects, it takes a considerable amount of time until the persistence provider can finish its job. I looked for answers on how to deal with such type of data, but some of them talk about storing the BLOB in a separate table and then using FetchType.LAZY. Is there any way to fetch this field only when needed without having to create another table? If not, is creating another table the most appropriate solution?
Entity Code
#Cache(alwaysRefresh = true)
public class ScdDocumento implements Serializable, MultipleSelector {
#Transient
public static final String QUERY_RELATORIO_DOC_LOC = "consultas/ctrl_docs/consulta_relatorio_doc_local.txt";
#Transient
public static final String QUERY_RELATORIO_DOC_GRUPO = "consultas/ctrl_docs/consulta_relatorio_doc_grupo.txt";
#Id
#Column(name = "nome", length = 50)
private String nome;
#Column(name = "revisao")
private int revisao;
#Column(name = "id_tipo")
private int id_tipo;
#Column(name = "situacao", length = 1)
private String situacao;
#Column(name = "doc_blob_nome", length = 50)
private String doc_blob_nome;
#Lob
#Basic(fetch = FetchType.LAZY)
#Column(name = "documento_blob", nullable = false)
private byte[] documento_blob; //The field that impacts the application perfomance
#Column(name = "abrangencia_geral")
private int abrangencia_geral;
#ManyToMany
#JoinTable(name = "SCD_DOC_GRUPO", joinColumns = {#JoinColumn(name = "id_doc")},
inverseJoinColumns = {#JoinColumn(name = "id_grupo")})
private Set<SosGrupo> grupos;
#ManyToOne
#JoinColumn(name = "id_tipo", insertable = false, updatable = false)
private ScdTipo tipo;
#ManyToMany
#JoinTable(name = "SCD_REFERENCIA", joinColumns = {#JoinColumn(name = "doc_pai")},
inverseJoinColumns = {#JoinColumn(name = "doc_filho")})
private Set<ScdDocumento> referencias;
#ManyToMany
#JoinTable(name = "SCD_REFERENCIA", joinColumns = {#JoinColumn(name = "doc_filho")},
inverseJoinColumns = {#JoinColumn(name = "doc_pai")})
private Set<ScdDocumento> referenciadoPor;
#ManyToMany
#JoinTable(name = "SCD_PALAVRA_REFERENCIA", joinColumns = {#JoinColumn(name = "documento")},
inverseJoinColumns = {#JoinColumn(name = "palavra")})
private Set<ScdPalavraChave> palavrasChaves;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "documento")
private Set<ScdOrdem> ordens;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "documento")
private Set<ScdArquivoOs> arquivosOs;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "documento")
private Set<ScdArquivoHistorico> arquivosHistorico;
#ManyToMany(cascade = {CascadeType.REFRESH, CascadeType.MERGE})
#JoinTable(name = "SCD_LOCAL_DOC", joinColumns = {#JoinColumn(name = "id_doc")},
inverseJoinColumns = {#JoinColumn(name = "id_local")})
private Set<ScdLocal> locais;
#Override
public String getNome() {
return nome;
}
public void setNome(String nome) {
this.nome = nome;
}
public int getRevisao() {
return revisao;
}
public void setRevisao(int revisao) {
this.revisao = revisao;
}
public int getIdTipo() {
return id_tipo;
}
public void setIdTipo(int id_tipo) {
this.id_tipo = id_tipo;
}
public String getSituacao() {
return situacao;
}
public void setSituacao(String situacao) {
this.situacao = situacao;
}
public String getDocBlobNome() {
return doc_blob_nome;
}
public void setDocBlobNome(String doc_blob_nome) {
this.doc_blob_nome = doc_blob_nome;
}
public byte[] getDocumentoBlob() {
return documento_blob;
}
public void setDocumentoBlob(byte[] documento_blob) {
this.documento_blob = documento_blob;
}
public int getAbrangenciaGeral() {
return abrangencia_geral;
}
public void setAbrangenciaGeral(int abrangencia_geral) {
this.abrangencia_geral = abrangencia_geral;
}
public Set<SosGrupo> getGrupos() {
return grupos;
}
public void setGrupos(Set<SosGrupo> grupo) {
this.grupos = grupo;
}
public ScdTipo getTipo() {
return tipo;
}
public void setTipo(ScdTipo tipo) {
this.tipo = tipo;
}
public Set<ScdDocumento> getReferencias() {
return referencias;
}
public void setReferencias(Set<ScdDocumento> referencias) {
this.referencias = referencias;
}
public Set<ScdDocumento> getReferenciadoPor() {
return referenciadoPor;
}
public void setReferenciadoPor(Set<ScdDocumento> referenciadoPor) {
this.referenciadoPor = referenciadoPor;
}
public Set<ScdPalavraChave> getPalavrasChaves() {
return palavrasChaves;
}
public void setPalavrasChaves(Set<ScdPalavraChave> palavrasChaves) {
this.palavrasChaves = palavrasChaves;
}
public Set<ScdOrdem> getOrdens() {
return ordens;
}
public void setOrdens(Set<ScdOrdem> ordens) {
this.ordens = ordens;
}
public Set<ScdArquivoOs> getArquivosOs() {
return arquivosOs;
}
public void setArquivosOs(Set<ScdArquivoOs> arquivosOs) {
this.arquivosOs = arquivosOs;
}
public Set<ScdArquivoHistorico> getArquivosHistorico() {
return arquivosHistorico;
}
public void setArquivosHistorico(Set<ScdArquivoHistorico> arquivosHistorico) {
this.arquivosHistorico = arquivosHistorico;
}
public Set<ScdLocal> getLocais() {
return locais;
}
public void setLocais(Set<ScdLocal> locais) {
this.locais = locais;
}
#Override
public String getIdRef() {
return nome;
}
#Override
public String getDesc() {
return tipo.getNome();
}
}
Methods that are causing the problem
GUI
private void loadDocumentTable(String situacao) {
mapaDocumentos = new TreeMap<>();
modelDocumentos.setRowCount(0);
docdao.getCriteria("situacao", situacao).forEach((e) -> {
mapaDocumentos.put(e.getNome(), e);
});
mapaDocumentos.entrySet().forEach((e) -> {
String desc = e.getValue().getDocBlobNome();
modelDocumentos.addRow(new Object[]{e.getKey(), desc.substring(0, desc.length() - 3), e.getValue().getRevisao()});
});
}
Generic Dao
#Override
public List<T> getCriteria(String column, Object value){
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<T> cq = cb.createQuery(clazz);
Root<T> root = cq.from(clazz);
EntityType<T> ent = root.getModel();
cq.where(cb.equal(root.get(ent.getSingularAttribute(column)), value.toString()));
return em.createQuery(cq).getResultList();
}
Table Model
Persistence
Added this to my persistence.xml, but eclipselink still fetched the byte[] field eagerly.
Maven Plugin
<plugin>
<groupId>de.empulse.eclipselink</groupId>
<artifactId>staticweave-maven-plugin</artifactId>
<version>1.0.0</version>
<executions>
<execution>
<phase>process-classes</phase>
<goals>
<goal>weave</goal>
</goals>
<configuration>
<persistenceXMLLocation>META-INF/persistence.xml</persistenceXMLLocation>
<logLevel>FINE</logLevel>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>org.eclipse.persistence.jpa</artifactId>
<version>2.5.2</version>
</dependency>
</dependencies>
</plugin>
Final Edit
The staticweave-maven-plugin actually worked, but I need to build the project everytime I change something in order for the performance to be boosted. This happens because the weaving is static, so it's applied at build time and not when running the project using the IDE.
JPA basics also allow specifying a fetch type of LAZY, which would prevent loading the BLOB until you access it in the entity. OneToOne, ManyToOne and basic mappings require byte code enhancement of your entities for EclipseLink to gain notification when you access a lazy attribute and load it, which is described here as weaving. This will ensure that it isn't loaded by default.
With the use of weaving, you can also use entity graphs to specify what is loaded and when. This can allow loading the blob in a single query with the rest of the entity when it is to be used, and exclude it by default elsewhere. See What is the diffenece between FETCH and LOAD for Entity graph of JPA? for info on load and fetch graphs.
I'm getting this errors when trying to create relation between 2 entities, this time i'm doing this in different way - passing JSON with 2 object into helper class and then getting those object and persisting them, one by one and setting the relation. When i remove setters of relation : 1. newPerson.setKoordynator(koordynatorzyPraktykEntity);
2.koordynatorzyPraktykEntity.setKoordynatorByIdOsoby(newPerson);
then it is persisting both entities without a problem, with setters only first one (KoordynatorzyPraktykEntity) is persisted (idKoordynatora = 1, idOsoby =0, test = test )
Here is the important part of error from POSTMAN ( full log http://pastebin.com/SRmnPMBH )
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.dao.InvalidDataAccessApiUsageException: detached entity passed to persist: praktyki.core.entities.KoordynatorzyPraktykEntity; nested exception is org.hibernate.PersistentObjectException: detached entity passed to persist: praktyki.core.entities.KoordynatorzyPraktykEntity
org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:978)
org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:868)
javax.servlet.http.HttpServlet.service(HttpServlet.java:644)
org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:842)
javax.servlet.http.HttpServlet.service(HttpServlet.java:725)
org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
KoordynatorzyEntity:
#Entity
#Table(name = "koordynatorzy_praktyk", schema = "public", catalog = "praktykidb")
public class KoordynatorzyPraktykEntity {
private int idKoordynatoraPraktyk;
private int idOsoby;
private String doTestow;
private OsobyEntity koordynatorByIdOsoby;
private Collection<KoordynatorzyKierunkowiEntity> koordynatorzyByIdKierunku;
#Id
#GeneratedValue
#Column(name = "id_koordynatora_praktyk")
public int getIdKoordynatoraPraktyk() {
return idKoordynatoraPraktyk;
}
public void setIdKoordynatoraPraktyk(int idKoordynatoraPraktyk) {
this.idKoordynatoraPraktyk = idKoordynatoraPraktyk;
}
#Basic
#Column(name = "id_osoby")
public int getIdOsoby() {
return idOsoby;
}
public void setIdOsoby(int idOsoby) {
this.idOsoby = idOsoby;
}
/*
STUFF
*/
#JsonIgnore
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "id_osoby", referencedColumnName = "id_osoby", insertable = false , updatable = false)
public OsobyEntity getKoordynatorByIdOsoby() {
return koordynatorByIdOsoby;
}
public void setKoordynatorByIdOsoby(OsobyEntity koordynatorByIdOsoby) {
this.koordynatorByIdOsoby = koordynatorByIdOsoby;
}
#JsonIgnore
#OneToMany(mappedBy = "koordynatorzyByIdKierunku", cascade = CascadeType.ALL)
#LazyCollection(LazyCollectionOption.FALSE)
public Collection<KoordynatorzyKierunkowiEntity> getKoordynatorzyByIdKierunku() {
return koordynatorzyByIdKierunku;
}
public void setKoordynatorzyByIdKierunku(Collection<KoordynatorzyKierunkowiEntity> koordynatorzyByIdKierunku) {
this.koordynatorzyByIdKierunku = koordynatorzyByIdKierunku;
}
OsobyEntity:
#Entity
#Table(name = "osoby", schema = "public", catalog = "praktykidb")
public class OsobyEntity {
private int idOsoby;
private String tytulZawodowy;
private String imie;
private String nazwisko;
private String email;
private String telefonKomorkowy;
private String telefonStacjonarny;
private KoordynatorzyPraktykEntity koordynator;
#Id
#GeneratedValue
#Column(name = "id_osoby")
public int getIdOsoby() {
return idOsoby;
}
public void setIdOsoby(int idOsoby) {
this.idOsoby = idOsoby;
}
/*
STUFF
*/
#OneToOne(mappedBy = "koordynatorByIdOsoby", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
public KoordynatorzyPraktykEntity getKoordynator() {
return koordynator;
}
public void setKoordynator(KoordynatorzyPraktykEntity koordynator) {
this.koordynator = koordynator;
}
KoordynatorzyPraktykService :
public class KoordynatorzyPraktykService implements iKoordynatorzyPraktykService {
#Autowired
private iKoordynatorzyPraktykDAO ikoordynatorzyPraktykDAO;
#Autowired
private iOsobyDAO iosobyDAO;
#Override
public KoordynatorzyPraktykEntity addCoordinator(KoordynatorzyPraktykEntity koordynatorzyPraktykEntity) {
return ikoordynatorzyPraktykDAO.addCoordinator(koordynatorzyPraktykEntity);
}
/*
STUFF
*/
#Override
public OsobyEntity addPerson(OsobyEntity osobyEntity, KoordynatorzyPraktykEntity koordynatorzyPraktykEntity) {
OsobyEntity newPerson = iosobyDAO.addPerson(osobyEntity);
newPerson.setKoordynator(koordynatorzyPraktykEntity);
System.out.println(koordynatorzyPraktykEntity.toString()); //shows idKoordynatora: 1 idOsoby: 0 test: test
System.out.println(newPerson.toString()); //shows idOsoby: 32768 imie: Tomasz nazwisko: Potempa
int idOsoby = newPerson.getIdOsoby();
koordynatorzyPraktykEntity.setIdOsoby(idOsoby);
System.out.println(koordynatorzyPraktykEntity.toString()); //shows idKoordynatora: 1 idOsoby: 32768 test: test
koordynatorzyPraktykEntity.setKoordynatorByIdOsoby(newPerson);
return newPerson;
}
Both DAOs have em.persist(entity)
and POST of KoordynatorzyPraktykController:
#RequestMapping(method = RequestMethod.POST)
public ResponseEntity<KoordynatorzyPraktykEntity> addCoordinator(#RequestBody Koordynator newCoordinator) {
KoordynatorzyPraktykEntity addCoordinator = ikoordynatorzyPraktykService.addCoordinator(newCoordinator.getKoordynator());
OsobyEntity addPerson = ikoordynatorzyPraktykService.addPerson(newCoordinator.getOsoba(), addCoordinator);
if (addCoordinator !=null && addPerson !=null) {
return new ResponseEntity<KoordynatorzyPraktykEntity>(addCoordinator, HttpStatus.OK);
}
else {
return new ResponseEntity<KoordynatorzyPraktykEntity>(HttpStatus.NOT_FOUND);
}
}
Helper Class Koordynator:
public class Koordynator {
private KoordynatorzyPraktykEntity koordynator;
private OsobyEntity osoba;
public KoordynatorzyPraktykEntity getKoordynator() {
return koordynator;
}
public void setKoordynator(KoordynatorzyPraktykEntity koordynator) {
this.koordynator = koordynator;
}
public OsobyEntity getOsoba() {
return osoba;
}
public void setOsoba(OsobyEntity osoba) {
this.osoba = osoba;
}
}
and this is parsed JSON into controller through POSTMAN
{
"koordynator":
{
"doTestow" : "test"
},
"osoba":
{
"tytulZawodowy" : "inzynier",
"imie" : "Tomasz",
"nazwisko" : "Potempa",
"email" : "tp#tp.pl",
"telefonKomorkowy" : "124675484",
"telefonStacjonarny" : "654786484"
}
}
Only way I got it work
Class A:
#OneToMany(cascade = CascadeType.MERGE)
private List<B> b;
Class B:
#ManyToOne
#JoinColumn(name = "aId", referencedColumnName = "id")
private A a;
private String test;
Service:
A a = new A();
//Create without children
aFacade.create(a);
//items
List<B> list = new ArrayList<>();
B b = new B();
b.setTest("Hello");
b.setA(a);
list.add(b);
//merge
a.setB(list);
aFacade.edit(a);
you hit the exception below simply because the entity isn't in the Entity Manager's session at the moment you are trying to persist it. That's due to laziness of your association.
"detached entity passed to persist: praktyki.core.entities.KoordynatorzyPraktykEntity;"
Try calling em.merge() to attach it to the session.