I'm having trouble with a many to many relation with JPA.
My code looks as follows:
The Sensor class:
#Entity
#Table(name = "sensor")
#Data
#NoArgsConstructor
#AllArgsConstructor
public class Sensor {
#Id
private long chipId;
#OneToMany(mappedBy = "sensor")
#JsonBackReference
private Set<Link> userLinks;
private String firmwareVersion;
private long creationTimestamp;
private String notes;
private long lastMeasurementTimestamp;
private long lastEditTimestamp;
private double gpsLatitude;
private double gpsLongitude;
private double gpsAltitude;
private String country;
private String city;
private boolean indoor;
private boolean published;
}
The user class:
#Entity
#Table(name = "user")
#Data
#NoArgsConstructor
#AllArgsConstructor
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#JsonManagedReference
private int id;
private String firstName;
private String lastName;
private String email;
private String password;
#OneToMany(mappedBy = "user")
private Set<Link> sensorLinks;
private int role;
private int status;
private long creationTimestamp;
private long lastEditTimestamp;
}
And the Link class (relation class):
#Entity
#Table(name = "link")
#Data
#AllArgsConstructor
#NoArgsConstructor
public class Link {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#ManyToOne
#JoinColumn(name = "user_id")
#MapsId("user_id")
private User user;
#ManyToOne
#JoinColumn(name = "sensor_id")
#MapsId("sensor_id")
private Sensor sensor;
private boolean owner;
private String name;
private int color;
private long creationTimestamp;
}
The controller:
...
#RequestMapping(method = RequestMethod.GET, path = "/user/{email}", produces = MediaType.APPLICATION_JSON_VALUE)
#ApiOperation(value = "Returns details for one specific user")
public User getUserByEmail(#PathVariable("email") String email) {
return userRepository.findByEmail(email).orElse(null);
}
...
The UserRepository:
public interface UserRepository extends JpaRepository<User, Integer> {
Optional<User> findByEmail(String email);
#Modifying
#Query("UPDATE User u SET u.firstName = ?2, u.lastName = ?3, u.password = ?4, u.role = ?5, u.status = ?6 WHERE u.id = ?1")
Integer updateUser(int id, String firstName, String lastName, String password, int role, int status);
}
I want to achieve, that the user endpoint shows all linked sensors with that particular user.
What I get is only an error message:
JSON mapping problem:
com.chillibits.particulatematterapi.model.db.main.User["sensorLinks"];
nested exception is
com.fasterxml.jackson.databind.JsonMappingException: Infinite
recursion (StackOverflowError) (through reference chain:
com.chillibits.particulatematterapi.model.db.main.User["sensorLinks"])
How can I fix this issue?
Thanks in advance
Marc
------------------------------------ Edit -----------------------------------
According to Abinash Ghosh's answer, I added following DTOs:
UserDto:
#Data
#NoArgsConstructor
#AllArgsConstructor
public class UserDto {
private int id;
private String firstName;
private String lastName;
private Set<LinkDto> sensorLinks;
private int role;
private int status;
private long creationTimestamp;
private long lastEditTimestamp;
}
LinkDto:
#Data
#NoArgsConstructor
#AllArgsConstructor
public class LinkDto {
private Integer id;
private SensorDto sensor;
private boolean owner;
private String name;
private int color;
private long creationTimestamp;
}
And the mapper (I realized it a bit different, but it should be the same):
public UserDto getUserByEmail(#PathVariable("email") String email) {
User user = userRepository.findByEmail(email).orElse(null);
return convertToDto(user);
}
private UserDto convertToDto(User user) {
return mapper.map(user, UserDto.class);
}
This leads to following Exception:
2020-04-13 14:22:24.383 WARN 8176 --- [nio-8080-exec-2] o.h.e.loading.internal.LoadContexts : HHH000100: Fail-safe cleanup (collections) : org.hibernate.engine.loading.internal.CollectionLoadContext#68ab57c7<rs=HikariProxyResultSet#2017009664 wrapping Result set representing update count of -1>
1) Error mapping com.chillibits.particulatematterapi.model.db.main.User to com.chillibits.particulatematterapi.model.io.UserDto
1 error] with root cause
java.lang.StackOverflowError: null
at com.mysql.cj.NativeSession.execSQL(NativeSession.java:1109) ~[mysql-connector-java-8.0.19.jar:8.0.19]
...
It's working!
This post helped: https://stackoverflow.com/a/57111004/6296634
Seems that you should not use Lombok #Data in such cases.
When User serialized for the response, all getter methods of User's fields are called.
So, User relational field sensorLinks's getter are also called to set value. This happened recursively. That's cause of infinite recursion.
It's better to not use Entity as a response. Create a DTO class for User then map User entity value into DTO then send response. Don't use any Enity class again into DTO then it will result same problem
For dynamically map one model to another you can use ModleMapper
public class UserDTO {
//Fields you want to show in response & don't use enity class
private Set<LinkDTO> sensorLinks;
}
public class LinkDTO{
//Fields you want to show in response &don't use enity class
}
public User getUserByEmail(#PathVariable("email") String email) {
User user = userRepository.findByEmail(email).orElse(null);
UserDTO userDto = merge(user,UserDTO.class)
return userDto;
}
public static <T> void merge(T source, T target) {
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
modelMapper.map(source, target);
}
Related
I have a few models that has relationship to each other within this object I am tryin to save and fetch from the database, but I can't seem to get it to work properly. The object contains a property that also contains another object. My approach was to save to all related models and for those models, I would also save this current model to them as well so it shows relation on the database table.
Below the models for the app.
#Entity
#Table(name = "daily_entry")
#Setter
#Getter
#AllArgsConstructor
#NoArgsConstructor
public class DailyEntry {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "daily_entry_id", unique = true)
private Long id;
#Column(unique = true)
private LocalDate date;
private int weight;
#ManyToOne
#JoinColumn(name="user_id")
private User user;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "daily_macros_id", unique = true)
private DailyMacros dailyMacros;
#OneToMany(mappedBy = "dailyEntry", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Exercise> exercise = new ArrayList<Exercise>();
}
#Entity
#Table(name = "daily_macros")
#Setter
#Getter
#AllArgsConstructor
#NoArgsConstructor
public class DailyMacros {
#Id
#GeneratedValue
#Column(name = "daily_macros_id")
private Long id;
private int calories;
private int protein;
private int fat;
private int carbs;
#OneToOne(mappedBy = "dailyMacros", cascade = CascadeType.ALL, orphanRemoval = true)
private DailyEntry dailyEntry;
}
#Setter
#Getter
#Entity
#AllArgsConstructor
#NoArgsConstructor
#Table(name= "exercises")
public class Exercise {
#Id
#GeneratedValue
#Column(name = "exercise_id")
private Long id;
private String name;
private int sets;
private int reps;
private int weight;
#ManyToOne
#JoinColumn(name = "daily_entry_id", unique = true)
private DailyEntry dailyEntry;
}
#AllArgsConstructor
#NoArgsConstructor
#Getter
#Setter
#Entity
#Table(name = "users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "user_id")
private Long id;
#Column(nullable = false)
private String firstName;
#Column(nullable = false)
private String lastName;
private String role = "user";
private String token;
#Column(unique = true, nullable = false)
protected String username;
#Column(unique = true ,nullable = false)
private String emailAddress;
#Column(nullable = false)
private String password;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "macros_goal_id")
private MacrosGoal macrosGoal;
#OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<DailyEntry> dailyEntry = new ArrayList<>();
}
Im tryin to save the model of DailyEntry to the database.
User is the owner of DailyEntry
DailyEntry owns MacrosGoal (#OneToOne) and Exercise (#OneToMany). How exactly can I save and fetch this model into the database?
This is what I've tried -
For my PutMapping, I get this error : Cannot call sendError() after the response has been committed
My GetMapping just returns a empty response although http status code 200
DailyEntryController file
#RequestMapping(path = "/api/v1/")
#RequiredArgsConstructor
public class DailyEntryController {
#Autowired
DailyEntryService dailyEntryService;
#GetMapping("/getDailyEntry")
public ResponseEntity<DailyEntry> getDailyEntry(#RequestParam("username") String username, #RequestParam String date) throws ResponseStatusException {
DailyEntry dailyEntry = dailyEntryService.getDailyEntry(username, date);
return new ResponseEntity<>(dailyEntry, HttpStatus.OK);
}
#PutMapping("/addDailyEntry")
public ResponseEntity<DailyEntry> addDailyEntry(#RequestBody DailyEntry dailyEntry, #RequestParam("username") String username) throws ResponseStatusException {
DailyEntry dailyEntryInfo = dailyEntryService.createDailyEntry(dailyEntry, username);
return new ResponseEntity<>( dailyEntryInfo, HttpStatus.OK);
}
}
#Slf4j
#Service
public class DailyEntryServiceImpl implements DailyEntryService {
#Autowired
DailyEntryRepository dailyEntryRepository;
#Autowired
ExerciseRepository exerciseRepository;
#Autowired
UserRepository userRepository;
#Autowired
DailyMacrosRepository dailyMacrosRepository;
#Override
public DailyEntry addExercise(Exercise exercise) {
return null;
}
#Override
public DailyEntry getDailyEntry(String username, String date) {
DailyEntry entry = null;
Optional<User> userFromDatabase = userRepository.findByUsername(username);
User user = userFromDatabase.get();
LocalDate localDate = LocalDate.parse(date);
List<DailyEntry> dailyEntryList = user.getDailyEntry();
for(DailyEntry e : dailyEntryList) {
if(e.getDate() == localDate){
entry = e;
}
}
return entry;
}
#Override
public DailyEntry createDailyEntry(DailyEntry dailyEntry, String username) {
DailyEntry entry = new DailyEntry();
// Find user
Optional<User> userFromDatabase = userRepository.findByUsername(username);
User user = userFromDatabase.get();
entry.setUser(user);
entry.setDate(LocalDate.parse(dailyEntry.getDate().toString()));
entry.setDailyMacros(dailyEntry.getDailyMacros());
entry.setWeight(dailyEntry.getWeight());
entry.setExercise(dailyEntry.getExercise());
DailyMacros dailyMacros = dailyEntry.getDailyMacros();
dailyMacros.setDailyEntry(entry);
dailyMacrosRepository.save(dailyMacros);
List<Exercise> exercise = dailyEntry.getExercise();
for (Exercise e : exercise) {
e.setDailyEntry(entry);
exerciseRepository.save(e);
}
List<DailyEntry> dailyEntryList = user.getDailyEntry();
dailyEntryList.add(entry);
user.setDailyEntry(dailyEntryList);
userRepository.save(user);
return entry;
}
I also tried with Query inside repository, but I may have done it wrong.
You should probably add #Transactional to createDailyEntry.
I don't really understand what's happening in createDailyEntry but I think it should look something like this:
#Override
#Transactional
public DailyEntry createDailyEntry(DailyEntry dailyEntry, String username) {
User user = userRepository
.findByUsername(username)
.get();
dailyEntryRepository.save(dailyEntry);
user.getDailyEntry().add(dailyEntry);
dailyEntry.setUser(User);
DailyMacros dailyMacros = dailyEntry.getDailyMacros();
dailyMacros.setDailyEntry(dailyEntry);
dailyMacrosRepository.save(dailyMacros);
dailyEntry.getExcercise()
.forEach( e -> {
e.setDailyEntry(dailyEntry);
exerciseRepository.save(e);
});
// I don't think you need this because user is already managed
// and userFromDatabase.get() will throw an exception
// if the user does not exist.
// userRepository.save(user);
return dailyEntry;
}
I think getDailyEntry doesn't work because you are using e.getDate() == localDate and it's always going to be false.
One solution is to add a method to the DailyEntryRepository that accept username and date:
interface DailyEntryRepository extends JpaRepository<DailyEntry, Long> {
#Query("select de from DailyEntry de where de.user.username = :username and de.date = :localDate")
DailyEntry findByUsernameAndDate(#Param("username") String username, #Param("localDate") LocalDate date);
}
...
#Override
public DailyEntry getDailyEntry(String username, String date) {
LocalDate localDate = LocalDate.parse(date);
return dailyEntryRepository.findByUsernameAndDate(username, localdate);
}
This solution will run a query and load only the entry you need.
But, if you need to validate the username, this should work:
#Override
public DailyEntry getDailyEntry(String username, String date) {
User user = userRepository
.findByUsername(username)
.get();
LocalDate localDate = LocalDate.parse(date);
for(DailyEntry e : user.getDailyEntry()) {
if (localDate.equals(e.getDate)){
// No need to continue the loop, you've found the entry
return entry;
}
}
// daily entry not found
return null;
}
Another thing that's missing is that in your model you are not handling the bidirectional association during convertion to JSON.
The solution is to use I end up using #JsonManagedReference and #JsonBackReference where you have the association.
I am trying to add multiple entities in a single entity, I don't know this way possible or not please refer to my below code and help
the below code are the entity tables
#Entity
#Table(name = "agent_employee")
public class AgentEmployee extends Agent implements Serializable{
private static final long serialVersionUID = 1L;
#OneToMany // unidirectional
#JoinColumn(name = "employment_id", referencedColumnName = "id")
List<Employment> employmnet = new ArrayList<Employment>();
#OneToMany(
mappedBy = "agent",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private Set<Officess> officess = new HashSet<>();
public List<Employment> getEmploymnet() {
return employmnet;
}
public void setEmploymnet(List<Employment> employmnet) {
this.employmnet = employmnet;
}
public Set<Officess> getOfficess() {
return officess;
}
public void setOfficess(Set<Officess> officess) {
this.officess = officess;
}
}
and Employment class is
#Data
#Entity
public class Employment {
#Id
#Column(nullable = false, unique = true)
private Long id;
private String empName;
private String location;
#Override
public String toString() {
return "Employment [id=" + id + ", empName=" + empName + ", location=" + location + "]";
}
}
and Offices class is
#Data
#Entity
#Table(name = "officess")
public class Officess implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String officeName;
#ManyToOne
#JoinColumn(name = "agent_emp")
private AgentEmployee agent;
}
And I have used spring boot repositories to all the respective entities
#GetMapping(path = "/add")
public #ResponseBody String addAgentEmployee() {
try {
AgentEmployee agemp = new AgentEmployee();
agemp.setFirstName("harish");
agemp.setLastName("kadsuru");
agemp.setEmail("hari**********is.net");
Employment emp1 = new Employment();
Employment emp2 = new Employment();
Employment emp3 = new Employment();
emp1.setId(501l);
emp2.setId(502l);
emp3.setId(503l);
emp1.setEmpName("junior engineer");
emp2.setEmpName("senior engineer");
emp3.setEmpName("team leader");
emp1.setLocation("bengaluru");
emp2.setLocation("mumbai");
emp3.setLocation("UAE");
List<Employment> emps = Arrays.asList(emp1, emp2, emp3);
employmentRepository.saveAll(emps);
agemp.setEmploymnet(emps);
agentEmployeeRepository.save(agemp);
return "saved";
} catch (Exception e) {
return "unable to save data due to exception";
}
}
#GetMapping("addOffice")
public #ResponseBody String addAgentEmployeeOffice() {
AgentEmployee emp;
Optional<AgentEmployee> agemp = agentEmployeeRepository.findById(27l);
if (agemp.isPresent()) {
emp = agemp.get();
}
else {
emp =new AgentEmployee();
emp.setFirstName("garish");
emp.setLastName("tumkur");
emp.setEmail("garish.kr#cyclotis.net");
}
log.info("###### {}",agemp);
Officess off1 = new Officess();
Officess off2 = new Officess();
Officess off3 = new Officess();
off1.setOfficeName("Google");
off2.setOfficeName("facebook");
off3.setOfficeName("Instagram");
Set<Officess> offices = emp.getOfficess();
offices.add(off1);
offices.add(off2);
offices.add(off3);
agentEmployeeRepository.save(emp);
log.info("######## {}", offices);
return "saved";
}
I don't think any problem with code, but I think I have a problem while saving the data. Kindly any body refer the correct way to analyse this problem.
Looks like your mapping is not correct. Also verify you have a EMPID column.
You don't need to use the #JoinTable annotation in your case.
As you are saving data you should use #PostMapping
StatusReport - removed private BigInteger EMPID; as it is used in joining
#Entity
#Table(name="statusreport")
public class StatusReport {
private BigInteger COMPLIANCEID;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private BigInteger STATUSRPTID;
private String COMMENTS;
private Date CREATEDDATE;
private BigInteger DEPARTMENT_ID;
#OneToOne
#JoinColumn(name = "EMPID")
private Employees employee;
//others methods
}
Employee - removed private BigInteger DEPARTMENT_ID; as it is used in joining
#Entity
public class Employees {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private BigInteger EMPID;
private String FIRSTNAME;
private String LASTNAME;
private Date DOB;
private String EMAIL;
#OneToOne
#JoinColumn(name = "DEPARTMENT_ID")
private Department department;
//others methods
}
I am building a simple get rest call from MySQL database, the problem is that it returns an empty object.
The call itself is takes in an email (I know this is not the best approach), here is my code:
Entity:
#Entity
public class User implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "user_id")
private int id;
private String email;
private String password;
private String firstName;
private String userName;
private String lastName;
private boolean active;
#Temporal(TemporalType.DATE)
private Date createDate;
#Temporal(TemporalType.DATE)
private Date updateDate;
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(name = "user_role", joinColumns = #JoinColumn(name = "user_id"), inverseJoinColumns = #JoinColumn(name = "role_id"))
private Collection<Role> roles;
// constructor
// get and setter
}
Repository:
public interface UserRepository extends JpaRepository<User, Long> {
// User findById (Integer Id);
#Query("SELECT u.id FROM User u where u.id = :id")
User findById(#Param("id") Integer id);
User findByEmail (String email);
}
Service:
#Service("userService")
public class UserService {
private String status, message;
private final HashMap map = new HashMap();
#Autowired
private UserRepository userRepository;
// #Autowired
// private RoleRepository roleRepository;
public User findByUserEmail (String email) {
return userRepository.findByEmail(email);
}
}
Controller:
#RestController("userControllerService")
#RequestMapping("/user/account")
public class UserController {
#Autowired
private UserService userService;
#GetMapping("/test-get/{email}")
public User jj(#PathVariable("email") String email){
return userService.findByUserEmail(email);
}
}
And my database happens to have the following data:
And here is the response I get after hitting the URL
I have no clue why my response is empty!
You cannot have the #in the URI path. Encode it with %40.
Reference: Can I use an at symbol (#) inside URLs?
Also, right way is to use as a query param as that's more of a good identifier and allows # as it parses as string
#GetMapping("/test-get")
public User jj(#RequestParam("email") String email){
return userService.findByUserEmail(email);
}
Either ways, hit as encoded url as /test-get/email=a#b.com ? or /test-get/a%40b.com for your previous code.
I am using Spring Data and I am in doubt why even after declaring foreign entities as Lazy loaded they are getting eagerly loaded for this method:
findByReportingManager_IdAndAndLevel(Long reporterManagerId, Integer level)
On logs, I can see the query as:
select userhierar0_.id as id1_28_,
userhierar0_.LEVEL as LEVEL5_28_,
userhierar0_.REPORTING_MANAGER_ID as REPORTIN9_28_,
userhierar0_.USER_ID as USER_ID10_28_
from USER_HIERARCHY userhierar0_
left outer join
USER_V3 user1_ on userhierar0_.REPORTING_MANAGER_ID=user1_.id
where user1_.id=? and userhierar0_.CUSTOMER_ID=? and userhierar0_.LEVEL=?
why extra join even if I am passing reporting manager id ?
UserHierarchy Class:
#Entity
#Table(name = "USER_HIERARCHY")
#JsonInclude(JsonInclude.Include.NON_EMPTY)
public class UserHierarchy {
#Id
#GeneratedValue
private Long id;
#ManyToOne(fetch = FetchType.LAZY) // LAZY LOADING
#JoinColumn(name = "USER_ID",referencedColumnName = "ID")
private User user;
#ManyToOne(fetch = FetchType.LAZY) //LAZY LOADING
#JoinColumn(name = "REPORTING_MANAGER_ID", referencedColumnName = "ID")
private User reportingManager;
#Column(name = "LEVEL")
private Integer level;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public User getReportingManager() {
return reportingManager;
}
public void setReportingManager(User reportingManager) {
this.reportingManager = reportingManager;
}
public Integer getLevel() {
return level;
}
public void setLevel(Integer level) {
this.level = level;
}
#Override
public String toString() {
return ReflectionToStringBuilder.toStringExclude(this, "user", "reportingManager");
}
User Entity class
#Entity
#Table(name = "USER")
public class User {
#Id
#GeneratedValue
private Long id;
#Column(name = "EMAIL")
private String email;
#Column(name = "CUSTOMER_ID")
private Long customerId;
#Column(name = "STATUS")
private String status;
// Getter and Setter
As per Spring's doc:
At query creation time you already make sure that the parsed property
is a property of the managed domain class.
So does that mean in order to make User object in "managed state" it uses join or I am wrong in the implementation ?
I stumbled across the same problem recently and it seems that there is no solution for this at the moment in Spring Data.
However I've created a ticket for it.
If you go with Criteria API or JPQL for this particular query, then it will work properly.
In my Spring project I have domain and model layer separately. Domain layer are just entities, that maps to tables in MySQL db. Models are used in service layer. I have two tables: User and Roles, that have ManyToMany relationship.
#Entity
#Table(name = "user")
public class UserEntity {
#Id
#GeneratedValue
private Long id;
private String name;
#ManyToMany(mappedBy="users", cascade=CascadeType.ALL,fetch=FetchType.EAGER)
private List<RoleEntity> roles;
//..Getters Setters
}
#Entity
#Table(name = "role")
public class RoleEntity {
#Id
#GeneratedValue
private Long id;
private String value;
#ManyToMany(cascade=CascadeType.ALL,fetch=FetchType.EAGER)
#JoinTable(name = "user_role",
joinColumns = #JoinColumn(name = "role_id") , inverseJoinColumns = #JoinColumn(name = "user_id") )
private List<UserEntity> users;
}
Model
public class User {
private Long id;
private String name;
private List<Role> roles;
}
public class Role {
private Long id;
private String value;
private List<User> users;
}
Repository Layer
#Repository
public interface UserRepo extends JpaRepository<UserEntity, Long>{
}
Service
#Service
public class UserService {
#Autowired
private UserRepo userRepo;
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
MapperFacade mapper = mapperFactory.getMapperFacade();
public void save(User user) {
userRepo.save(mapper.map(user, UserEntity.class));
}
public User getUser(Long id) {
return mapper.map(userRepo.findOne(id), User.class);
}
}
The problem is - each time I retrieve user, and update it properties (name for example), and than save back it into table, row in the join table user_role, overrides too. For example if it was
id=1, user_id=1, role_id=1
, than after update it becomes
id=2, user_id=1, role_id=1
.
#Test
public void contextLoads() {
User user = userService.getUser(1L);
user.setName("kyk");
userService.save(user);
}
It works correct without mapper, so mapper is the reason. And I can't get any workaround. Has any one faced the same problem? Will be thankful for any help.