Springboot PUT request fails when updating nested Json object - java

I have a customer object and inside that customer object i have a login object which contains username and password. When i do a POST the request works fine however when i try to do a PUT request it fails. It fails because it says Duplicate entry on the username.
I would like to be able to update the customer details without having to change the username. How can i achieve this.
This is my code :
UserLogin Entity :
#Entity
#Table(name = "Customer",
uniqueConstraints =
{
#UniqueConstraint(columnNames = "email"),
#UniqueConstraint(columnNames = "id"),
#UniqueConstraint(columnNames = "phoneNumber")
}
)
public class Customer implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int customerNumber;
#OneToOne(cascade= CascadeType.ALL)
#JoinColumn(name = "loginCredentialsID")
private UserLogin userlogin;
private String phoneNumber;
private String email;
private String physicalAddress;
private String country;
... getters and setters
}
UserLogin Entity :
#Entity
#Table(name = "UserLogin",
uniqueConstraints =
{
#UniqueConstraint(columnNames = "userName")
})
public class UserLogin implements Serializable, UserDetails {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int loginCredentialsID;
private String username;
private String password;
... getters and setters
}
CustomerService Class :
public Response updatCustomeretails(int id,Customer customer) {
customer.setCustomerNumber(id);
if( customer== null ){
throw new ResourceNotFoundException("Empty", "Missing Data");
}else {
customerRepository.save(customer);
return new Response(" Customer Updated Successfully","Thank you ");
}

When using Sping data JPA to update you should use save which you correctly did when saving on this line customerRepository.save(customer);. However when persisting data to a database in a PUT request JPA uses the keys within your entity mappings to be able to update the proper record.
So in your case you get that error when JPA tries to save a new record rather than an update to an existing record. Your intent is to update but I suspect your keys are missing or they are not properly defined so JPA tries to go and save a new record instead of updating.
So when you do the update(PUT) make sure the object you are passing has the same keys as the one you want to update.

Related

Save successful but Update failed - Many to Many Spring JPA with extra column using Jointable

Am getting an error while updating the existing entity with below approach using PostRepository save method.
Here are my objects,
#Entity
public class Post {
#Id
private String postId;
private String postName;
#OneToMany(mappedBy = "Post", cascade = CascadeType.ALL)
private Collection<PostTag> postTags = new HashSet<PostTag>();
}
#Entity
public class Tag {
#Id
private String tagId;
private String tagName;
#OneToMany(mappedBy = "tag", cascade = CascadeType.ALL)
#JsonIgnore
private Collection<PostTag> postTags = new HashSet<PostTag>();
}
#Entity
public class PostTag {
#EmbeddedId
private PostTagId postTagId = new PostTagId();
#ManyToOne
#MapsId("postId")
#JoinColumn(name = "post_Id")
#JsonIgnore
private Post post;
#ManyToOne
#MapsId("tagId")
#JoinColumn(name = "tag_Id")
private Tag tag;
//extra columns ommited
}
#Embeddable
public class PostTagId implements Serializable {
private String postId;
private String tagId;
//equals & hashcode ommited
}
I try to save the post as in the form of below POST json,
{
"postId": "post-001",
"postName": "post-001",
"postTags": [
{
"tag": {
"tagId": "tag-001",
"tagName": "tag-001"
}
}
]
}
Service implementation looks as below,
public Post save(Post post){
Post newPost = new Post();
newPost.setPostName(Post.getPostName());
newPost.setPostId(Post.getPostId());
for (PostTag posttag : post.getPostTags()) {
PostTag newPostTag = new PostTag();
Tag dbTag = tagRepo.getById(posttag.getTag().getTagId());
if(dbTag == null){
Tag newtag = new Tag();
newtag.setTagId(posttag.getTag().getTagId());
newtag.setTagName(posttag.getTag().getTagName());
tagRepo.save(newTag);
dbTag = newTag;
}
newPostTag.setTag(dbTag);
newPostTag.setPost(newPost);
newPost.getPostTags().add(newPostTag);
}
return PostRepository.save(newPost);
}
The above code works first time and creates record in POST & TAG & POSTTAG.
But when i run the save again with same input it complains below error,
javax.persistence.EntityExistsException: A different object with the same identifier value was already associated with the session : [PostTagId#c03f34a0]
Clearly it says there is already an obj which is there in PostId + TagId combination,
But how can i just do update or merge in that case for only PostTag entity extra fields if there is already same combination available?
Please help.
There was already same question on next link, so it maybe helps you: answer

Problem with saving foreign key with #OneToOne annotation. Saving as null

I have two entities (Project, OtherData) with one abstract entity. I'm using MySQL and Quarkus framework.
Problem: When I try to save Project entity field project_id remains null.
Table schemas:
On next picture there is shown, fk constraint in "project_other_data" table:
Abstract Entity:
#MappedSuperclass
public class AbstractEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
protected Long id;
// getters and setters
}
Project Entity
#Entity
#Table(name = "projects")
public class Project extends AbstractEntity {
#NotNull
#Column(name = "name")
private String name;
#NotNull
#Column(name = "surname")
private String surname;
#Column(name = "date_create")
#JsonbDateFormat(value = "yyyy-MM-dd")
private LocalDate dateCreate;
#Column(name = "date_update")
#JsonbDateFormat(value = "yyyy-MM-dd")
private LocalDate dateUpdate;
#OneToOne(mappedBy = "project", cascade = CascadeType.ALL)
private OtherData otherData;
// getters and setters
}
OtherData Entity
#Entity
#Table(name = "project_other_data")
public class OtherData extends AbstractEntity {
#OneToOne
#JoinColumn(name = "project_id")
private Project project;
#Column(name = "days_in_year")
private Integer daysInYear;
#Column(name = "holidays_in_year")
private Integer holidaysInYear;
#Column(name = "weeks_in_year")
private Integer weeksInYear;
#Column(name = "free_saturdays")
private Integer freeSaturdays;
#Column(name = "downtime_coefficient")
private BigDecimal downtimeCoefficient;
#Column(name = "changes")
private Integer changes;
// getters and setters
}
Saving entities with code:
#Path("projects")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
public class ProjectRest {
#Inject
ProjectService projectService;
#POST
public Response saveProject(Project project) {
return Response.ok(projectService.saveProject(project)).build();
}
}
#RequestScoped
#Transactional
public class ProjectService {
#Inject
EntityManager entityManager;
public Project saveProject(Project project) {
if (project.getId() == null) {
entityManager.persist(project);
} else {
entityManager.merge(project);
}
return project;
}
}
I was able to reproduce the problem by POSTing a new Project with an embedded OtherData. The body I used for the POST:
{
"name": "John",
"surname": "Doe",
"otherData": {}
}
Point is: the database entity is also used as DTO. Thus, the field project in otherData for the request body is set to null (since no Project is passed along this would be a recursive infinite definition).
During processing the entity from the rest controller to the service to the repository, the project of otherData is never set. A quick fix is to modify ProjectService::saveProject as follows:
public Project saveProject(Project project) {
project.getOtherData().setProject(project); // This line was added
if (project.getId() == null) {
entityManager.persist(project);
} else {
entityManager.merge(project);
}
return project;
}
This will fix the database issue (the project_id will be set), but leads to the next issue. The response body cannot be serialized due to an
org.jboss.resteasy.spi.UnhandledException: javax.ws.rs.ProcessingException: RESTEASY008205: JSON Binding serialization error javax.json.bind.JsonbException: Unable to serialize property 'otherData' from com.nikitap.org_prod.entities.Project
...
Caused by: javax.json.bind.JsonbException: Recursive reference has been found in class class com.nikitap.org_prod.entities.Project.
The object structure is cyclic (project references otherData, which return references project, ...) and Jackson is unable to resolve this cycle.
To fix this issue, I would suggest to separate DTOs and database entity and explicitly map between them. In essence:
Structure the Dto-object to represent the JSON-Request and -Response you expect to receive, in a non-cyclic order
Transfer JSON-related annotations from the database entity classes to the DTO classes
In the service- or repository-layer (your choice), map the DTO to the database entites, setting all fields (including the references from project to otherData and vice-versa)
In the same layer, map database-entites back to non-cyclic DTOs
Return the DTOs from the REST endpoint

Hibernate 4.3 Cascade Merge Through Multiple Lists With Embeded ID

Hibernate 4.3.11
I have an issue saving the following object graph in hibernate. The Employer is being saved using the merge() method.
Employer
|_ List<EmployerProducts> employerProductsList;
|_ List<EmployerProductsPlan> employerProductsPlan;
The Employer & EmployerProducts have a auto generated pk. The EmployerProductsPlan is a composite key consisting of the EmployerProducts id and a String with the plan code.
The error occurs when there is a transient object in the EmployerProducts list that cascades to List<EmployerProductsPlan>. The 1st error that I encountered which I have been trying to get past was an internal hibernate NPE. This post here perfectly describes the issue that I am having which causes the null pointer Hibernate NullPointer on INSERTED id when persisting three levels using #Embeddable and cascade
The OP left a comment specifying what they did to resolve, but I end up with a different error when changing to the suggested mapping. After changing the mapping, I am now getting
org.hibernate.NonUniqueObjectException: A different object with the same identifier value was already associated with the session : [com.webexchange.model.EmployerProductsPlan#com.webexchange.model.EmployerProductsPlanId#c733f9bd]
Due to other library dependencies, I cannot upgrade above 4.3.x at this time. This project is using spring-boot-starter-data-jpa 1.3.3. No other work is being performed on the session other than calling merge() and passing the employer object.
Below is the mappings for each class:
Employer
#Entity
#Table(name = "employer")
#lombok.Getter
#lombok.Setter
#lombok.EqualsAndHashCode(of = {"employerNo"})
public class Employer implements java.io.Serializable {
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "EMPLOYER_NO", unique = true, nullable = false)
private Long employerNo;
.....
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "employer", orphanRemoval = true)
private List<EmployerProducts> employerProductsList = new ArrayList<>(0);
}
EmployerProducts
#Entity
#Table(name = "employer_products")
#Accessors(chain = true) // has to come before #Getter and #Setter
#lombok.Getter
#lombok.Setter
#lombok.EqualsAndHashCode(of = {"employerProductsNo"})
public class EmployerProducts implements Serializable {
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "employer_products_no", unique = true, nullable = false)
private Long employerProductsNo;
#ManyToOne
#JoinColumn(name = "employer_no", nullable = false)
private Employer employer;
......
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "employerProducts", orphanRemoval = true)
private List<EmployerProductsPlan> employerProductsPlanList = new ArrayList<>(0);
}
EmployerProductsPlan
#Accessors(chain = true) // has to come before #Getter and #Setter
#lombok.Getter
#lombok.Setter
#lombok.EqualsAndHashCode(of = {"id"})
#Entity
#Table(name="employer_products_plan")
public class EmployerProductsPlan implements Serializable {
#EmbeddedId
#AttributeOverrides({ #AttributeOverride(name = "plan", column = #Column(name = "epp_plan", nullable = false)),
#AttributeOverride(name = "employerProductsNo", column = #Column(name = "employer_products_no", nullable = false)) })
private EmployerProductsPlanId id;
#ManyToOne
#JoinColumn(name = "employer_products_no")
#MapsId("employerProductsNo")
private EmployerProducts employerProducts;
}
I am populating the employerProducts above with the same instance of the EmployerProducts object that is being saved. It is transient and has no id populated as it does not existing in the db yet.
EmployerProductsPlanId
#Accessors(chain = true) // has to come before #Getter and #Setter
#lombok.Getter
#lombok.Setter
#lombok.EqualsAndHashCode(of = {"plan", "employerProductsNo"})
#Embeddable
public class EmployerProductsPlanId implements Serializable {
private String plan;
private Long employerProductsNo;
// This was my previous mapping that was causing the internal NPE in hibernate
/* #ManyToOne
#JoinColumn(name = "employer_products_no")
private EmployerProducts employerProducts;*/
}
UPDATE:
Showing struts controller and dao. The Employer object is never loaded from the db prior to the save. Struts is creating this entire object graph from the Http request parameters.
Struts 2.5 controller
#lombok.Getter
#lombok.Setter
public class EditEmployers extends ActionHelper implements Preparable {
#Autowired
#lombok.Getter(AccessLevel.NONE)
#lombok.Setter(AccessLevel.NONE)
private IEmployerDao employerDao;
private Employer entity;
....
public String save() {
beforeSave();
boolean newRecord = getEntity().getEmployerNo() == null || getEntity().getEmployerNo() == 0;
Employer savedEmployer = newRecord ?
employerDao.create(getEntity()) :
employerDao.update(getEntity());
setEntity(savedEmployer);
return "success";
}
private void beforeSave() {
Employer emp = getEntity();
// associate this employer record with any products attached
for (EmployerProducts employerProduct : emp.getEmployerProductsList()) {
employerProduct.setEmployer(emp);
employerProduct.getEmployerProductsPlanList().forEach(x ->
x.setEmployerProducts(employerProduct));
}
// check to see if branding needs to be NULL. It will create the object from the select parameter with no id
// if a branding record has not been selected
if (emp.getBranding() != null && emp.getBranding().getBrandingNo() == null) {
emp.setBranding(null);
}
}
}
Employer DAO
#Repository
#Transactional
#Service
#Log4j
public class EmployerDao extends WebexchangeBaseDao implements IEmployerDao {
private Criteria criteria() {
return getCurrentSession().createCriteria(Employer.class);
}
#Override
#Transactional(readOnly = true)
public Employer read(Serializable id) {
return (Employer)getCurrentSession().load(Employer.class, id);
}
#Override
public Employer create(Employer employer) {
getCurrentSession().persist(employer);
return employer;
}
#Override
public Employer update(Employer employer) {
getCurrentSession().merge(employer);
return employer;
}
}
As of right now, my solution is to loop through the EmployerProducts and check for new records. I called a persist on the new ones before calling the merge() on the parent Employer. I also moved the logic I had associating all the keys into the dao instead of having it in my Struts action. Below is what my update() method in the Employer DAO now looks like
public Employer update(Employer employer) {
// associate this employer record with any products attached
for (EmployerProducts employerProduct : employer.getEmployerProductsList()) {
employerProduct.setEmployer(employer);
if (employerProduct.getEmployerProductsNo() == null) {
// The cascade down to employerProductsPlanList has issues getting the employerProductsNo
// automatically if the employerProduct does not exists yet. Persist the new employer product
// before we try to insert the new composite key in the plan
// https://stackoverflow.com/questions/54517061/hibernate-4-3-cascade-merge-through-multiple-lists-with-embeded-id
List<EmployerProductsPlan> plansToBeSaved = employerProduct.getEmployerProductsPlanList();
employerProduct.setEmployerProductsPlanList(new ArrayList<>());
getCurrentSession().persist(employerProduct);
// add the plans back in
employerProduct.setEmployerProductsPlanList(plansToBeSaved);
}
// associate the plan with the employer product
employerProduct.getEmployerProductsPlanList().forEach(x ->
x.getId().setEmployerProductsNo(employerProduct.getEmployerProductsNo())
);
}
return (Employer)getCurrentSession().merge(employer);
}

Spring data JPA: how to enable cascading delete without a reference to the child in the parent?

Maybe this is an overly simple question, but I am getting an exception when I try to delete a user entity.
The user entity:
#Entity
#Table(name = "users")
public class User
{
#Transient
private static final int SALT_LENGTH = 32;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
#NotNull
private String firstName;
#NotNull
private String lastName;
#Column(unique = true, length = 254)
#NotNull
private String email;
// BCrypt outputs 60 character results.
#Column(length = 60)
private String hashedPassword;
#NotNull
private String salt;
private boolean enabled;
#CreationTimestamp
#Temporal(TemporalType.TIMESTAMP)
#Column(updatable = false)
private Date createdDate;
And I have an entity class which references a user with a foreign key. What I want to happen is that when the user is deleted, any PasswordResetToken objects that reference the user are also deleted. How can I do this?
#Entity
#Table(name = "password_reset_tokens")
public class PasswordResetToken
{
private static final int EXPIRATION_TIME = 1; // In minutes
private static final int RESET_CODE_LENGTH = 10;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
private String token;
#OneToOne(targetEntity = User.class, fetch = FetchType.EAGER)
#JoinColumn(nullable = false, name = "userId")
private User user;
private Date expirationDate;
The exception I am getting boils down to Cannot delete or update a parent row: a foreign key constraint fails (`heroku_bc5bfe73a752182`.`password_reset_tokens`, CONSTRAINT `FKk3ndxg5xp6v7wd4gjyusp15gq` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`))
I'd like to avoid adding a reference to PasswordResetToken in the parent entity, becaue User shouldn't need to know anything about PasswordResetToken.
It is not possible on JPA level without creating a bidirectional relation. You need to specify cascade type in User class. User should be owner of the relation and it should provide the information on how to deal with related PasswordResetToken.
But if you cannot have a bidirectional relation I would recommend you to setup relation directly in schema generation SQL script.
If you create your schema via SQL script and not via JPA autogeneration (I believe all serious projects must follow this pattern) you can add ON DELETE CASCADE constraint there.
It will look somehow like this:
CREATE TABLE password_reset_tokens (
-- columns declaration here
user_id INT(11) NOT NULL,
CONSTRAINT FK_PASSWORD_RESET_TOKEN_USER_ID
FOREIGN KEY (user_id) REFERENCES users (id)
ON DELETE CASCADE
);
Here is the documentation on how to use DB migration tools with spring boot. And here is the information on how to generate schema script from hibernate (that will simplify the process of writing your own script).
Parent Entity:
#OneToOne
#JoinColumn(name = "id")
private PasswordResetToken passwordResetToken;
Child Entity:
#OneToOne(mappedBy = "PasswordResetToken", cascade = CascadeType.ALL, orphanRemoval = true)
private User user;
If you want the Password entity to be hidden from the client, you can write a custom responses and hide it. Or if you want to ignore it by using #JsonIgnore
If you don't want the reference in the Parent Entity (User), then you have to override the default method Delete() and write your logic to find and delete the PasswordResetToken first and then the User.
You can use Entity listener and Callback method #PreRemove to delete an associated 'Token' before the 'User'.
#EntityListeners(UserListener.class)
#Entity
public class User {
private String name;
}
#Component
public class UserListener {
private static TokenRepository tokenRepository;
#Autowired
public void setTokenRepository(TokenRepository tokenRepository) {
PersonListener.tokenRepository = tokenRepository;
}
#PreRemove
void preRemove(User user) {
tokenRepository.deleteByUser(user);
}
}
where deleteByPerson is very simple method of your 'Token' repository:
public interface TokenRepository extends JpaRepository<Token, Long> {
void deleteByUser(User user);
}
Pay attention on static declaration of tokenRepository - without this Spring could not inject TokenRepository because, as I can understand, UserListener is instantiated by Hybernate (see additional info here).
Also as we can read in the manual,
a callback method must not invoke EntityManager or Query methods!
But in my simple test all works OK.
Working example and test.

Cannot add or update a child row: a foreign key constraint fails - Bidirectional mapping in hibernate

Hi I have a problem with JPA...
DB:
Java classes (irrelevant fields are ommited):
User:
#Entity
public class User{
#Id
private int iduser;
//bi-directional many-to-one association to UserInfo
#OneToMany(mappedBy="user", cascade= {CascadeType.ALL}, fetch=FetchType.EAGER)
private List<UserInfo> userInfos;
}
UserInfo:
#Entity
#Table(name="user_info")
public class UserInfo {
#Id
#Column(name="iduser_info")
private int iduserInfo;
//bi-directional many-to-one association to User
#ManyToOne
private User user;
}
Currently when I try to do this (again I omitted setting irrelevant fields):
User u = new User();
UserInfo info = new UserInfo();
u.addUserInfo(info);
em.persist(u); // save user
I get this error:
Cannot add or update a child row: a foreign key constraint fails (`webstore`.`user_info`, CONSTRAINT `fk_user_info_user` FOREIGN KEY (`user_iduser`) REFERENCES `user` (`iduser`) ON DELETE NO ACTION ON UPDATE NO ACTION)
I have been banging my head all day and I can't figure it out... I have also searched for solutions here but they all suggest that this error shows that I want to enter UserInfo without user_iduser value entered, but even if I add this before persist:
info.setUser(u);
it still doesn't work - is bidirectional mapping even supported with cascading? The desired effect is that User should be inserted and then all the UserInfos in the list after it refering the original User. How can I achieve that?
I don't want to do
SET foreign_key_checks = 0/1;
everytime =(
Try:
#ManyToOne
#JoinColumn(name="user_iduser", nullable=false)
private User user;
Then see if this works. I'm assuming that in user_info table, the user_id field is NOT NULL in your RDBMS. Also, since it's bidirectional, you will have to setXXX on UserInfo and User.
User u = new User();
UserInfo info = new UserInfo();
info.setUser(u);
u.addUserInfo(info);
em.persist(u); // save user
Update: Are you sure you're specifying an ID for both User and UserInfo? It looks like the ID is not set hence there is no reference to link UserInfo to User.
If the ID are AUTO_INCREMENT, then add the following after #Id annotation:
#GeneratedValue(strategy=GenerationType.IDENTITY)
I also had this error, fixed this error by adding #GeneratedValue(strategy = GenerationType.IDENTITY) annotation on top of iduser and you must have foreign keys CASCADE ON DELETE, UPDATE.
this is worked for me in my example ,
i created books and library.
#Entity
#Table(name = "book")
public class Books{
#Id
private int library_id;
private String libraryname;
//add getters and setters bellow
}
#Entity
#Table(name = "library")
public class Book {
#Id
private int book_id;
private String book_title;
#JsonIgnore
#ManyToOne
#JoinColumn(name="library_id" , nullable = false)
#OnDelete(action = OnDeleteAction.CASCADE)
private Library library;
//set getters and setters
}
in controller i used this method
#RequestMapping(value = "/{libraryId}/book", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
public Book createBook(#PathVariable(value = "libraryId") Integer libraryId, #RequestBody Book book) {
List<Book> books = new ArrayList<Book>();
Library author1 = new Library();
Optional<Library> byId = LibraryRepository.findById(libraryId);
Library author = byId.get();
book.setLibrary(author);
Book book1 = bookRepository.save(book);
books.add(book1);
author1.setBooks((List<Book>) books);
return book1;
}

Categories