Hibernate Unidirectional ManyToMany Update Target Constituent Relation - java

There is a unidirectional ManyToMany mapping between Role and Privilege with Role as the owning entity like so
Role
#Entity
public class Role extends BaseEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "role_id")
private Integer roleId;
#Size(max = 45)
#Column(name = "role")
private String role;
#JoinTable(name = "role_privilege", joinColumns = {
#JoinColumn(name = "role_role_id", referencedColumnName = "role_id")}, inverseJoinColumns = {
#JoinColumn(name = "privilege_privilege_id", referencedColumnName = "privilege_id")})
#ManyToMany(
cascade = {
CascadeType.DETACH,
CascadeType.MERGE,
CascadeType.REFRESH,
CascadeType.PERSIST }, fetch = FetchType.EAGER, targetEntity = Privilege.class)
private Collection<Privilege> privilegeCollection;
#Transient
private Collection<Privilege> parentPrivilegeCollection;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "roleId")
#JsonIgnore
private Collection<User> userCollection;
public Role() {
}
//getters,setter,hashcode,equals removed for brevity
}
Privilege
#Entity
public class Privilege extends BaseEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "privilege_id")
private Integer privilegeId;
#Size(max = 45)
#Column(name = "name")
private String name;
#Size(max = 150)
#Column(name = "description")
private String description;
#Size(max = 45)
#Column(name = "friendly_name")
private String friendlyName;
#JoinTable(name = "privilege_hierachy", joinColumns = {
#JoinColumn(name = "parent_privilege", referencedColumnName = "privilege_id")}, inverseJoinColumns = {
#JoinColumn(name = "child_privilege", referencedColumnName = "privilege_id")})
#ManyToMany
private Collection<Privilege> privilegeCollection;
public Privilege() {
}
}
The Problem
Whenever i set updated list of privileges in a role and update, the join table is successfully updated without removing either target or owning entity, and that is desired result. The problem is on update it also affect another self join table in Privilege called privilege_hierachy which is not what is expect.
Is it possible for hibernate to only update the Role-Privilege mant-to-many relationship and let other relation unchanged.
Spring Data Jpa is used for data persistence

It sounds like you are updating the privileges by (removing old privileges and) adding new ones. If you do that, clearly, the second join table (the self-referencing table) could be updated with new rows, based on what you are passing.
I see that for the self-referencing table, Privilege, you are not setting cascade type. It defaults to no operation, and that sounds like what you want. But my guess is based on what you said "Whenever i set updated list of privileges in a role", and that tells me you are creating new privileges for a role, instead of using existing privileges and associate them with the role.

Related

Spring Boot - combine nested resources for single API calls

Suppose you have two resources, User and Account. They are stored in separate tables but have a one-to-one relationship, and all API calls should work with them both together. For example a POST request to create a User with an Account would send this data:
{ "name" : "Joe Bloggs", "account" : { "title" : "My Account" }}
to /users rather than have multiple controllers with separate routes like users/1/account. This is because I need the User object to be just one, regardless of how it is stored internally.
Let's say I create these Entity classes
#Table(name = "user")
public class User {
#OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
#NotNull
Account account;
#Column(name = "name")
String name;
}
#Table(name = "account")
public class Account {
#OneToOne(cascade = CascadeType.ALL, optional = false, fetch = FetchType.LAZY)
#JoinColumn(name = "user_id", nullable = false)
#NotNull
User user;
#Column(name = "title")
String title;
}
The problem is when I make that POST request above, it throws an error because user_id is missing, since that's required for the join, but I cannot send the user_id because the User has not yet been created.
Is there a way to create both entities in a single API call?
Since it is a bi-directional relation, and one-to-one is a mandatory in this case, you should persist a user entity and only then persist an account. And one more thing isn't clear here is db schema. What are the pk's of entities? I coukd offer to use user.id as a single identity for both of tables. If so, entities would be as:
User(id, name), Account(user_id, title) and its entities are:
#Table(name = "account")
#Entity
public class Account {
#Id
#Column(name = "user_id", insertable = false, updatable = false)
private Long id;
#OneToOne(mappedBy = "account", fetch = FetchType.LAZY, optional = false)
#MapsId
private User user;
#Column(name = "title")
private String title;
}
#Table(name = "user")
#Entity
public class User {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;;
#OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
#JoinColumn(name = "id", referencedColumnName = "user_id")
private Account account;
#Column(name = "name")
private String name;
}
at the service layer you must save them consistently:
#Transactional
public void save(User userModel) {
Account account = user.getAccount();
user.setAccount(null);
userRepository.save(user);
account.setUser(user);
accountRepository.save(account);
}
it will be done within a single transaction. But you must save the user first, coz the user_id is a PK of the account table. #MapsId shows that user's id is used as an account's identity
Another case is when account's id is stored in the user table:
User(id, name, account_id), Account(id, title) and entities are:
#Table(name = "account")
#Entity
public class Account {
#Id
#Column(name = "id")
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#OneToOne(cascade = CascadeType.ALL, mappedBy = "account")
private User user;
#Column(name = "title")
private String title;
}
#Table(name = "user")
#Entity
public class User {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#Column(name = "account_id", insertable = false, updatable = false)
private Long accountId;
#OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
#JoinColumn(name = "account_id", referencedColumnName = "id", unique = true)
private Account account;
#Column(name = "name")
private String name;
}
in this case an Account entity will be implisitly persisted while User entity saving:
#Transactional
public void save(User userModel) {
userRepository.save(user);
}
will cause an insertion into the both of tables. Since cascade and orphane are declared, for deletion would be enough to set null for the account reference:
user.setAccount(null);
userRepository.save(user);

Automatically delete child entity using orphanRemoval Spring JPA

Currently I have the following 2 entities with a one to many relationship -
#Data
#Entity
#Table(name = "invoice_line")
#IdClass(InvoiceLinePK.class)
public class InvoiceLineEntity {
#Id
#Column(name = "line_id")
private String lineId;
#Id
#Column(name = "client_id")
private Integer clientId;
#Id
#Column(name = "invoice_id")
private String invoiceId;
#Column(name = "item_id")
private String itemId;
#Column(name = "amount")
private BigDecimal amount;
#ManyToOne
private InvoiceEntity invoice;
}
#Entity
#Table(name = "invoice")
#IdClass(InvoicePK.class)
#Data
public class InvoiceEntity {
#Id
#Column(name = "client_id")
private Integer clientId;
#Id
#Column(name = "invoice_id")
private String invoiceId;
#Column(name = "description")
private String description;
#Column(name = "txn_total_amount")
private BigDecimal txnTotalAmount;
#Column(name = "created_time", updatable = false)
#CreationTimestamp
private Date createdTime;
#Column(name = "updated_time")
#UpdateTimestamp
private Date updatedTime;
#OneToMany(fetch = FetchType.EAGER, orphanRemoval = true, mappedBy = "invoice")
private List<InvoiceLineEntity> invoiceLines;
}
In a case wherein let's say, one of my existing invoice has 3 lines and I receive a request that this particular invoice has been updated and it now has only 1 line instead of the previous 3 (so the other 2 have to be deleted), I would like to create a new Invoice object with this 1 InvoiceLineEntity and then do a invoiceRepository.save(invoice)
I am expecting that the other 2 InvoiceLine records would be automatically deleted because the orphanRemoval flag is enabled.
Can someone tell me how I can achieve this relationship by tweaking the entity relationship structure of the above 2 entities?
Your child entity must be the owner of the relationship, so that the orphans are allowed to be deleted
If you change and add mappedBy to that relation
#OneToMany(fetch = FetchType.EAGER, orphanRemoval = true, mappedBy = "bill")
private List<BillLine> billLines;
Then the BillLine must also hold a reference
public class BillLine {
#Id
#Column(name = "line_id")
private String lineId;
#Id
#Column(name = "company_id")
private Integer companyId;
#Id
#Column(name = "bill_id")
private String billId;
#Column(name = "item_id")
private String itemId;
#Column(name = "amount")
private BigDecimal amount;
#ManyToOne
private Bill bill;
}
Now it will remove the orphans
Also since you have multiple #Id on each entity. Do you know that you have to either declare a composite class or an embeddable class? Without one of those the multiple Ids are not valid.
Edit:
1) My bad mappedBy should be placed inside #OneToMany and not #JoinColumn. I have corrected it in my answer
2) Remove #JoinColumn. It is wrong in your configuration. By default #OneToMany inserts a column in the side of the #ManyToOne which holds the references to the primary table. You can override those default configurations and create a separate table for mappings but then you need the #JoinTable and I don't see any reason for that here.
This here
#JoinColumns(value = { #JoinColumn(name = "company_id", referencedColumnName = "company_id"),
#JoinColumn(name = "bill_id", referencedColumnName = "bill_id") })
definitely does not belong on #OneToMany
The following can be applied to #OneToMany but as said before I don't see any reason to do that and complicate a simple mapping which does not require a separate table.
#JoinTable(joinColumns = #JoinColumn(name = "company_id", referencedColumnName = "company_id"),
inverseJoinColumns = #JoinColumn(name = "bill_id", referencedColumnName = "bill_id") )
Check here for more information Jpa primary key

Hibernate ManyToMany realation

I have a many to many User <-> Roles relation.
The User entity looks like this:
#Entity
#Table(name = "user",
uniqueConstraints = {#UniqueConstraint(columnNames = {"username", "email"})})
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", unique = true, nullable = false)
private Long id;
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(name = "user_roles",
joinColumns = #JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "role_id", referencedColumnName = "id"))
private Set<Role> roles = new HashSet<>();
}
And the Roles entity is the following:
#Entity
#Table(name = "role")
public class Role {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id", updatable = true, nullable = false)
private Long id;
#Enumerated(EnumType.STRING)
#Column(length = 60, name = "roleName", updatable = true, nullable = false)
private RoleName roleName;
}
Everything works fine, except one thing. When I insert two users with the same role the roles table getting two records with the same role, but different IDs.
The question is, can I eliminate this behavior? Ideally the ROLES table should not contain duplicated roles.
Any advice would be appreciated. :)
I cannot find a problem in your entity mapping. The problem should be in the business logic where you try to save the user entity.
The problem you have described can happen if you set a not-yet-saved Role entity to a User. In other terms, your Role do not have an id field yet.
You need to get the Role from your persistent provider and set it to User.
If you are using spring-data, it can be like:
User user = ....
Role role = rolesRepository.findByName(roleName);
user.setRole(role);
// The persist User

Is it possible to implement (0..n) many to many (using #ManyToMany annotation) relation in JPA?

I'm looking for a way to implement (0..n) many to many relation in JPA, much possibly using #ManyToMany annotation. All examples that I found were about (1..n) relations. What I need to accomplish:
- I've got two entities: Contact and Tag. Each Contact can have 0..n Tags. Each Tag can have 0..n Contacts. From SQL point of view it would look like
this: Contact (0..n) --- (1) Contact_has_Tag (1) --- (0..n) Tag.
Code below is not working for me because JPA is linking columns with INNER JOIN.
OFC I could do this using intermediate entity and #OneToMany and #ManyToOne annotations, but I want a simpler sollution.
#Data
#Entity
public class Contact {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#JsonIgnore
private long id;
#Column(unique = true)
private String email;
// ...
#ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JsonIgnore
#JoinTable(
name = "contact_has_tag",
joinColumns = #JoinColumn(name = "contact_id", referencedColumnName = "id", updatable = false, nullable = true),
inverseJoinColumns = #JoinColumn(name = "tag_id", referencedColumnName = "id", updatable = false, nullable = true))
private List<ContactTag> contactTags = new ArrayList<ContactTag>();
}
#Entity
#Data
public class ContactTag {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#Column
private String name;
#ManyToMany(mappedBy="contactTags", fetch = FetchType.LAZY)
#JsonIgnore
private List<Contact> contacts = new ArrayList<Contact>();
}
Any ideas how it should be done?

Exception thrown when having multiple relations between two entities

I have two classes that have 2 relations between them.
User class:
#Entity
#Table(name="User")
public class User implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="Id")
private long id;
#OneToMany(cascade = CascadeType.ALL, fetch=FetchType.EAGER, mappedBy="admin")
private Set<Group> ownedGroup;
#ManyToMany(cascade = CascadeType.ALL, fetch=FetchType.EAGER, mappedBy="members")
private Set<Group> memberGroups;
//.......
}
Group class:
#Entity
#Table(name="Group")
public class Group implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="Id")
private long id;
#NotNull
#ManyToOne(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
#JoinColumn(name="Admin", referencedColumnName="Id")
private User admin;
#ManyToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
#JoinTable(name = "Group_User",
joinColumns = #JoinColumn(name = "Group"),
inverseJoinColumns = #JoinColumn(name = "User"))
private Set<User> members;
//....
}
When I want to start the App, I get this exception:
org.hibernate.AnnotationException: Use of #OneToMany or #ManyToMany targeting an unmapped class: com.myproject.model.User.memberGroups[com.querydsl.core.group.Group]
I saw other similar posts, but apparently they didn't use javax.persistence.Entity and it was the root cause of this error. Any idea how to resolve this issue ?
Group is a reserved keyword. Use backtick to escape a reserved keyword. Check out this answer.
#Table(name = "`Group`")
Also your #ManyToMany mapping is unfortunately not correct.
Update your Group entity like so :
#Entity
#Table(name="`Group`") // Change your table name
public class Group implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="Id")
private long id;
#NotNull
#ManyToOne(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
#JoinColumn(name="Admin", referencedColumnName="Id")
private User admin;
#ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinTable(
name = "Group_User",
joinColumns = #JoinColumn(name = "group_id", referencedColumnName = "id"), //Give a column name 'group_id' and map it to Group primary key id
inverseJoinColumns = #JoinColumn(name = "user_id", referencedColumnName = "id") //Give a column name 'user_id' and map it to User primary key id.
)
private Set<User> members;
...
}

Categories

Resources