I'm trying to understand how to properly delete a many to one relationship.
Let's suppose I have the following entities:
#Entity
#Table(name = "user")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
private String lastname;
}
#Entity
#Table(name = "badge")
public class Badge {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String code;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "badge_id")
private User user;
}
Now these two entites have different controllers and services. I can delete a badge in the BadgeService and delete a user in its service.
#Service
public class BadgeService {
#Autowired
BadgeRepository badgeRepository;
public void delete(int id) {
badgeRepository.deleteById(id);
}
}
#Service
public class UserService {
#Autowired
UserRepository userRepository;
public void delete(int id) {
userRepository.deleteById(id);
}
}
The problem is that if I delete a badge everthing works but If I delete a User a got an error due to the FK.
To solve the problem I came up with 2 ways but I was wondering if there is a better way to handle this kind of problem:
First Way
I simply create a method in the badge repository to delete all badges related to the specific user.
public interface BadgeRepository extends CrudRepository<Badge, Integer> {
#Modifying
#Query(value = "DELETE Badge b WHERE b.user.id = :userId")
public void deleteByUserId(#Param("userId") int userId);
}
Then I create a method in the badge service.
#Service
public class BadgeService {
#Autowired
BadgeRepository badgeRepository;
public void delete(int id) {
badgeRepository.deleteById(id);
}
#Transactional
public void deleteByUserId(int userId) {
badgeRepository.deleteByUserId(userId);
}
}
And last I simply autowire badge service in user service and call the method in the user delete.
#Service
public class UserService {
#Autowired
UserRepository userRepository;
#Autowired
BadgeService badgeService;
#Transactional
public void delete(int id) {
badgeService.deleteByUserId(id);
userRepository.deleteById(id);
}
}
Cons:
If I have multiple relationships with the User entity, I will end up autowiring a lot of services in the user service and that is bad.
Second Way
Instead of having an unidirectional relationship I create a bidirectional relationship between User and Badge.
#Entity
#Table(name = "user")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
private String lastname;
#OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<Badge> badges = new ArrayList<Badge>();
}
And when I delete a user, the cascade or simplying removing the badge from the colletion will delete all the related badges.
Cons:
Extra Query
If the collection is too big the app performances will decrease
That being said, what would you suggest? first or second approach? Maybe there is a better approach to handle this problem?
Thank you all.
By handling the badge deletion in user service, you make it clear what should happen at user deletion. Now anyone who deletes an user will delete also its relations (you can think about that as a side effect). So the first way is the best that you can do.
Just make sure to not try to delete users from badge service. That would mean circular dependency and would create confusion about who is the owner of the relation.
First approach looks good in this scenario. To keep UserService clean you can create a PreRemove lisener on User entity which will remove all associations before user deletion, and this will help to keep UserService clean.
The next thing to handle here, you need to change CASCADE
#Entity
#Table(name = "user")
#EntityListener(UderDeletionListener.class)
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
private String lastname;
#OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<Badge> badges = new ArrayList<Badge>();
}
#Entity
#Table(name = "badge")
public class Badge {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String code;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
#JoinColumn(name = "badge_id")
private User user;
}
#Component
#RequiredArgsConstructor
public class UderDeletionListener {
private final BadgeRepository badgeRepo;
#PreRemove
#Transactional(propagation = Propagation.REQUIRES_NEW)
public void onDeletion(final User user) {
badgeRepo.deleteByUserId(user.getId());
}
}
In this Scenario its better to archive your badge rather then direct delete.
#Entity
#Table(name = "badge")
public class Badge {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String code;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "badge_id")
private User user;
#Column(name = "archived")
private boolean archived;
}
Because the user entity may be associated with some other entities also.
So instead of delete you may mark batch entity as archived.
Or you can try the following...First Update the relation of batch user ,set it to null and then delete the badge.
#Service
public class BadgeService {
#Autowired
BadgeRepository badgeRepository;
public void updateBadgeUser(int id) {
Badge badge = findBadgeById(id);
badge.setUser(null);
}
public void deleteBadge(int id) {
badgeRepository.deleteById(id);
}
}
But i would suggest you to follow the archive policy.
Related
I have two entity table one category and other is subject
My category entity
#Entity
public class Category extends AuditableEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#Column(unique = true)
private String name;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "description_id")
private CategoryDescription description;
#OneToMany( mappedBy = "category", cascade = CascadeType.ALL,
orphanRemoval = true)
private List<Subject> subjects;
//getter and setter
}
And my Subject entity
#Entity
public class Subject extends AuditableEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#Column(unique = true)
private String name;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "description_id")
private SubjectDescription description;
#ManyToOne(fetch = FetchType.EAGER)
private Category category;
//Getter and Setter
}
Category Repository
#Repository
#Transactional
public interface CategoryRepository extends
JpaRepository<Category, Integer> {
}
Subject Repository
#Repository
#Transactional
public interface SubjectRepository extends JpaRepository<Subject,
Integer> {
}
Category Controller
#RestController
#RequestMapping("/category/api/")
public class CategoryResource {
private final CategoryService categoryService;
private final SubjectService subjectService;
private final TopicService topicService;
public CategoryResource(CategoryService categoryService,
SubjectService subjectService, TopicService topicService) {
this.categoryService = categoryService;
this.subjectService = subjectService;
this.topicService = topicService;
}
#PostMapping("save")
public void saveCategory(#RequestBody Category category) {
categoryService.save(category);
}
I am using postman to save data. Problem is that after saving data to the category and subject table my subject table column category_id is null i can not established a relationship between them my sql structure and data is after saving data it shows like
Category table
Subject Table
category_id is NULL how to set category id i am trying many ways but couldn't find a solution.Please help me to solve this issue
It's great that you are learning spring boot!
To answer your question since the answer is pretty simple, your code is missing category in subject.
subject.setCategory(category);
Now this might cause you an exception, so make sure you save category before you persist subject.
Cheers!
I am trying to save a JPA entity which has ManytoMany Relationship (Consumer and Product table) and OnetoOne relation with ConsumerDetailstable.Below are my entities
#JsonInclude(JsonInclude.Include.NON_NULL)
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class)
#Entity
public class Consumer {
#Id
#GeneratedValue
private Long id;
private String firstName;
private String lastName;
#JsonManagedReference
#OnToMany(mappedBy = "consumer")
private Set<ConsumerProduct> consumerProducts;
#OneToOne
private CustomerDetails consumerDetails;
}
#Entity
public class Product {
#Id
#GeneratedValue
private Long productId;
private String productCode;
#OneToMany(mappedBy = "product")
private Set<ConsumerProduct> consumerProducts;
}
#JsonInclude(JsonInclude.Include.NON_NULL)
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class)
#Entity(the join table)
public class ConsumerProduct {
#EmbeddedId
ConsumerProductKey id;
#JsonBackReference
#ManyToOne
#MapsId("id")
#JoinColumn(name = "id")
private Consumer consumer;
#ManyToOne
#MapsId("productId")
#JoinColumn(name = "product_id")
private Product product;
}
#Embeddable (forgein keys combined as embeded id)
public class ConsumerProductKey implements Serializable {
#Column(name="id")
private Long id;
#Column(name = "product_id")
private Long productId;
}
#Enitity (one to one relation table)
public class CustomerDetails {
#Id
#GeneratedValue
private Long consumerDtlId;
#OneToOne
private Consumer consumer;
private String city;
private String state;
private String country;
}
To save the entity am have just extended JPARepository and called save method
public class ConsumerRepository<Consumer> Implements JPARepository<Consumer, Long> {
#Override
public Consumer S save(Consumer entity) {
return save(entity);
};
}
I get java.lang.StackOverFlowError at save method.
Anything wrong with my Mappings ?
Question: Since this will be save operation and since Consumer Id is yet to be generated how do I assign to below Entities
ConsumerProduct.ConsumerProductKey (how do i assign Id of consumer table once it is inserted to join table ? will JPA take care of it)
CustomerDetails (how do i assign Id of consumer table once it is inserted to join table ? will JPA take care of it)
EDIT: I have updated the entity with JsonManagedReference and JsonBackedReference but still i have am facing stackoverflow error
It is due to Consumer trying to access ConsumerProduct and ConsumerProduct trying to access consumer entity and end up with StackOverflow error.
You should use #JsonManagedReference and #JsonBackReference annotation in consumer and ConsumerProduct respectivly.
I was trying to do #ManyToMany association and it worked when I tried to do relations like
User can have multiple group and one group can multiple user.. it worked ,and hibernate created custom table based on it automatically and it did its worked. later I had to add more columns to the association table so I followed a article and set the things up as per that, which worked pretty good.
I am using SpringBoot and is using SpringDataJPA
Here is my implementation :
#Entity
#Table(name = "USERS")
public class User {
#Id
#GeneratedValue
private long id;
private String username;
private String password;
private String email;
#OneToMany(mappedBy = "user")
private Set<UserGroup> userGroups = new HashSet<UserGroup>();
}
#Entity
#Table(name = "GROUPS")
public class Group {
#Id
#GeneratedValue
private long id;
private String name;
#OneToMany(mappedBy = "group")
private Set<UserGroup> userGroups = new HashSet<UserGroup>();
}
#Entity
#Table(name = "USERS_GROUPS")
public class UserGroup {
#Id
#GeneratedValue
private long id;
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "USER_ID")
private User user;
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "GROUP_ID")
private Group group;
// additional fields
private boolean activated;
private Date registeredDate;
}
User user = new User("tommy", "ymmot", "tommy#gmail.com");
Group group = new Group("Coders");
User persistedUser = userRepository.save(user);
Group persistedGroup = groupRepositry.save(group);
UserGroup userGroup = new UserGroup();
userGroup.setGroup(persistedGroup);
userGroup.setUser(persistedUser);
userGroup.setActivated(true);
userGroup.setRegisteredDate(new Date());
userGroupRepository.save(userGroup);
Now how to write a SpringData equavalent methd name for getting user's
group where the user is active ? i.e I make user active = false when
some one deletes users from a group instead of deleting the entry from
the user_group assossiation table.
Can we do it on the userRepository?
I think that you would like to have repository similar to this one:
public interface UserGroupRepository extends JpaRepository<UserGroup, Long> {
List<UserGroup> findAllByUserAndActivatedIsTrue(User user);
}
This method will give you List of all groups that this user is assigned to and is active.
If you would like to parameterize also activated field, you should instead use
List<UserGroup> findAllByUserAndActivated(User user, boolean activated);
I hope that this helps you. Good luck.
And btw, I recommend reading this:
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods
Helps a lot
I have one User class and one Note class defined as below:
#Entity
#Table(name = "notes")
public class Note {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#Column
private String title;
#Column
private String content;
#ManyToOne
#JoinColumn(name = "user_id")
private User user;
// getters and setters
}
#Entity
#Table(name="users")
public class User {
#Id
#Column
private int id;
#Column
private String name;
#Column
private String userName;
//getters and setters
}
I'm trying to create a "Note" and what I want to achieve is, as part of my request payload, I'll pass my user details, with id, as below:
[{
"title": "Mynote",
"content": "This is another note",
"user": {
"id": 4,
"name": "user_1",
"userName": "S"
}
}]
I want that, if "User" is already there it should not update the users table with new entry, and if user with given id is not there it should create a new one and link that one to the notes.
For this I used #ManyToOne(cascade = CascadeType.ALL), above private User user field in "Notes" class.
It works for one request, but for next request it throws
java.sql.SQLIntegrityConstraintViolationException:
Duplicate entry '5' for key 'PRIMARY'
This is expected as it is trying to insert same row twice.
Can I achieve this using some annotation in spring or od I have to handle this in my own code as: while creating note, I have to check if the user exists or not and if it does not exist, then create a new one, and then save the note.
(Basically, this is what I want to achieve using hibernate/spring-data-jpa, if some support is there).
For more technical details, please take a look at the link provided by #Chinthaka Dinadasa under your question.
For a quick and easier solution, think of it from a different side. Let's add notes to a user and save this user (not note).
User entity
#Entity
#Table(name = "users")
public class User {
#Id
private Integer id;
#OneToMany(mappedBy = "user", cascade = CascadeType.ALL) // !!! add "cascade"
private List<Note> notes = new ArrayList<Note>();
Note entity
#Entity
#Table(name = "note_table")
public class Note {
#Id
#GeneratedValue(strategy= GenerationType.IDENTITY)
private Integer id;
#ManyToOne
#JoinColumn(name = "user_id")
private User user;
now if you try to call twice the following method in some #Service, you'll have one user and 2 notes for this user:
#Service
public class MyService {
#Autowired
private UserRepository userRepository;
#Transactional
public void acceptNewNote() {
User user = new User();
user.setId(0L);
Note note = new Note();
user.setNotes(Arrays.asList(note));
userRepository.save(user);
where UserRepository is:
public interface UserRepository extends CrudRepository<User, Integer>
I'm using Spring with Hibernate as a JPA provider and are trying to get a #OneToMany (a contact having many phonenumbers) to save the foreign key in the phone numbers table. From my form i get a Contact object that have a list of Phone(numbers) in it. The Contact get persisted properly (Hibernate fetches an PK from the specified sequence). The list of Phone(numbers) also gets persisted with a correct PK, but there's no FK to the Contacts table.
public class Contact implements Serializable {
#OneToMany(mappedBy = "contactId", cascade = CascadeType.ALL, fetch=FetchType.EAGER)
private List<Phone> phoneList;
}
public class Phone implements Serializable {
#JoinColumn(name = "contact_id", referencedColumnName = "contact_id")
#ManyToOne
private Contact contactId;
}
#Repository("contactDao")
#Transactional(readOnly = true)
public class ContactDaoImpl implements ContactDao {
#Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
public void save(Contact c) {
em.persist(c);
em.flush();
}
}
#Controller
public class ContactController {
#RequestMapping(value = "/contact/new", method = RequestMethod.POST)
public ModelAndView newContact(Contact c) {
ModelAndView mv = new ModelAndView("contactForm");
contactDao.save(c);
mv.addObject("contact", c);
return mv;
}
}
Hopefully I got all of the relevant bits above, otherwise please let me know.
You have to manage the Java relationships yourself. For this kind of thing you need something like:
#Entity
public class Contact {
#Id
private Long id;
#OneToMany(cascade = CascadeType.PERSIST, mappedBy = "contact")
private List<Phone> phoneNumbers;
public void addPhone(PhoneNumber phone) {
if (phone != null) {
if (phoneNumbers == null) {
phoneNumbers = new ArrayList<Phone>();
}
phoneNumbers.add(phone);
phone.setContact(this);
}
}
...
}
#Entity
public class Phone {
#Id
private Long id;
#ManyToOne
private Contact contact;
...
}
In reply to Cletus' answer. I would say that it's important to have the #column annotation on the id fields, as well as all the sequence stuff. An alternative to using the mappedBy parameter of the #OneToMany annotation is to use the #JoinColumn annotation.
As a kinda aside your implementation of addPhone needs looking at. It should probably be something like.
public void addPhone(PhoneNumber phone) {
if (phone == null) {
return;
} else {
if (phoneNumbers == null) {
phoneNumbers = new ArrayList<Phone>();
}
phoneNumbers.add(phone);
phone.setContact(this);
}
}
If the Contact-Phone relationship is unidirectional, you can also replace mappedBy in #OneToMany annotation with #JoinColumn(name = "contact_id").
#Entity
public class Contact {
#Id
private Long id;
#OneToMany(cascade = CascadeType.PERSIST)
#JoinColumn(name = "contact_id")
private List<Phone> phoneNumbers;
// normal getter/setter
...
}
#Entity
public class PhoneNumber {
#Id
private Long id;
...
}
Similar in JPA #OneToMany -> Parent - Child Reference (Foreign Key)
I don't think the addPhone method is necessary, you only have to set the contact in the phone object:
phone.setContact(contact);
If you want your relationship unidirectional i.e. can navigate from Contact to Phone's only, you need to add
#JoinColumn(name = "contact_id", nullable = false)
Under your #OneToMany on your parent entity.
nullable = false IS VITAL if you want hibernate to populate the fk on the child table
Try this sample:
#Entity
public class Contact {
#Id
private Long id;
#JoinColumn(name = "contactId")
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Phone> phones;
}
#Entity
public class Phone {
#Id
private Long id;
private Long contactId;
}
In JPA this helped me
contact.getPhoneList().forEach(pl -> pl.setContact(contact));
contactRepository.save(contact);