I'm using OneToMany mapping in my SpringBoot project, while I'm having problems when updating the children along with parent update, sample code is like below:
User.java
#Table(name = "user")
#Entity
public class User {
#Id
#GeneratedValue
private Integer id;
#OneToMany(mappedBy = "groupUser", cascade = {CascadeType.ALL}, orphanRemoval = true)
private List<UserGroup> userGroups = new ArrayList<>();
}
UserGroup.java
#Table(name = "user_group")
#Entity
public class UserGroup {
#Id
#GeneratedValue
private Integer id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name="user_id")
private User groupUser;
}
SampleUsageCode.java
#Service
public class UserService {
#Autowired
private UserRepository userRepositry;
#Transactaional
public batchUpdateUsers(Collection<User> toBeSavedUsers) {
Map<Integer, User> toBeSavedIdUserMap = toBeSavedUsers.stream()
.collect(groupBy(User::getId(), toList()));
Collection<User> existingUsers = userRepositry.findByIdIn(toBeSavedIdUserMap.entrySet().stream()
.map(Map.Entry::getKey).collect(toList()));
existingUsers.forEach(user -> user.getUserGroups().add(toBeSavedIdUserMap.get(user.getId()).getUserGroups()));
}
}
To simplify the problem, Let's just assume the user groups in to-be-saved users is totally different with the existing ones in the database. The problem is when I try to add new user groups to existing users, it throws java.lang.UnsupportedOperationException. It seems the persistentBag type of userGroups in User is not editable.
I tried with just creating a new collection to store both existing and new user groups, but another error with A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance occurs when I try to save the updated users. How can I achieve this cascading-children merge requirement?
So the problem is caused by that the user groups list I prepared for the test is Unmodifiable
Related
I have a entity user with self dependency. When i Map this entity to DTO I have the problem of circular dependency. .
User.class:
public class User {
#Id
#GeneratedValue
private Long id;
#NotNull
#Column(name = "first_name")
private String firstName;
#NotNull
#Column(name = "last_name")
private String lastName;
#JsonBackReference
#ManyToMany(
private List<User> friedns_of = new ArrayList<>();
#ManyToMany(cascade = CascadeType.ALL,
mappedBy = "followers")
private List<User> friends = new ArrayList<>();
UserMapper method in UserMapper:
public static UserResponse toUser(User user) {
UserResponse userResponse = new UserResponse();
userResponse.setId(user.getId());
userResponse.setFollowers(user.getFollowers().stream().map(UserMapper::toUser).toList());
userResponse.setFollowing(user.getFollowing().stream().map(UserMapper::toUser).toList());
return userResponse;
}
When i run the method toUser() I get stackOverFlowError exception caused by the infinite circular dependency. Any advise how to solve this?
One way to resolve this is to model the 'follows' relationship as a separate entity:
#Table(name="user_followers")
public class Follows {
#Id
#GeneratedValue
private Long id;
#NotNull
#Column(name = "follower_Id")
private User follower;
#NotNull
#Column(name = "user_id")
private User user;
}
Then you could give your user two one-to-many lists of these entities:
public class User {
#Id
#GeneratedValue
private Long id;
#OneToMany(mappedBy = "user_id")
private List<Follows> followers;
#OneToMany(mappedBy = "follower_Id")
private List<Follows> following;
}
EDIT: instead of the id field in Follows you could use the user_id and follower_id as a composite primary key using #Embeddable. Omitted here for brevity. See here for more details: https://www.baeldung.com/jpa-many-to-many
Since you already have a DTO of UserResponse, you are on the right path towards a correct solution. My suggestion would be to avoid #ManyToMany on an entity level, and manage followers on a service level.
This means you will have to split relation ManyToMany join column into a separate entity, such as UserFollowsEntity with fields userId and followsUserId. Then remove followers and following lists from your User entity entirely.
Now when creating UserResponse in a service, you will have to
Select the actual user from repository – userRepository.findById(userId)
Select followers – userFollowsRepository.findByFollowsUserId(userId)
Select following – userFollowsRepository.findByUserId(userId)
It is a good practice to try and avoid bidirectional in entities relationships entirely if possible.
EDIT: This will give you two lists: followers and following. You will probably want to know their user names, so what you can do is to merge followers and following lists into one, then extract all user ids from that list. Then query user repository with a list of those IDs, and just attach the required user information to your response model.
Yes it does sound like a bit more work compared to the seeming simplicity of utilizing JPA annotations, but this is the best way to avoid circular dependency as well as decouple the Follower functionality from your user entity.
I have 2 entities, LCPUserDetails and LCPUserPrivilege. LCPUserDetails has a List class member, so a One to Many relationship. When I run my unit test I am getting this exception:
#Entity
#Table(name = "LCP_USER_DETAILS")
public class LCPUserDetails {
#OneToMany(orphanRemoval = true, cascade = {CascadeType.ALL},
mappedBy = "userDetails")
private List<LCPUserPrivilege> privileges= new ArrayList<>();
}
#Entity
#Table(name = "LCP_USER_PRIVILEGE")
public class LCPUserPrivilege {
#ManyToOne
#JoinColumn(name = "USER_ID")
private LCPUserDetails userDetails;
}
As Sheik Sena Reddy mentioned, you have to update your list of entities. If you don't use an xml file, you can check where you set your EntityManagerFactory and add a list of package that your EMF will scan to list your entities emf.setPackagesToScan(['my.package.to.scan']);.
I would like to extend the requirements mentioned in the earlier post to support deletes. We have two data model object - Organization & Department sharing a one-to-many relationship. With the below mapping I am able to read the list of departments from the organization object. I have not added the cascade ALL property to restrict adding a department when creating an organization.
How should I modify the #OneToMany annotation (and possibly #ManyToOne) to restrict inserts of department but cascade the delete operation such that all associated departments are deleted when deleting an organization object?
#Entity
#Table(name="ORGANIZATIONS")
public class Organization{
#Id
#GeneratedValue
Private long id;
#Column(unique=true)
Private String name;
#OneToMany(mappedBy = "organization", fetch = FetchType.EAGER)
private List<Department> departments;
}
#Entity
#Table(name="DEPARTMENTS")
Public class Department{
#Id
#GeneratedValue
Private long id;
#Column(unique=true)
Private String name;
#ManyToOne(fetch = FetchType.EAGER)
private Organization organization;
}
The code to delete the organization is just a line
organizationRepository.deleteById(orgId);
The test case to validate this is as below
#RunWith(SpringJUnit4ClassRunner.class)
#DataJpaTest
#Transactional
public class OrganizationRepositoryTests {
#Autowired
private OrganizationRepository organizationRepository;
#Autowired
private DepartmentRepository departmentRepository;
#Test
public void testDeleteOrganization() {
final organization organization = organizationRepository.findByName(organizationName).get(); //precondition
Department d1 = new Department();
d1.setName("d1");
d1.setorganization(organization);
Department d2 = new Department();
d2.setName("d2");
d2.setorganization(organization);
departmentRepository.save(d1);
departmentRepository.save(d2);
// assertEquals(2, organizationRepository.getOne(organization.getId()).getDepartments().size()); //this assert is failing. For some reason organizations does not have a list of departments
organizationRepository.deleteById(organization.getId());
assertFalse(organizationRepository.findByName(organizationName).isPresent());
assertEquals(0, departmentRepository.findAll().size()); //no departments should be found
}
}
See code comments on why it fails:
#RunWith(SpringJUnit4ClassRunner.class)
#DataJpaTest
#Transactional
public class OrganizationRepositoryTests {
#Autowired
private OrganizationRepository organizationRepository;
#Autowired
private DepartmentRepository departmentRepository;
#PersistenceContext
private Entitymanager em;
#Test
public void testDeleteOrganization() {
Organization organization =
organizationRepository.findByName(organizationName).get();
Department d1 = new Department();
d1.setName("d1");
d1.setOrganization(organization);
Department d2 = new Department();
d2.setName("d2");
d2.setOrganization(organization);
departmentRepository.save(d1);
departmentRepository.save(d2);
// this fails because there is no trip to the database as Organization
// (the one loaded in the first line)
// already exists in the current entityManager - and you have not
// updated its list of departments.
// uncommenting the following line will trigger a reload and prove
// this to be the case: however it is not a fix for the issue.
// em.clear();
assertEquals(2,
organizationRepository.getOne(
organization.getId()).getDepartments().size());
//similary this will execute without error with the em.clear()
//statement uncommented
//however without that Hibernate knows nothing about the cascacding
//delete as there are no departments
//associated with organisation as you have not added them to the list.
organizationRepository.deleteById(organization.getId());
assertFalse(organizationRepository.findByName(organizationName).isPresent());
assertEquals(0, departmentRepository.findAll().size());
}
}
The correct fix is to ensure that the in-memory model is always maintained correctly by encapsulating add/remove/set operations and preventing
direct access to collections.
e.g.
public class Department(){
public void setOrganisation(Organisation organisation){
this.organisation = organisation;
if(! organisation.getDepartments().contains(department)){
organisation.addDepartment(department);
}
}
}
public class Organisation(){
public List<Department> getDepartments(){
return Collections.unmodifiableList(departments);
}
public void addDepartment(Department departmenmt){
departments.add(department);
if(department.getOrganisation() != this){
department.setOrganisation(this);
}
}
}
Try this code,
#OneToMany( fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinColumn(name = "organisation_id", referencedColumnName = "id")
private List<Department> departments;
#ManyToOne(fetch = FetchType.EAGER,ascade = CascadeType.REFRESH,mappedBy = "departments")
private Organization organization;
if any issue inform
You can try to add to limit the cascade to delete operations only from Organization to department:
#OneToMany(mappedBy = "organization", fetch = FetchType.EAGER, cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<Department> departments;
Please note that if you have dependents/foreign key constraints on the department entity, then you would need to cascade the delete operations to these dependent entities as well.
You can read this guide, it explains the cascade operations nicely:
https://vladmihalcea.com/a-beginners-guide-to-jpa-and-hibernate-cascade-types/
I am working on a Restful service built with Java Spring and I have some issues modeling the data. I want to store shelfs with books. The books belong to a given category. I have a POST request to store shelfs to a mysql database (via service and CrudRepository). However I am not able to store more than one book of the same category. Here are my (simplified) entities.
A Shelf with an id and a collection of books.
#Entity
public class Shelf{
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "shelf")
private List<Book> books= new ArrayList<>();
...
}
The class Book is defined as follows:
#Entity
public class Book{
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinColumn(name = "category_id")
private Category category;
#OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JsonIgnore
private Shelf shelf;
Each book belongs to a category(e.g. thriller, fiction, etc.). Here is the category entity:
#Entity
public class Category {
private Long id;
private String name;
And finally my Controller:
#RestController
public class ShelfController {
#Autowired
private ShelfService shelfService;
#PostMapping("/shelfs")
public Shelf addShelf(#RequestBody Shelf shelf) {
return shelfService.addShelf(shelf);
}
Now here is my problem: The categories will be given and there will be no option to change these, I would therefore like to have them stored in the database or hard code them as static objects. In the Post request for new shelfs I would like to provide only the category id and make the controller find the corresponding object itself.
What I did so far was to treat the categories as a usual Entity, so whenever I added a new shelf with books having a category_id, the category was created with the given id and an empty name. But as soon as I used the same category id again, the application threw a com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry '1' for key 'PRIMARY' exception. I don't want the controller to create new category objects, but instead want it to fetch the corresponding objects from a service or a static Collection.
So my question is: How can I achieve this?
Hints for solutions/tricks to improve the design are most welcome, I am new to the topic.
One solution is to create Data Transfer Objects (DTO). Example:
class ShelfDTO {
private Long id;
private List<Long> bookIds;
}
Then use this class to receive the POST requests:
#RestController
public class ShelfController {
#Autowired private ShelfService shelfService;
#PostMapping("/shelfs")
public Shelf addShelf(#RequestBody ShelfDTO shelfDto) {
return shelfService.addShelf(shelfDto);
}
}
Then modify your ShelfService to convert the DTO to an Entity:
#Service
public class ShelfService {
#Autowired private ShelfRepository shelfRepository;
#Autowired private BookRepository bookRepository;
#Transactional
public Shelf addShelf(ShelfDTO shelfDto) {
List<Book> books = bookRepository.findAllById(shelfDto.getBookIds());
return shelfService.addShelf(new Shelf(books));
}
}
Final comment: I noticed that you have a bidirectional relationship. You are responsible for keeping it in a consistent state.
The easiest way is to create the methods addTo(shelf, book) and removeFrom(shelf, book) that encapsulate the logic of both adding the book to the list in the shelf and setting the shelf in the book.
I have 2 classes mapped with OpenJPA. One class is User and has relationship ManyToMany with the AppProfile. In the database I have the table relations USER_APP_PROFILES (ID,User_ID,App_ID).
My class Open JPA User
#Table(name = "USER_PROFILE",schema = "BPMS")
public class UserProfile {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Column(name="USER_ID")
private Integer userID;
#ManyToMany(mappedBy="listUserProfile", fetch=FetchType.EAGER)
private List<AppProfile> listAppProfile;
}
My class AppProfile
#Table(name = "APP_PROFILE",schema = "BPMS")
public class AppProfile {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Column(name="APP_ID")
private Integer appID;
#ManyToMany(fetch=FetchType.LAZY)
#JoinTable(
name="USER_APP_PROFILES",
joinColumns={#JoinColumn(name="APP_ID", referencedColumnName="APP_ID")},
inverseJoinColumns={#JoinColumn(name="USER_ID", referencedColumnName="USER_ID")})
private List<UserProfile> listUserProfile;
}
I fetch a user into the database by EntitiyManager, after I add the List AppProfile.
Example:
UserProfile userProfile //(populate with fetch in database)
AppProfile app = new App();
app.setAppID(11);
List<AppProfile> listApp = new ArrayList<AppProfile>();
listApp.add(app);
userProfile.setListAppProfile(listApp);
em.merge(userProfile)
How do I merge, if I need JPA automatic insert in table USER_APP_PROFILES:
User_App_Profile_ID : new register
UserID : 1
AppID : 11
Try moving the #JoinTable to UserProfile (changing the parameters accordingly) and adding cascade={CascadeType.ALL} to the #ManyToMany annotation.
That should do the trick.