I don't know what I am doing wrong, but apparently I am not able to create a simple OneToMany relationship with hibernate.
Here are my tables how they look in DB:
I will only show relevant part, so the question does not get to bloated.
My User Looks like
#Entity(name = "CORE_USER")
public class User extends AbstractPersistentObject {
...
#ManyToMany(cascade=CascadeType.ALL,fetch=FetchType.LAZY)
#JoinTable(name = "CORE_USER_TO_ROLE",
joinColumns = { #JoinColumn(name = "USER_ID") },
inverseJoinColumns = { #JoinColumn(name = "ROLE_ID") })
private Set<UserRole> roles = new HashSet<UserRole>();
#OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "USER_ID")
private Set<UserRoleParam> userRoleParams = new HashSet<UserRoleParam>();
...(getter and setters)
}
Here Core user role param entity
#Entity(name = "CORE_USER_ROLE_PARAM")
public class UserRoleParam extends AbstractPersistentObject {
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "ROLE_ID")
private UserRole userRole;
#Column(name = "ROLE_PARAMETER")
private String paramter;
...(getter and setter)....
}
UserRole entity
#Entity(name="CORE_USER_ROLE")
public class UserRole extends AbstractPersistentObject {
#Enumerated(EnumType.STRING)
#Column(name = "ROLE_NAME", length = 30, nullable=false, unique=true)
private UserRoleEnum roleName;
...(getter and setters)
}
When I do this in my test:
#Test
#Transactional(propagation = Propagation.NEVER)
public void saveUserRoleParametersTest() throws Exception {
// load an user which exists and check params is zero
UserDTO userDTO = userService.getUserDTO(Users.DE678_ACTIVE_ALLROLES.getObject().getId());
Assert.assertNotNull(userDTO);
Assert.assertNotNull(userDTO.getUserRoleParams());
Assert.assertEquals(0, userDTO.getUserRoleParams().size());
Map<UserRoleEnum, List<String>> userRoleParams = new HashMap<>();
userRoleParams.put(UserRoleEnum.BASIC, new ArrayList<>());
userRoleParams.get(UserRoleEnum.BASIC).add("BASIC_PARAM");
// save params to user
userService.saveUserRoleParameters(Users.DE678_ACTIVE_ALLROLES.getObject().getId(), userRoleParams);
userDTO = userService.getUserDTO(Users.DE678_ACTIVE_ALLROLES.getObject().getId());
Assert.assertNotNull(userDTO);
Assert.assertNotNull(userDTO.getUserRoleParams());
Assert.assertEquals(1, userDTO.getUserRoleParams().size());
Assert.assertEquals(1, userDTO.getUserRoleParams().get(UserRoleEnum.BASIC).size());
Assert.assertTrue(userDTO.getUserRoleParams().get(UserRoleEnum.BASIC).contains("BASIC_PARAM"));
// delete params of user
userService.saveUserRoleParameters(Users.DE678_ACTIVE_ALLROLES.getObject().getId(), null);
userDTO = userService.getUserDTO(Users.DE678_ACTIVE_ALLROLES.getObject().getId());
Assert.assertNotNull(userDTO);
Assert.assertNotNull(userDTO.getUserRoleParams());
Assert.assertEquals(0, userDTO.getUserRoleParams().size());
}
here is also the user service method which I invoke:
#Override
public void saveUserRoleParameters(final String userId, final Map<UserRoleEnum, List<String>> userRoleParams) throws UserNotFoundException {
User user = userDAO.get(userId);
if (user == null) {
throw new UserNotFoundException(userId);
}
if (userRoleParams == null || userRoleParams.isEmpty()) {
user.getUserRoleParams().clear();
} else {
List<UserRole> roles = userDAO.getUserRolesByEnums(userRoleParams.keySet());
Map<UserRoleEnum, UserRole> enumToEntity = new HashMap<>();
roles.stream().forEach(r -> enumToEntity.put(r.getRoleName(), r));
for (Entry<UserRoleEnum, List<String>> entry : userRoleParams.entrySet()) {
UserRoleParam urp = new UserRoleParam(enumToEntity.get(entry.getKey()), entry.getValue().stream().collect(Collectors.joining(";")));
user.getUserRoleParams().add(urp);
}
}
userDAO.saveOrUpdate(user);
}
The problem is that my test fails on first service method call of saveUserRoleParameters which is (EDIT: now with sql log enabled):
DEBUG [main] 12.05.17 08:46:50.264 org.hibernate.engine.jdbc.spi.SqlStatementLogger#logStatement: select user0_.id as id1_0_0_, user0_.version as version2_0_0_, user0_.ACTIVE as ACTIVE1_38_0_, user0_.APP_LANG as APP_LANG2_38_0_, user0_.DEFAULT_MODULE as DEFAULT_3_38_0_, user0_.ORGA_UNIT as ORGA_UNI4_38_0_, user0_.USER_FULL_NAME as USER_FUL5_38_0_, user0_.USER_NAME as USER_NAM6_38_0_ from CORE_USER user0_ where user0_.id=?
DEBUG [main] 12.05.17 08:46:50.270 org.hibernate.engine.jdbc.spi.SqlStatementLogger#logStatement: select userrole0_.id as id1_0_, userrole0_.version as version2_0_, userrole0_.ROLE_NAME as ROLE_NAM1_41_ from CORE_USER_ROLE userrole0_ where userrole0_.ROLE_NAME in (?)
DEBUG [main] 12.05.17 08:46:50.287 org.hibernate.engine.jdbc.spi.SqlStatementLogger#logStatement: select userrolepa0_.USER_ID as USER_ID3_0_0_, userrolepa0_.id as id1_42_0_, userrolepa0_.id as id1_0_1_, userrolepa0_.version as version2_0_1_, userrolepa0_.ROLE_PARAMETER as ROLE_PAR1_42_1_, userrolepa0_.ROLE_ID as ROLE_ID2_42_1_ from CORE_USER_ROLE_ROLE_PARAM userrolepa0_ where userrolepa0_.USER_ID=?
DEBUG [main] 12.05.17 08:46:50.290 org.hibernate.engine.jdbc.spi.SqlStatementLogger#logStatement: insert into CORE_USER_ROLE_PARAM (version, ROLE_PARAMETER, ROLE_ID, id) values (?, ?, ?, ?)
WARN [main] 12.05.17 08:46:50.291 org.hibernate.engine.jdbc.spi.SqlExceptionHelper#logExceptions: SQL Error: 23502, SQLState: 23502
ERROR [main] 12.05.17 08:46:50.291 org.hibernate.engine.jdbc.spi.SqlExceptionHelper#logExceptions: NULL nicht zulässig für Feld "USER_ID"
NULL not allowed for column "USER_ID"; SQL statement:
insert into CORE_USER_ROLE_ROLE_PARAM (version, ROLE_PARAMETER, ROLE_ID, id) values (?, ?, ?, ?) [23502-175]
WARN [main] 12.05.17 08:46:50.291 org.hibernate.engine.jdbc.spi.SqlExceptionHelper#logExceptions: SQL Error: 23502, SQLState: 23502
ERROR [main] 12.05.17 08:46:50.292 org.hibernate.engine.jdbc.spi.SqlExceptionHelper#logExceptions: NULL nicht zulässig für Feld "USER_ID"
NULL not allowed for column "USER_ID"; SQL statement:
Shouldn't jpa put the UserId where it belongs? What I want is a Unidirectional relationship where User knows about UserRoleParams but not the other way around. Like in the example here http://www.objectdb.com/api/java/jpa/annotations/relationship
EDIT#2:
I found a solution. I added following on User Entity:
#OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "USER_ID", nullable = false)
private Set<UserRoleParam> userRoleParams = new HashSet<UserRoleParam>();
Now I have the issue that clearing the set will not be persisted. My test fails as the second check if the Set is empty fails. It shows that the Parameters are still set.
The fields User.userRoleParams and UserRoleParam.user are part of a bidirectional relation. To do that you must add
mappedBy="user" to the #OneToMany side.
You cannot have those two parts as independent relations (i.e unidirectional 1-N and unidirectional N-1) reusing the same FK column ("USER_ID").
As for your documentation you linked to in the comments, the only use of unidirectional 1-N has no field on the other side (and you do have a field on the other side of the relation).
User has changed their question since this answer! Why do I bother?
Related
I have a OneToMany relationship (two tables bi-directional).
When I save the doctor's specialties, it does work, but when I remove any specialty and update the doctor, it doesn't.
Doctor
#Entity
#Table(name = "doctors")
public class Doctor implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer doctorId;
#Column(length = 20)
private String doctorName;
#Column(length = 9)
private String doctorPhoneNo;
#Column(length = 30)
private String doctorEmailAddress;
private String doctorProfileImage;
#Enumerated(EnumType.STRING)
private Status status;
#Column(length = 6)
private String doctorCmp;
#OneToMany(mappedBy = "doctor", cascade = CascadeType.ALL)
// #JsonIgnore
private Set<DoctorSpecialties> doctorSpecialties;
public Doctor() {
this.doctorSpecialties = new HashSet<>();
}
public Doctor(Integer id){
this();
this.doctorId = id;
}
// getters y setters
}
Specialty
#Entity
#Table(name = "specialties")
public class Specialty implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer specialtyId;
private String specialtyName;
#OneToMany(mappedBy = "specialty")
#JsonIgnore
private Set<DoctorSpecialties> doctorSpecialties;
public Specialty() {
}
public Specialty(Integer id) {
this.specialtyId = id;
}
// getters and setters
}
DoctorSpecialties
#Entity
#Table(name = "doctor_specialties")
public class DoctorSpecialties implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "doctor_id")
private Doctor doctor;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "specialty_id")
private Specialty specialty;
#OneToMany
#JoinColumn(name = "doctor_specialties_id")
private Set<Appointment> appointments;
#OneToMany
#JoinColumn(name = "doctor_specialties_id")
private Set<DoctorSchedule> schedules;
public DoctorSpecialties(){
}
public DoctorSpecialties(Specialty specialty, Doctor doctor){
this.specialty = specialty;
this.doctor = doctor;
}
getters / setters
}
Controller
#PostMapping(value = "/saveSpecialties/{id}")
public String saveDoctorSpecialties(#RequestParam(required = false) String[] specialtiesId,
#PathVariable Integer id, RedirectAttributes message) {
if (id != null && id > 0) {
Doctor doctor = doctorService.findOne(id);
if (doctor != null) {
// It does not work
doctor.getDoctorSpecialties().forEach(ds -> doctorSpecialtiesService.delete(ds.getId()));
doctor.getDoctorSpecialties().clear();
if (specialtiesId != null) {
for (String specialtyId : specialtiesId) {
DoctorSpecialties ds = new DoctorSpecialties();
ds.setSpecialty(new Specialty(Integer.parseInt(specialtyId)));
ds.setDoctor(doctor);
doctor.getDoctorSpecialties()
.add(ds);
}
}
doctorService.update(doctor);
message.addFlashAttribute("success", "Specialties successfully saved.");
return "redirect:/doctors/profile/{id}/specialties";
}
}
// specialtiesId = new String[]{};
message.addFlashAttribute("error", "Doctor doesn't exists");
return "redirect:/doctors/list";
}
console:
2021-10-30 21:19:13.330 DEBUG 44504 --- [nio-8080-exec-7] org.hibernate.SQL : select doctor0_.doctor_id as doctor_i1_3_0_, doctor0_.doctor_cmp as doctor_c2_3_0_, doctor0_.doctor_email_address as doctor_e3_3_0_, doctor0_.doctor_name as doctor_n4_3_0_, doctor0_.doctor_phone_no as doctor_p5_3_0_, doctor0_.doctor_profile_image as doctor_p6_3_0_, doctor0_.status as status7_3_0_ from doctors doctor0_ where doctor0_.doctor_id=?
2021-10-30 21:19:13.339 DEBUG 44504 --- [nio-8080-exec-7] org.hibernate.SQL : select doctorspec0_.doctor_id as doctor_i2_2_0_, doctorspec0_.id as id1_2_0_, doctorspec0_.id as id1_2_1_, doctorspec0_.doctor_id as doctor_i2_2_1_, doctorspec0_.specialty_id as specialt3_2_1_ from doctor_specialties doctorspec0_ where doctorspec0_.doctor_id=?
2021-10-30 21:19:13.401 DEBUG 44504 --- [nio-8080-exec-8] org.hibernate.SQL : select doctor0_.doctor_id as doctor_i1_3_0_, doctor0_.doctor_cmp as doctor_c2_3_0_, doctor0_.doctor_email_address as doctor_e3_3_0_, doctor0_.doctor_name as doctor_n4_3_0_, doctor0_.doctor_phone_no as doctor_p5_3_0_, doctor0_.doctor_profile_image as doctor_p6_3_0_, doctor0_.status as status7_3_0_ from doctors doctor0_ where doctor0_.doctor_id=?
2021-10-30 21:19:13.404 DEBUG 44504 --- [nio-8080-exec-8] org.hibernate.SQL : select specialty0_.specialty_id as specialt1_7_0_, doctorspec1_.id as id1_2_1_, doctor2_.doctor_id as doctor_i1_3_2_, specialty0_.specialty_name as specialt2_7_0_, doctorspec1_.doctor_id as doctor_i2_2_1_, doctorspec1_.specialty_id as specialt3_2_1_, doctorspec1_.specialty_id as specialt3_2_0__, doctorspec1_.id as id1_2_0__, doctor2_.doctor_cmp as doctor_c2_3_2_, doctor2_.doctor_email_address as doctor_e3_3_2_, doctor2_.doctor_name as doctor_n4_3_2_, doctor2_.doctor_phone_no as doctor_p5_3_2_, doctor2_.doctor_profile_image as doctor_p6_3_2_, doctor2_.status as status7_3_2_ from specialties specialty0_ inner join doctor_specialties doctorspec1_ on specialty0_.specialty_id=doctorspec1_.specialty_id inner join doctors doctor2_ on doctorspec1_.doctor_id=doctor2_.doctor_id where doctor2_.doctor_id=?
2021-10-30 21:19:13.565 DEBUG 44504 --- [nio-8080-exec-4] org.hibernate.SQL : select specialty0_.specialty_id as specialt1_7_0_, doctorspec1_.id as id1_2_1_, doctor2_.doctor_id as doctor_i1_3_2_, specialty0_.specialty_name as specialt2_7_0_, doctorspec1_.doctor_id as doctor_i2_2_1_, doctorspec1_.specialty_id as specialt3_2_1_, doctorspec1_.specialty_id as specialt3_2_0__, doctorspec1_.id as id1_2_0__, doctor2_.doctor_cmp as doctor_c2_3_2_, doctor2_.doctor_email_address as doctor_e3_3_2_, doctor2_.doctor_name as doctor_n4_3_2_, doctor2_.doctor_phone_no as doctor_p5_3_2_, doctor2_.doctor_profile_image as doctor_p6_3_2_, doctor2_.status as status7_3_2_ from specialties specialty0_ inner join doctor_specialties doctorspec1_ on specialty0_.specialty_id=doctorspec1_.specialty_id inner join doctors doctor2_ on doctorspec1_.doctor_id=doctor2_.doctor_id where doctor2_.doctor_id=?
There is no delete statement...
------------------------
EDIT 1
------------------------
Doctor find = doctorRepository.findById(1).get();
DoctorSpecialties ds1 = new DoctorSpecialties();
ds1.setSpecialty(specialtyRepository.findById(1).get());
ds1.setDoctor(find);
DoctorSpecialties ds2 = new DoctorSpecialties();
ds2.setSpecialty(specialtyRepository.findById(2).get());
ds2.setDoctor(find);
find.getDoctorSpecialties().add(ds1);
find.getDoctorSpecialties().add(ds2);
doctorRepository.save(find);
I have done some tests and I can not understand completely. I did and it only saves once when actually I am adding two objects.
insert into doctor_specialties (id, doctor_id, specialty_id) values (null, ?, ?)
------------------------
EDIT 2
------------------------
DoctorSpecialties (Modify the constructor)
#Entity
#Table(name = "doctor_specialties")
public class DoctorSpecialties implements Serializable {
public DoctorSpecialties(Integer specialtyId, Doctor doctor) {
this.specialty = new Specialty(specialtyId);
this.doctor = doctor;
}
}
Controller
#PostMapping(value = "/saveSpecialties/{id}")
public String saveDoctorSpecialties(#RequestParam(required = false) String[] specialtiesId,
#PathVariable Integer id, RedirectAttributes message) {
if (id != null && id > 0) {
doctorService.saveDelete(id);
Doctor doctor = doctorService.findOne(id);
if (specialtiesId != null && specialtiesId.length > 0) {
for(String idSpecialty : specialtiesId){
doctorSpecialtiesService.save(new DoctorSpecialties(Integer.parseInt(idSpecialty), doctor));
}
}
message.addFlashAttribute("success", "Specialties successfully saved.");
return "redirect:/doctors/profile/{id}/specialties";
}
message.addFlashAttribute("error", "Doctor doesn't exists");
return "redirect:/doctors/list";
}
Service
#Override
#Transactional
public void saveDelete(Integer doctorId) {
Doctor doctor = this.doctorRepository
.findById(doctorId).get();
doctor.getDoctorSpecialties().clear();
}
Console:
select doctor0_.doctor_id as doctor_i1_3_0_, doctor0_.doctor_cmp as doctor_c2_3_0_, doctor0_.doctor_email_address as doctor_e3_3_0_, doctor0_.doctor_name as doctor_n4_3_0_, doctor0_.doctor_phone_no as doctor_p5_3_0_, doctor0_.doctor_profile_image as doctor_p6_3_0_, doctor0_.status as status7_3_0_ from doctors doctor0_ where doctor0_.doctor_id=?
select doctorspec0_.doctor_id as doctor_i2_2_0_, doctorspec0_.id as id1_2_0_, doctorspec0_.id as id1_2_1_, doctorspec0_.doctor_id as doctor_i2_2_1_, doctorspec0_.specialty_id as specialt3_2_1_ from doctor_specialties doctorspec0_ where doctorspec0_.doctor_id=?
update appointments set doctor_specialties_id=null where
doctor_specialties_id=?
update doctor_schedules set
doctor_specialties_id=null where doctor_specialties_id=?
delete from doctor_specialties where id=?
For the one-many relationship in a transaction, once you get the parent (Doctor) and loop over its children (DoctorSpecialties) [In other words, once you load entire the parent and its children into Persistent state], you won't be able to delete your DoctorSpecialties directly by its repository.
You could try something like the below example to see it:
#Transactional
public void removeLine(Long doctorId, Long specId) {
Doctor doctor = this.doctorRepository // (1)
.findById(doctorId)
.orElseThrow(IllegalArgumentException::new);
this.doctorSpecialtiesRepository.deleteById(specId); // (2)
}
At (1), we load doctor into the persistent state. So here, if fetch = FetchType.EAGER, this means that it will load doctor and its all doctorSpecialties into the persistent state and this cause (2) won't give you any effect.
Otherwise, if fetch = FetchType.LAZY, it only load doctor into the persistent state, and at (2) it will be deleted successfully.
Your case was similar, although fetch = FetchType.LAZY, but you looped over the children by using forEach and that loads doctorSpecialties into the persistent state. That's why you cannot delete them.
Suggestion: Using orphanRemoval = true in your parent entity
#OneToMany(mappedBy = "doctor", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<DoctorSpecialties> dss;
and just simply clear its children in your method (In #Transactional method)
doctor.getDoctorSpecialties().clear();
Whichever side has mappedBy is owning side.
This means something like: "modifications on this side of the relation
are already Mapped By the other side of the relation
, so no need to track it here separately in an extra
table."
Check this url for more information What is the "owning side" in an ORM mapping?
I am working on a Spring JPA, microservices-based project where I have two entities, UserEntity and RegionEntity, associated via a many-to-many relationship.
Below the (simplified) code for the two entities:
#EqualsAndHashCode(of = {"id"})
#Data
#Entity
#Table(name = "users")
#Audited
public class UserEntity implements Serializable {
#Id
#Column(name = "userId", columnDefinition = "int", unique = true, nullable = false)
private Long id;
#NotAudited
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "users_regions",
joinColumns = #JoinColumn(name = "userId", columnDefinition = "int", nullable = false, updatable = false),
inverseJoinColumns = #JoinColumn(name = "regionId", columnDefinition = "varchar(50)", nullable = false, updatable = false))
private Set<RegionEntity> regions = new HashSet<>();
}
#EqualsAndHashCode(exclude = {"users"})
#Entity
#Table(name = "regions")
#Data
#Audited
public class RegionEntity {
#Id
#Column(name = "name", columnDefinition = "varchar(50)", unique = true, nullable = false)
private String name;
#ManyToMany(mappedBy = "regions", fetch = FetchType.EAGER)
private Set<UserEntity> users = new HashSet<>();
}
Now, I have a route for persisting instances of a third entity, CallEntity, that makes use of both the previous entities. It may be useful to know that CallEntity has also a many-to-many association with RegionEntity, and a many-to-one with user.
Here's in short the method invoked by the route.
public CallDTO myMethod(String userName, /* ... more stuff */) {
UserProjection userProj = userConsumer.findByUserName(userName, UserBasicProjection.class); // from another repository, retrieve a user projection
UserEntity user = userRepository.findById(user.getId()).orElse(null); // use that projection to initialise the user
Set<RegionEntity> userRegions = user != null ? user.getRegions() : null;
CallEntity newCall = new CallEntity();
/ * some actions, mainly setting values for newCall using userRegions and user... */
CallEntity call = callRepository.save(newCall);
return callMapper.toDTO(call);
}
The problem is that Hibernate automatically deletes the lines related to the user-region pair in the associated table when saving the CallEntity object. So for example, if the user has an id of 1234 and has regions A, B and C associated,
user | region
-------------
1234 | A
-------------
1234 | B
-------------
1234 | C
-------------
then all three lines will be deleted in the table after this line of code:
CallEntity call = callRepository.save(newCall);
is executed.
This is the Hibernate version currently in use:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>5.3.12.Final</version>
</dependency>
This is the console printout after execution:
Hibernate:
/* get current state package-name.UserEntity */ select
userent_.userId
from
user userent_
where
userent_.userId=?
Hibernate:
/* get current state package-name.RegionEntity */ select
regione_.name
from
region regione_
where
regione_.name=?
Hibernate:
/* insert package-name.CallEntity
*/ insert
into
call
(id, userid, regionid)
values
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate:
select
last_insert_id()
Hibernate:
/* delete collection package-name.UserEntity.regions */ delete
from
users_regions
where
userId=?
Hibernate:
/* insert collection
row package-name.CallEntity.userRegions */ insert
into
call_region
(id, name)
values
(?, ?)
I found that if I change the direction of the relationship (the owning entity being, in this case, RegionEntity) the problem disappears. However, this is by no means a viable solution, as it would impact other parts of the project.
Also, I found that a similar question was asked before on this site (ManyToMany assoicate delete join table entry), but unfortunately the answer was not satisfactory. I tried adding and using convenience methods to correctly establish the association (as the answer from the linked question suggests), but that just didn't work.
Any help would be immensely appreciated.
Thank you.
Finally, the problem was solved by wrapping the set of regions in a copy.
That is, by replacing this line:
Set userRegions = user != null ? user.getRegions() : null;
with:
Set userRegions = userxpc != null ? Set.copyOf(userxpc.getRegions()) : null;
I have an entity as below. I am curious if it is possible to create a relationship as I will be describing with the example:
I am creating 2 Person entities Michael and Julia.
I am adding Julia to Michael's friends set.
After that I am retrieving Michael as a JSON response and Julia is available in the response. But when I am retrieving Julia, her friends set is empty. I want to create the bidirectional friendship relation by saving just one side of the friendship. I would like to get Michael on Julia's friends set without doing any other operations. I think that it must be managed by Hibernate. Is it possible and how should I do it?
#ToString(exclude = "friends") // EDIT: these 2 exclusion necessary
#EqualsAndHashCode(exclude = "friends")
public class Person{
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id")
private Long id;
#Column(name = "name",unique = true)
private String name;
#JsonIgnoreProperties("friends") // EDIT: will prevent the infinite recursion
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(name = "FRIENDSHIP",
joinColumns = #JoinColumn(name = "person_id",
referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "friend_id",
referencedColumnName = "id"))
private Set<Person> friends;
Here is my service layer code for creating a friendship:
#Override
public Person addFriend(String personName, String friendName)
throws FriendshipExistsException, PersonNotFoundException {
Person person = retrieveWithName(personName);
Person friend = retrieveWithName(friendName);
if(!person.getFriends().contains(friend)){
person.getFriends().add(friend);
return repository.save(person);
}
else{
throw new FriendshipExistsException(personName, friendName);
}
}
Related Question:
N+1 query on bidirectional many to many for same entity type
Updated the source code and this version is working properly.
// Creating a graph to help hibernate to create a query with outer join.
#NamedEntityGraph(name="graph.Person.friends",
attributeNodes = #NamedAttributeNode(value = "friends"))
class Person {}
interface PersonRepository extends JpaRepository<Person, Long> {
// using the named graph, it will fetch all friends in same query
#Override
#EntityGraph(value="graph.Person.friends")
Person findOne(Long id);
}
#Override
public Person addFriend(String personName, String friendName)
throws FriendshipExistsException, PersonNotFoundException {
Person person = retrieveWithName(personName);
Person friend = retrieveWithName(friendName);
if(!person.getFriends().contains(friend)){
person.getFriends().add(friend);
friend.getFriends().add(person); // need to setup the relation
return repository.save(person); // only one save method is used, it saves friends with cascade
} else {
throw new FriendshipExistsException(personName, friendName);
}
}
If you check your hibernate logs, you will see:
Hibernate: insert into person (name, id) values (?, ?)
Hibernate: insert into person (name, id) values (?, ?)
Hibernate: insert into friendship (person_id, friend_id) values (?, ?)
Hibernate: insert into friendship (person_id, friend_id) values (?, ?)
I'm having a big problem for which I can't find an answer.
I have a OneToOne bidirectional Lazy fetched relation between two entities, but when I query one of them, it eagerly fetch the other, even though I'm explicitly saying that it's Lazy.
According to what I've found, almost all from 2010 and back, says that that is the way hibernate behaves on OneToOne relations. This is my first time using Hibernate ever but I highly doubt that is true, and if it was that it still is true. But i can't find any recent information about it. Post1, Post2, Post3 as you can see, they are not really recent. So I wanted to know if that's still the case or maybe guide me to where I can find the answer, examples, anything would help really.
Here's what I'm doing, just in case I'm doing something wrong.
Note the classes has fewer attributes than the original, but all of them are columns of the entity, no FK, PK, or something like that.
User Entity
#Entity
#Table(name = "\"User\"")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "Id", unique = true, nullable = false)
private long id;
#Column(name = "Username", nullable = false, unique = true, length = 100)
private String username;
#Valid
#OneToOne(fetch = FetchType.LAZY, mappedBy = "user", cascade = CascadeType.ALL)
#JsonBackReference
private UserProfile userProfile;
//Getters and Setters
}
UserProfile Entity
#Entity
#Table(name = "UserProfile")
public class UserProfile {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#NotBlank
#Size(max = 100)
#Column(name = "FirstName", nullable = false, unique = false, length = 100)
private String firstName;
#NotBlank
#Size(max = 100)
#Column(name = "LastName", nullable = false, unique = false, length = 100)
private String lastName;
#Valid
#OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "UserId")
#JsonManagedReference
private User user;
//Getters and Setters
}
Test controller method
#RequestMapping(value = "/test", method = RequestMethod.GET)
public ResponseEntity<Void> test(){
User user = userService.findUserByUsername("admin");
if(user.getUserProfile() == null){
return new ResponseEntity<Void>(HttpStatus.OK);
}
return new ResponseEntity<Void>(HttpStatus.CONFLICT);
}
And the UserService simply calls the UserDao method, which is
#Override
public User findByUsername(String username) {
Criteria criteria = createEntityCriteria();
criteria.add(Restrictions.eq("username", username));
return (User) criteria.uniqueResult();
}
The result of that service is always Conflict. Even though I'm calling the UserService (Which is Transactional) and the direct relation is in UserProfile. The controller is not transactional.
The log:
2016-02-29 18:50:58 DEBUG SQL::logStatement:92 -
select
this_.Id as Id1_2_0_,
this_.Username as Username5_2_0_
from
Public.[User] this_
where
this_.Username=?
Hibernate:
select
this_.Id as Id1_2_0_,
this_.Username as Username5_2_0_
from
Public.[User] this_
where
this_.Username=?
2016-02-29 18:50:58 DEBUG Loader::debugf:384 - Result set row: 0
2016-02-29 18:50:58 DEBUG Loader::getRow:1514 - Result row: EntityKey[com.portal.model.user.User#1]
2016-02-29 18:50:58 DEBUG TwoPhaseLoad::doInitializeEntity:144 - Resolving associations for [com.portal.model.user.User#1]
2016-02-29 18:50:58 DEBUG Loader::loadEntity:2186 - Loading entity: [com.portal.model.user.UserProfile#1]
2016-02-29 18:50:58 DEBUG SQL::logStatement:92 -
select
userprofil0_.id as id1_18_0_,
userprofil0_.FirstName as FirstNam5_18_0_,
userprofil0_.LastName as LastName7_18_0_,
userprofil0_.UserId as UserId10_18_0_
from
Public.UserProfile userprofil0_
where
userprofil0_.UserId=?
Hibernate:
select
userprofil0_.id as id1_18_0_,
userprofil0_.FirstName as FirstNam5_18_0_,
userprofil0_.LastName as LastName7_18_0_,
userprofil0_.UserId as UserId10_18_0_
from
Public.UserProfile userprofil0_
where
userprofil0_.UserId=?
Is there something wrong? Am I doing something wrong?
Thanks for any help you can provide! Let me know if you need more info!
#Entity
public class User{
#ElementCollection
#Enumerated(EnumType.STRING)
#CollectionTable(name = "SEC_USER_ROLES",
joinColumns =
#JoinColumn(name = "USER_ID", referencedColumnName = "ID"))
#Column(name = "ROLE_NAME")
private List<Role> roles;
[...]
}
public enum Role {
ROLE_SUPER_ADMIN,
ROLE_ADMIN,
ROLE_ARB,
ROLE_AP;
[...]
}
With this mapping, when I try do delete one ROLE, for example ROLE_ARB, it always ends up with deleting the role and inserting it once again.
DELETE FROM SEC_USER_ROLES WHERE ((USER_ID = ?) AND (ROLE_NAME = ?))
bind => [9451, ROLE_ADMIN]
INSERT INTO SEC_USER_ROLES (USER_ID, ROLE_NAME) VALUES (?, ?)
bind => [9451, ROLE_ADMIN]
I tried to solve the problem with #OrderColumn (name="USER_ID") but then the mapping of the User_id is not correct.
Any idea would be appreciated.
The Roles are represented as selectManyCheckbox
The ManagedBean prepares the entity (User)
...
List<String> selectedroles = this.getSelectedItems();
List<Role> newroles = new ArrayList<Role>();
if (selectedroles != null) {
for (String r : selectedroles) {
newroles.add(Role.valueOf(r));
}
getEntity().setRoles(newroles);
...
security.save(getEntity());
and the EJB makes updates if it is an existing Entity
EntityManager em;
...
this.em.merge(user);
So when someone deselects all (previous selected) Roles there is always one Role left in the database which is not deleted, because of the delete/insert behavior I described before.
#OrderColumn solved the problem