Hibernate - 1:N - The parent is duplicated upon saving the child - java

GitHub repo if needed, Maven web project (pom.xml), SQL script under resources directory.
I'm aware this is my fault, the problem is I haven't been able to fix it in the entire day, its likely something simple and over my head, also please ignore the relations regarding the table names and columns, it's a sample project to show the problem.
Expected:
Store the new child along its parent relation (the child has a column for it), without storing the parent again.
Error:
CascadeType.ALL causes the parent to duplicate, but attempting to remove it to use the other types throws: java.sql.SQLIntegrityConstraintViolationException: Column 'user_id' cannot be null
Column 'user_id' is the name of the column in the child table that stores the parent relation.
I will skip some annotations among other things so this doesn't become a wall of code
User entity
private Long id;
private String name;
#OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = {
CascadeType.DETACH, CascadeType.MERGE, CascadeType.REFRESH})
private List<Username> usernameList = new ArrayList<>();
Username entity
private Long id;
private String username;
#ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinColumn(name = "user_id")
private User user;
Again, using anything but CascadeType.All throws an error for some reason
UsernameDAO (this is the method being used to store the child, username, which duplicates the parent, user)
Session session = factory.getCurrentSession();
session.save(username);
UsernameService
usernameDAO.save(username);

I believe the way you are assignment the object User in the object Username is incorrect. If you are doing something like this:
User user = new User();
user.set(... // set your attributes
Username username = new Username();
username.set(user);
Now if you save the username, hibernate will create a entry in the database for user because the way you have created user is by using the new keyword and this will make a new entry in the db.
If you don't want to create a new entry for the user, then you will have to load the entity from the database, so you should add a new method in your service class User which will return you a user given a user id.
e.g:
User user = userService.getUser(10);
Username username = new Username();
username.set(user);
Now when you will save username, this will not create a new entry in the table User. This is the way hibernate works. We have to load the entity, then do our operation on it and save it. The new keyword will create a new entry even if the id of the entity (primary key in db) is the same.

Please let me know if it's wrong to answer one's own question, so I just remove this.
The problem was in the front-end, I had the following in my Spring Form:
<form:select path="user" items="${listUsers}" />
When it should of been
<form:select path="user.id" items="${listUsers}" itemValue="id"/>
1) If itemValue is not specified Spring will take the value from toString.
2) Use the id field (binded entity) as the itemValue and the relationship field id as the path
3) You don't necessarily have to remove ClassType.PERSIST in order to avoid duplicates, as long as your entities are binded by their unique identifier (like the id), otherwise hibernate will see it as a new entry.

Related

Hibernate not saving join column id

I have a ManyToOne relationship set up. Each team can have multiple users. I'm using CascadeType.PERSIST in Team.java as I don't want user accounts deleted when team gets removed or vice versa. Is the cascade type correct for my use case?
The join column in User.java table team_id stays null after saving a Team and adding a user to this team in the controller.
Team.java
#OneToMany(mappedBy = "memberAtTeam", cascade = CascadeType.PERSIST)
private List<User> usersInTeam;
User.java
#ManyToOne
#JoinColumn(name = "team_id")
private Team memberAtTeam;
TeamController.java
Team teamInvitedTo = invite.getTeamInvitedTo(); //Returns a Team object. Works.
teamInvitedTo.getUsersInTeam().add(invitedUser); //invitedUser is a User.java object
teamRepository.save(teamInvitedTo); //Doesn't set team_id, stays null in DB
teamInviteRepository.delete(invite); //Deletes used invite, this works.
The memberAtTeam field of invitedUser needs to point to usersInTeam when you add it to the usersInTeam collection of teamInvitedTo. Or else the foreign key column value of usersInTeam will not point to teamInvitedTo when you save it, instead it will remain null.
Team teamInvitedTo = invite.getTeamInvitedTo(); //Returns a Team object. Works.
invitedUser.setMemberAtTeam(teamInvitedTo);
teamInvitedTo.getUsersInTeam().add(invitedUser); //invitedUser is a User.java object
teamRepository.save(teamInvitedTo); //Doesn't set team_id, stays null in DB
teamInviteRepository.delete(invite); //Deletes used invite, this works.

Lazy loading collection of objects for insert with HQL

I need to load a collection of objects from DB via HQL with the purpose of using those objects only for setting an association in some new records.
Consider the following: I need to send an email to all students with nr of credits > 50 and I need to keep a trace of all the recipients included in the transmission.
First select the students:
Query query = sessionFactory.getCurrentSession().createQuery("from Student student left join fetch student.scores where student.credits>50");
List<Student> students = query.list();
this will return a list of students with all columns (non association attributes like first name, last name, date of birth...) loaded for each record.
The problem is that when I have a lot of data, the above query is very slow and occupies a lot of memory due to a large amount of useless data being transferred from db to app server.
I can't make all attributes lazy loaded directly in the entity as this will kill other areas of the application.
What I need is a way of loading the above collection with only the ids fetched as I am using the objects just to set them for some new objects in an association. I know this can be done for OneToMany associations easily but how can I do it for a direct queried collection?
List<Recipient> emailRecipients = new ArrayList<>();
for(Student student: students){
Recipient rec = new Recipient();
//this is the only usage of the student object
rec.setStudent(student);
...
set other properties for the recipient
sessionFactory.getCurrentSession().save(rec);
}
Inside the recipient, the Student object is setup like this:
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "STD_ID", nullable = false)
private Student student;
As you can see, only the id of the Student object is needed, but I don't know how to do it.
You can use one Hibernate specific trick, which allows you to set the FK association even if you don't provide an actual managed entity. Let's say we have a studentId, we can simply set:
Student student = new Student();
student.setId(studentId);
rec.setStudent(student);
This is not supported by standard JPA, but it works with Hibernate. Just make sure you don't have any cascade propagation from child to parent (which you should not have anyway).
In the end I've loaded the studentId only and modified the mapping inside the recipient to include the studentId as well:
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "STD_ID", insertable=false, updatable = false)
private Student student;
#Column(name = "STD_ID", nullable = false)
private Long studentId;
now I can simply set the studentId and the performance is much better.

ElementCollection with same collectiontable

I want to store emailIds that needs to be notified when a change happens into a table.
I only have one table with columns ownerid and emailid. I want to store emailid in separate rows, rather than a comma separated list. I don't need a owner table, because i dont have any other information for the owner. I just want to save the emailids, but into separate rows.
how do i do that in hibernate.
I tried using something like this
#Table(name = "OwnerEmailIds")
#Entity
public class OwnerEmailIds implements Serializable {
private static final long serialVersionUID = 5906661729869048121L;
#Column(name = "OwnerID", nullable = false)
private String ownerId;
#ElementCollection(fetch=FetchType.EAGER) // must use eager as we close the session before returning
#CollectionTable(name="OwnerEmailIds", joinColumns=#JoinColumn(name="vendorId"))
#Column(name = "emailId")
private Set<String> EmailIds = new HashSet<String>();
But problem here is my collection table is same as the entity table.
And as my emailIds column in not null, when i try to save the object it fails with sql error stating emailid column can't be null.
To be able to map an entity to a table, Hibernate requires a unique id property, that maps to a unique (primary) key on the table.
As you have stated that you don't have this and if this is the only table your application is using, then Hibernate might not be the best choice.
You could still get the required information through Hibernate from your database by executing a native SQL query:
SQLQuery q = session.createSQLQuery("select OwnerID from OwnerEmailIds where emailId = :email");
q.setScalar("OwnerID", StandardBasicTypes.LONG);
q.setParameter("email", emailId)
List<Long> ids = (List<Long>) q.list();
Storing the ids could also be done with a native query, but here you would have to take into account data that is already there, so it is a little harder.
I think what you are asking for is to put collection in multiple rows and repeat other values, in you case email list will be inserted and owner_id will repeated for it's emails.
Hibernate does not provide any configuration for that. You have to do it manually.
You can do what you are looking for using the following structure and code. You must have a primary key, here you can make your email id as primary key or you can add an autoincrement integer id.
#Table(name = "OwnerEmailIds")
#Entity
public class OwnerEmailIds implements Serializable {
private static final long serialVersionUID = 5906661729869048121L;
#Column(name = "OwnerID", nullable = false)
private String ownerId;
#Id
#Column(name = "emailId")
private String emailId;
//setter and getters
}
Then you iterate through the list of email ids and create the below object and then save them individually. You can not create a collection object and configure hibernate to store them in the current table in multiple rows.
//loop throught the list of email ids
OwnerEmailIds oei = new OwnerEmailIds();
oei.setOwnerId("first last name");
oei.setEmailId("some#somedomain.com");
session.save(oei);
If you make emailId as primary key then you will have to make sure that no two owners has same email id. If that is the case then you have to add another auto_increment primary key.

Delete a referenced/OneToMany relation instead of "Nulling" the column

We have two entities:
public class User {
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinColumn(name = "user_id", insertable = true, updatable = true)
#ElementList(name = "tabledata")
public List<MyUserTableData> tableData = new ArrayList<MyUserTableData>();
}
public class MyUserTableData {
public Long user_id;
}
The action that I do is that I remove an entry from u.tableData and then call the EntityManager to merge(u).
OpenJPA will remove the entry from the User object by setting the corresponding record in the MyUserTableData with a user_id = "null".
But what I want is that if the entry is deleted from the User, it should also delete the record from the MyUserTableData and not just NULL the column user_id.
How can I force OpenJPA to delete the OneToMany related entry instead of putting a null in the column?
I will not accept answers that do asume that my database schema is bad :) The table MyUserTableData is basically a foreign key table that connects the user to another entity but holds some more information then just a foreign key, it adds some meta data to the foreign key that neither belong to the user nor to the other entity.
Thanks!
Sebastian
I was able to resolve my issue:
http://openjpa.apache.org/builds/1.0.4/apache-openjpa-1.0.4/docs/manual/manual.html#dependent
#ElementDependent => does exactly what I want.

strange sql behavior when do deleting relation for many-to-many in JPA

This is a question about Hibernate's generated sql about deleting one relationship under many-to-many mapping, not 'cascade' problem.
I use JPA 2 and hibernate as its implementation.
I have two models, User and Role. One user can have many role, and one role can have many users, so they are many-to-many mapping:
#Entity
class User{
#Id Long id;
#ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.REFRESH)
#JoinTable(name = "user_role", inverseJoinColumns = #JoinColumn(name = "role_id"),
joinColumns = #JoinColumn(name = "user_id"))
private List<Role> roles;
}
#Entity
class Role{
#Id Long id;
#ManyToMany(fetch = FetchType.LAZY,cascade = CascadeType.REFRESH, mappedBy = "roles")
private List<User> users;
}
and the mapping works well , hibernate auto create three tables for this mapping
table user
table role
table user_role
Now here is the problem, what I want is just remove one role from one user (not remove a user or a role, just one relation between one user and one role, means only need remove one record from the table user_role). Here is the code I tried:
public void removeOneRoleFromUser(long userId, long roleId){
User user = userService.getById(userId);
Role role = roleService.getById(roleId);
user.getRoles().remove(role); //here
userService.update(user);
}
when I execute this code it work, the role was removed the from the user indeed. But when I check the sql which hibernate generated for it, it's not what I expected, The hibernate generated sql is:
delete from user_role where user_id = {userId}
insert into user_role values({user_id}, {role_id_not_removed})
...
insert into user_role values({user_id}, {another_role_id_not_removed})
So for deleting one role from one user, hibernate first delete all roles from the user, then add those role which should not be removed back to the user one by one.
And what I expect is just one sql sentence archive it:
delete from user_role where user_id = {userId} and role_id = {role_id}
I know there is some other ways I can archive this like introducing another entity UserRoleMapping which mapping to the table user_role, then directly remove one UserRoleMapping instance will remove one role from one user; but I want to know is there any solution I can get the expect with the current solution.
I've not checked that this explanation is true, but it has good points.
Without any index column, a List is in fact a bag: no order, and duplicates allowed. So Hibernate considers it possible that you have the same role twice in the list of roles of a user.
So issuing delete from user_role where user_id = ? and role_id = ? is not possible because it would potentially remove several roles instead of just the one you removed from the list.
Try adding an index column, or using a Set<Role> instead of a List<Role>.

Categories

Resources