I have 2 entities: Role and Privilege. One role has many privileges. The entities look like this:
#Entity
public class Role {
private Integer id;
private String code;
#OneToMany(mappedBy = "role", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Privilege> privileges;
}
#Entity
public class Privilege {
private Integer id;
private String code;
#ManyToOne
#JoinColumn(name = "role_id")
private Role role;
}
The privilege table has the unique constraint U__ROLE_ID__CODE__PRIVILEGE on the role_id and code columns.
I have a REST endpoint which updates roles. The update includes also changing privileges assigned to a role:
private static void setPrivileges(Set<Privilege> existing, Set<Privilege> privileges) {
existing.clear();
existing.addAll(privileges);
}
By some reason, when I do the update of a role, Hibernate first inserts new privileges into the privilege table, and only then it removes the orphaned privileges. As a result, the update fails with the U__ROLE_ID__CODE__PRIVILEGE constraint violation in case when the new list of privileges contains at least one privilege from the old list.
Without the constraint everything works fine. However, removing the constraint does not look like a perfect solution.
Is it possible to change the order the Hibernate handles the role-privilege relationship update so that first the orphaned privileges are removed and only then the new ones are inserted?
The reproduction project is available here.
You may want to revisit the CascadeType. If you are looking to propagate deletes upon removal of the Role you will want to use CasadeType.REMOVE
Because you have it set to CascadeType.ALL you'll notice that each update/persistence attempt on Role will propagate to the Privilege. Thus conflicting with the the unique constraint on these tables.
// You can flush:
private static void setPrivileges(Set<Privilege> existing, Set<Privilege> privileges) {
existing.clear();
repository.flush(); // queues delete statements before subsequent operations
existing.addAll(privileges);
}
Related
I'm working on a Spring Boot Application with Hibernate and I'm just trying to understand the correct way to approach a OneToOne mapping when it comes to using cascade delete.
So, we have a User table and a PasswordResetToken table. A user has standard user columns: id, username, password, email.
A password reset token has an id, a FK to userId, and a string for a token.
So, my question now is: how do I correctly model this so we can properly cascade delete?
My thought process is that we have a unidirectional mapping since password reset token has a FK to user, and user does NOT have a FK to password reset token.
So I would think that we would place the #OneToOne on our PasswordResetToken class in Java and not have a reference to PasswordResetToken in our User class, but then the PasswordResetToken class will have a reference to a User object.
But, through some stackoverflowing, I found that people would have the child object (PasswordResetToken) inside the parent object (User) despite the parent object's table not having a reference to the child object's table (since the User table doesn't have a PasswordResetToken in it) which allows for adding the cascade remove to the #OneToOne annotation which means that when a User gets deleted, all children will get deleted as well.
So, which way is the right way to model this relationship?
Thanks for your time
There are many ways to solve your problem. Some are less, some are more efficient.
Bidirectional with foreign key
#Entity
public class PasswordResetToken {
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_id")
private User User;
// other fields
}
#Entity
public class User {
#OneToOne(mappedBy = "user", cascade = CascadeType.ALL,
fetch = FetchType.LAZY, optional = false)
private PasswordResetToken passwordResetToken;
// other fields
}
Bidirectional with principal/parent's primary key as foreign key
Since it's 1-1 relationship, you could use User's ID as a primary key for PasswordResetToken table.
#Entity
public class PasswordResetToken {
#OneToOne(fetch = FetchType.LAZY)
#MapsId
private User User;
// other fields
}
#Entity
public class User {
#OneToOne(mappedBy = "user", cascade = CascadeType.ALL,
fetch = FetchType.LAZY, optional = false)
private PasswordResetToken passwordResetToken;
// other fields
}
Unidirectional
If you want to have unidirectional mapping, and to have PasswordResetToken entity as part of User entity, you'll have to move the foreign key to User table, since #JoinColumn has to be applied on entity owning the foreign key.
#Entity
public class User {
#OneToOne(cascade = CascadeType.ALL,
fetch = FetchType.LAZY)
#JoinColumn("password_reset_token_id") // FK in User table
private PasswordResetToken passwordResetToken;
// other fields
}
As for performance, the most efficient is bidirectional with #MapsId. Bidirectional with #JoinColumn is less efficient, and I'm not sure about unidirectional mapping. One to one mappings are not that common in practice, and I'm not sure how often people use unidirectional mapping. Probably not at all, since the foreign key is usually on dependent side.
I don't know how big the token is, but what is wrong with storing the token in the User entity as simple column? You can abstract some parts by using an #Embeddable but really this should IMO be in the same table. If you are concerned with the amount of data fetched, you should be using DTOs to reduce the amount of data.
I'm having issues with defining a foreign key field within an entity. One specific thing that I can't find an answer to, is how to define such field but as a Long type, and not as that target entity type, and also set it up as ON DELETE CASCADE.
E.g.
#Entity
#Table(name = "user")
public class UserEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
and
#Entity
#Table(name = "address")
public class AddressEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#JoinColumn(
table = "user",
name = "user_id",
referencedColumnName = "id")
private Long userId;
}
This example works fine, but now one can't easily define this DELETE ON CASCADE for the userId field i.e. Address entity.
One specific thing that I can't find an answer to, is how to define
such field but as a Long type, and not as that target entity type, and
also set it up as ON DELETE CASCADE.
It stands to reason that you cannot find an answer, because JPA does not provide one. If you want JPA to manage relationships between entities, then you must define those relationships in the JPA way, with entities holding references to other entity objects and declaring appropriate relationship annotations.* And if you want cascading deletes in your persistence context then you definitely do want them to be managed / recognized by JPA, for any other kind of approach is likely to create problems involving the context falling out of sync with the underlying data store.
It's unclear what problem you are trying to solve by avoiding JPA-style relationship management, but I'm inclined to think that there must be a better way. For example, if you want to avoid requiring the persistence context to load the associated UserEntity whenever an AddressEntity is loaded, then you would define the relationship with a lazy fetch strategy:
#Entity
public class AddressEntity {
// ...
#OneToOne(optional = true, fetch = FetchType.LAZY)
private UserEntity user;
}
#Entity
public class UserEntity {
// ...
#OneToOne(optional = true, fetch = FetchType.LAZY, cascade = CascadeType.ALL,
mappedBy = user)
AddressType address;
}
(Do note, however, that FetchType.LAZY is a hint, not a constraint. The context might sometimes still load the user together with its address if that's convenient.)
If you want to get the associated user id from an address, then the best way to do so is to read it from the user:
// ...
public Long getUserId() {
return (user == null) ? null : user.getId();
}
That does require the UserEntity to define an accessible getId() method, but since you are using JPA field-based access, you do not need also to provide a setter, and you may give the method default access. Or you could just declare UserEntity.id such that it is directly accessible by AddressEntity.
On the other hand, if you want to provide for the user ID to be accessible without loading the user entity then instead of a method such as the above getUserId(), in addition to the relationship field you could define a persistent, read-only AddressEntity.userId field, mapped to the appropriate column. It must be read-only because the value of the id in the underlying data store will necessarily be managed via the entity relationship, so it cannot also be managed via this separate field. For example:
#Entity
public class AddressEntity {
// ...
#OneToOne(optional = true, fetch = FetchType.LAZY)
private UserEntity user;
#Column(name = "user_id", insertable = false, updatable = false, nullable = true)
public Long userId;
}
This is a brittle approach, and I do not recommend it. It will be prone to problems with the userId field falling out of sync with the user entity. That may be bearable for the usage you have in mind, but this sort of weirdness is fertile ground for future bugs.
*Side note: as far as I know or can determine, JPA does not define semantics for a #JoinColumn annotation on a non-relationship field such as in your original code. That doesn't mean that your particular persistence provider can't interpret it in a way that you characterize as "works fine", but at minimum you are on thin ice with that.
I am simply trying to perform an update of an entity. However hibernate attempts 2 SQL statements, one to perform the correct update and an unwanted second to update the ID alone to null, which causes my application to fail.
I am using Spring Data alongside Hibernate and when performing an update of an Entity, I see the expected update SQL is performed, however when running the application with SQL Server, a subsequent update is attempted which does the following:
update my_table set id=null where id=?
This fails obviously.
Cannot update identity column 'ID'.
Running the same code with H2 I do not see this second update triggered.
Any idea what might be the cause of this behaviour?
I am extending JpaRepository and using the default save().
Here is a snippet of my entity:
#Table(name = "MY_TABLE")
#Entity
public class MyEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column
private String anotherValue;
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
#JoinColumn(name="id")
private List<ChildEntity> children = new ArrayList<>();
// getters, builder, private default constructor ...
Snippet building my entity:
MyEntity.newBuilder()
.withId(id)
.withAnotherValue(valueUpdate)
.build();
Repository:
public interface MyRepository extends JpaRepository<MyEntity, Long>
Saving:
myRepository.save(myUpdatedEntity);
As i think of probable cause for this is if you associate two entities with their IDs as foreign keys then hibernate may try to update ID of parent as foreign key of other entity. Its not correct way to associate.
In a one-to-many relation add a foreign key in the many side entity, that have to reference the primary key of the one side entity class.
#Entity
public class MyEntity {
..
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
#JoinColumn(name="id", referencedColumnName = "MYENTITY_ID")
private List<ChildEntity> children = new ArrayList<>();
}
I have three tables/entities which are Event, Participant, and ParticipantEvent. ParticipantEvent is kind of like join table of many to many relationship but I have made it as an entity. And the mapping goes like this.
public class Event {
#OneToMany(mappedBy = "event", cascade=CascadeType.REMOVE)
private List<ParticipantEvent> participantEvents;
}
public class Participant {
#OneToMany(mappedBy = "participant", cascade=CascadeType.ALL)
private List<ParticipantEvent> participantEvents;
}
public class ParticipantEvent {
#ManyToOne
private Event event;
#ManyToOne
private Participant participant;
}
When I delete an event, hibernate does not trigger deletion of ParticipantEvent. It give foreign key constraint violation error until I give ParticipantEvent -> Participant cascade to ALL. This will triggers delete on ParticipantEvent fine, but also deletes data from Participant table as well yet I don't want to delete any data from Participant table.
I am lost here, I don't think ParticipantEvent DML should depend on Participant or Event.
Let me get my question straight, using the #OnDelete here will delete this and any other InventoryPreference entities if the Inventory entity is deleted? I just can't understand a thing from Hibernate's annotations reference.. so I need your help to confirm that I understood it correctly.
public class InventoryPreference {
...
#ManyToOne
#OnDelete(action = OnDeleteAction.CASCADE)
#JoinColumn(name = "inventory_id", nullable = false)
public Inventory getInventory() {
return inventory;
}
}
Do I then in the Inventory entity need to use CascadeType.ALL too to get all the InventoryPreferences deleted if the Inventory entity is deleted?
public class Inventory {
...
#OneToMany(mappedBy = "inventory", cascade = CascadeType.ALL)
public Set<InventoryPreference> getPreferenceItems() {
return preferenceItems;
}
}
If the first question is true, then I don't see the point of CascadeType.ALL. If it's not then what do each of these do and what annotations and configuration I need to specify to get the InventoryPreferences deleted when Inventory is deleted? Oh and I don't want the Inventory to be deleted if InventoryPreference gets deleted. Sorry if it's too obvious.
They do somewhat different things. #OnDelete is a schema generation instruction. It will add 'on delete cascade' to the end of the DDL generated for the foreign key (or dialect equivalent.) If you're not using hibernate to generate your database, it isn't going to do anything.
The cascade property on the #OneToMany or #ManyToOne is what's used at runtime to generate additional actual SQL statements. That's probably what you actually want, additional delete statements to remove the children, not delete cascades turned on in the database table? If what you want is for InventoryPreferences to get removed when you delete an Inventory, then you want:
#OneToMany(mappedBy = "inventory", cascade = CascadeType.REMOVE, orphanRemoval=true)
public Set<InventoryPreference> getPreferenceItems() {
return preferenceItems;
}
And of course add additional Cascade Types as appropriate to your design.