Hibernate not saving join column id - java

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.

Related

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

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.

Why isn't the foreign key field of a Many-to-One relationship being set on insert?

My Spring web application allows users to update "Employee" records to change the fields or add new "Phone" records related to this "Employee" record. However, when the "Employee" record is submitted for update after adding a new "Phone" record, it's throwing a SQL error exception.
The problem is that the "employee_id" foreign key on "Phone" table to the "Employee" table isn't set in the eventual SQL insert statement submitted to the database. However, in the "PhoneEntity" JPA entity object that is referenced by the updated/merged "EmployeeEntity" object, the property associated with the employee_id database field isn't null, it's set to the "EmployeeEnity" Object being updated/merged.
From my understanding of JPA, having the entity property associated with a database field should set it when the insert statement for the entity's record is submitted to the database, but in this case it isn't which is causing this error.
I've tried stepping through with a debugger, and I have verified that the created PhoneEntity object is a member of EmployeeEntity's phones property, and that the same PhoneEntity's employee property is set to the same EmployeeEntity object (with the same object IDs) in a bidirectional relationship.
I've also set the hibernate.show_sql=true to see the SQL statement being submitted to the database and it includes the statement (with the ellipses being more fields):
Hibernate:
insert
into
phone
(id, employee_id, ...)
values
(?, ?, ...)
Which means that it is inserting a new phone for the new PhoneEntity object.
After trying to running this insert statement it gives the SQL error "Column 'employee_id' cannot be null". However like I said before, I've checked with the debugger and the employee property is indeed set to the EmployeeEntity object.
this is a simplified example of what my code looks like:
#Entity
#Table(name = "employee")
public class EmployeeEntity implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", unique = true, nullable = false)
private Integer id;
#OneToMany(mappedBy="employee", cascade = {CascadeType.PERSIST})
private Set<PhoneEntity> phones = new HashSet<>();
...
}
#Entity
#Table(name = "phone")
public class PhoneEntity implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", unique = true, nullable = false)
private Integer id;
#ManyToOne
#JoinColumn(name = "employee_id", nullable = false)
private EmployeeEntity employee;
...
}
With tables that have the structure created by the following SQL statements.
CREATE TABLE employee (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
...
);
CREATE TABLE phone (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
employee_id INT NOT NULL,
...
FOREIGN KEY(employee_id) REFERENCES employee(id)
);
And the following is where it actually submits the updates to the entity manager to make updates to the database.
public void update(EmployeeDomain employee) {
EmployeeEntity entity = employeeDomainToEntity.transform(employee)
getEntityManager().merge(entity);
}
The EmployeeEntity and PhoneEntity objects are created by converting similar domain objects that were in turn deserialized from a http request. I'd include more of this section of the code but, as I've mentioned, I've already confirmed with my debugger that the actual entity objects being submitted to the merge are already in the form that we expected with the phones fields and employee fields being set correctly, so the end entities should be correct.
In the official JPA specification document (version 2.1) in section "3.2.7.1 Merging Detached Entity State" (page 85) we find:
For all entities Y referenced by relationships from X having the cascade element value cascade=MERGE or cascade=ALL, Y is merged recursively as Y'. For all such Y referenced by X, X' is set to reference Y'. (Note that if X is managed then X is the same object as X'.)
This explains that you are lacking cascade=MERGE for the annotation of the phones field.
As proposed in thanh ngo's answer, the aforementioned definition (or: explanation) thus translates to:
#OneToMany(mappedBy="employee", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private Set<PhoneEntity> phones = new HashSet<>();
Alternatively, you could also make use of cascade=CascadeType.ALL. However, this would also include operations such as CascadeType.REMOVE which might not always be intended.
Hope it helps.
I think the problem is that you are using merge.
The cascade type setting for the entity should be:
#OneToMany(mappedBy="employee", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private Set<PhoneEntity> phones = new HashSet<>();

org.hibernate.ObjectDeletedException: deleted object would be re-saved by cascade when collection objecs have a one to one association

I run into the "deleted object would be re-saved by cascade" problem when I try to remove a TicketLine object from a collection that belongs to the Ticket class and TicketLine has a OneToOne association to class Reservation.
Tickets defines a collection of TicketLines with the following getter
class Tickets
...
#OneToMany(targetEntity = TicketLine.class, fetch = FetchType.EAGER)
#Fetch(org.hibernate.annotations.FetchMode.SUBSELECT)
#Cascade({org.hibernate.annotations.CascadeType.ALL,
org.hibernate.annotations.CascadeType.LOCK,
org.hibernate.annotations.CascadeType.DELETE_ORPHAN,})
public List<TicketLine> getLines() {
return ticketlines;
}
....
class Reservation defines a OneToOne relationship to TicketLines as follows:
class Reservation
...
#OneToOne()
#Cascade({org.hibernate.annotations.CascadeType.SAVE_UPDATE, org.hibernate.annotations.CascadeType.DELETE_ORPHAN})
#JoinColumn(name = "resource_id")
public TicketLine getTicketLine() {
return ticketLine;
}
Adding a TicketLine object to ticket and a Reservation object to the TicketLine object with
ticket.getLines().add(line);
session.save(ticket);
Reservation res = new Reservation();
res.setTicketLine(m_ticketline);
....
session.save(res);
works as expected. A record in Reservations is being created with the tickets id the resource_id field.
When I remove a line from the collection which has an associated Reservation object I get the following error:
Save ticket failed: org.hibernate.ObjectDeletedException: deleted object would be re-saved by cascade (remove deleted object from associations): [org.myapp.hibernate.TicketLine#ff8081814a45ebb5014a45ebe4540003]
This error only comes up when there is a Reservation associated with the line.
Interestingly, a second try in a new session does not throw an exception but the reservation is not deleted!
Removing a line from the TicketLines collection can happen at many places, i.e. removing the reservation manually is not really an option.
I hope this can be managed by Hibernate and I've just done something wrong with the cascade options.
Please help.
The exception has gone with the code below. However, the problem is now that line.getReservation() always returns null, although a reservation record exists.
UPDATE: adding the mappedBy attribute
#OneToOne(mappedBy="reservation")
in Reservation solved the last problem. line.getReservation() now also works.
Sorry for bothering you.
2nd UPDATE:
The previously suggested solution worked as long as the second level cache was valid. After objects had been reloaded the NPE happened again. After some poking in terminology about owning a relationship I came to the following solution that finally works.
class Tickets defines a collection of TicketLines as before.
class TicketLine defines the relationship like so:
class TicketLine
...
private reservation;
...
#OneToOne(mappedBy="ticketline", cascade= CascadeType.ALL, fetch= FetchType.EAGER)
#JoinColumn(name="id", referencedColumnName="resource_id")
#Cascade({org.hibernate.annotations.CascadeType.SAVE_UPDATE, org.hibernate.annotations.CascadeType.DELETE_ORPHAN})
public Reservation getReservation() {
return reservation;
}
...
class Reservation defines the OneToOne relationship to TicketLines as follows:
class Reservation
...
privat TicketLine ticketline;
...
#OneToOne(fetch= FetchType.EAGER)
#JoinColumn(name = "resource_id", insertable=false, updatable=false)
public TicketLine getTicketline() {
return ticketline;
}
...
Maybe I need to explain that I've named the field resource_id so that it can also be used for additional types of one-to-one relationships in the future. But this should not play a role for now.
Adding a TicketLine object to ticket and a Reservation object to the TicketLine object now goes:
ticket.getLines().add(line);
session.save(ticket);
Reservation res = new Reservation();
/* set property values */
/* line might not yet have an id at that pit of time, but we need one! */
sesssion.save(line);
/* Instead of explicitly setting resource_id I tried to call
res.setTicketline(line);
but this will not set the resource_id as I expected
*/
res.setResource_id(line.getId());
m_ticketline.setReservation(res);
....
/* session.save(res);
res is now saved when the ticket containing the line will be saved later*/
....
session.save(ticket);
Saving the line and deleting a line with
ticket.getLines().remove(line);
session.save(ticket);
or deleting all lines (and resarvations) for a ticket along with the ticket with
session.delete(ticket);
all works as expected.
I apologize for the many changes but this was hard to resolve for me and I wanted to provide an answer that is actually working.
Thank you.

JPA / EclipseLink: Joined WHERE query does not give expected result

Now that's very confusing... I have a JPA entity Order that references an entity User. The User can be either buyer or seller of the Order.
Because both buyer and seller can enter additional information for an order, I moved that to an extra entity OrderUserData. There might or might not be a corresponding OrderUserData object, but IF one exists, the user should only be able to see the entry they created (based on USER_ID) and not the one of the other party.
The entities look like this:
#Entity
#Table(name = "T_ORDER")
public class Order {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToOne
#JoinColumn(name = "SELLER_ID")
private User seller;
#ManyToOne
#JoinColumn(name = "BUYER_ID")
private User buyer;
#OneToMany(mappedBy = "order", fetch = FetchType.EAGER)
private List<OrderUserData> userData = new ArrayList<>();
//..
}
--
#Entity
#Table(name = "T_ORDER_USERDATA")
public class OrderUserData {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToOne
#JoinColumn(name = "ORDER_ID")
private Order order;
#ManyToOne
#JoinColumn(name = "USER_ID")
private User user;
private String comment;
//...
}
( User is not very exciting, just ID and basic name fields )
Now when I'm trying to select the appropriate data to display in the website, I have a problem:
String qry = "SELECT o FROM Order o LEFT JOIN o.userData ud "
+ " WHERE (o.seller.id = :userId OR o.buyer.id = :userId)"
+ " AND ( ud.user IS NULL OR ud.user.id = :userId )";
TypedQuery<Order> query = em.createQuery(qry, Order.class);
query.setParameter("userId", userId);
Let's say I execute this, setting userId to 2:
My Database looks like this:
ORDER
=====
ID SELLER_ID BUYER_ID
1 1 2
2 2 3
3 3 1
ORDER_USERDATA
===============
ID ORDER_ID USER_ID COMMENT
1 1 1 Comment that only user 1 should see
2 1 2 Comment that only user 2 should see
But unlike you would expect, when executing the above query, both records are included in the userData list! It seems like JPA is executing two queries (despite the EAGER fetch) and ignoring the WHERE on the second one. Why is that? And what other solution than to loop through the userData list on Java level and kick out the entry that the appropriate user should not see?
There is no way to load OrderUserData objects inside an Order object using a query. Maybe you're confusing the ORM functionality, mapping rows in the database to Java objects, with the query functionality.
Mapping means 1-1 correspondence between rows and objects, hence Order objects always contain all OrderUserData objects for each OrderUserData row related to Order rows.
The fetch type is just a loading strategy, determining at which time are the related objects fetched, as soon as the containing object is loaded (EAGER) or as soon as the contained objects are accessed (LAZY).
You can obtain your list issuing a query on OrderUserData objects with the proper filters and getting Order objects from each of them, i.e.
SELECT ud FROM OrderUserData ud WHERE (ud.order.seller.id = :userId
OR ud.order.buyer.id = :userId) AND ( ud.user IS NULL OR ud.user.id =
:userId )
your query seems to work well as it selects properly Order entity. Then JPA fetch all the OrderUserData child of the selected Order : that's because oneToMany join is not filtered.
I don't think it is possible to modelize pre-filtered oneToMany with eclipseLink (like Hibernate #FILTER), so you should remove it and map orderUserDataId field only. Then you can fetch your entities in 1 query, but they will not be linked
SELECT o, ud FROM Order o, o.userData ud WHERE (o.seller.id = :userId OR o.buyer.id = :userId) AND ( ud.orderUserDataId = o.id and (ud.user IS NULL OR ud.user.id = :userId) )";
On the other hand, if the oneToMany is required by other use cases, then you can create 2 different Order entities :
1 "OrderLight" without the oneToMany
1 "OrderFull" with the oneToMany, derived from OrderLight.
While user3580357 and remigio have already given the correct answer as to why this doesn't work, might I suggest that you create a view on database level.
Something like (might need to be adapted for your needs or RDBMS):
CREATE OR REPLACE VIEW
ORDER_WITH_USERDATA
AS
SELECT o.*, oud.*
FROM ORDER o
LEFT JOIN ORDER_USERDATA oud
ON o.id = oud.order_id
This will essentially give you two different "logical" records for every order. You can then create an additional JPA entity that works on this view and do your SELECT/WHERE... without needing to (LEFT)JOIN at all.

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