Spring JPA add pagination to #ManyToMany [duplicate] - java

This question already has answers here:
Spring Data JPA Pageable with #ManyToMany
(2 answers)
Closed 1 year ago.
In my current project, I am using JPA and Hibernate to handle my database layer.
One of my entities has a Many-To-Many relation to itself, which works great using the #ManyToMany and the #JoinTable annotations.
My problem is that I am asked to add a pagination support to the entity. I have searched online for a solution but the closest thing I have found to a solution works on a different use case, where the relation is between 2 entities (and not an entity to itself).
This is the important parts from the entity's class:
package iob.data;
import iob.data.primarykeys.InstancePrimaryKey;
import iob.logic.exceptions.instance.InvalidBindingOperationException;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import java.util.Date;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
#NoArgsConstructor
#AllArgsConstructor
#Getter
#Setter
#RequiredArgsConstructor
#Entity
#Table(name = "INSTANCES")
#IdClass(InstancePrimaryKey.class)
public class InstanceEntity {
//<editor-fold desc="Primary key">
#Id
// The index would be generated from a sequence named "INSTANCES_SEQUENCE"
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "INSTANCES_SEQUENCE")
// Here we create that sequence, and define it to be updated every insertion (allocationSize = 1).
#SequenceGenerator(name = "INSTANCES_SEQUENCE", sequenceName = "INSTANCES_SEQUENCE", allocationSize = 1)
private long id;
#Id
#NonNull
private String domain;
//</editor-fold>
//<editor-fold desc="Many to Many relation">
// Define a new table that would store the references
#JoinTable(name = "INSTANCES_TO_INSTANCES",
// The main columns for this attribute are the parent's primary key which is constructed from domain and id
inverseJoinColumns = {#JoinColumn(name = "PARENT_ID", referencedColumnName = "id"),
#JoinColumn(name = "PARENT_DOMAIN", referencedColumnName = "domain")},
// The referenced columns for this attribute are the child's primary key which is also constructed from domain and id
joinColumns = {#JoinColumn(name = "CHILD_ID", referencedColumnName = "id"),
#JoinColumn(name = "CHILD_DOMAIN", referencedColumnName = "domain")})
// Declare the parent's side of the relation
#ManyToMany(fetch = FetchType.LAZY)
private Set<InstanceEntity> parentInstances = new HashSet<>();
// Declare the children's size of the relation, and define that it is related to the parentInstances
#ManyToMany(mappedBy = "parentInstances", fetch = FetchType.LAZY)
private Set<InstanceEntity> childInstances = new HashSet<>();
//</editor-fold>
public void addParent(InstanceEntity parent) {
if (this.equals(parent))
throw new InvalidBindingOperationException("Cannot assign parent to himself");
parentInstances.add(parent);
}
public void addChild(InstanceEntity child) {
if (this.equals(child))
throw new InvalidBindingOperationException("Cannot assign child to himself");
childInstances.add(child);
}
#Override
public int hashCode() {
return Objects.hash(id, domain);
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
InstanceEntity entity = (InstanceEntity) o;
return id == entity.id && domain.equals(entity.domain);
}
}
So far I tried (probably naively) write the getter for parentInstance and pass it a Pagable object (because it works on derived-queries, so worth a shot 😅) but obviously it did nothing.
The only solution I can think of is removing the relation, and creating it manually (create the table using another entity and add a new PagingAndSortingRepository that would retrieve the relevant instances and then convert them to InstanceEntity in the service).
So, how can I add a pagination support for parentInstances and childInstances?

I have found the solution in some unexpected post. It is not about pagination, but it still solved my problem.
So the solution in my case was to add the following function to my PagingAndSortingRepository interface:
Page<InstanceEntity> findDistinctByParentInstancesIn(#Param("parent_instances") Set<InstanceEntity> parentInstances, Pageable pageable);
And then calling it is:
InstanceEntity ie = new InstanceEntity();
ie.setDomain(parentDomain);
ie.setId(Long.parseLong(parentId));
instancesDao.findDistinctByParentInstancesIn(Collections.singleton(ie), PageRequest.of(page, size));
To be honest, I'm not sure how/why it works. IntelliJ IDEA gives me an error in the repository interface Expected parameter types: Collection<Set<InstanceEntity>>.
So if someone has an explanation, I would be glad to hear it.

Related

How to set parentId in Child Entity of one to many mapping using Spring Boot Data JPA

Use Case: We have one - many bidirectional relationships and we will be receiving requests to update as a parent which contains either child being modified or not.
Technology stack
Spring boot 2.0.2
Spring data Jpa
Sample Code:
Parent Class Entity:
package com.example.demo.model;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
#Getter
#Setter
#NoArgsConstructor
#DynamicInsert
#DynamicUpdate
#Entity
public class Parent {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false)
private String a;
private String b;
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "parent")
private Set<Child> childs = new HashSet<>();
public void addChild(Child child) {
childs.add(child);
child.setParent(this);
}
public void removeChild(Child child) {
childs.remove(child);
child.setParent(null);
}
public void setChilds(
Set<Child> childrens) {
if (this.childs == null) {
this.childs = childrens;
}
else {
this.childs.retainAll(childrens);
this.childs.addAll(childrens);
}
}
}
Child Class Entity
package com.example.demo.model;
import java.util.Objects;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
#Getter
#Setter
#NoArgsConstructor
#DynamicInsert
#DynamicUpdate
#Entity
#Table(name = "child", uniqueConstraints = {
#UniqueConstraint(columnNames = { "a", "b", "c", "parent_id" }) })
public class Child {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String a;
private String b;
private String c;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "parent_id")
private Parent parent;
#Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Child)) {
return false;
}
Child that = (Child) o;
return Objects.equals(getA(), that.getA()) && Objects.equals(getB(), that.getB())
&& Objects.equals(getC(), that.getC());
}
#Override
public int hashCode() {
return Objects.hash(getA(), getB(), getC());
}
}
Repository Class:
package com.example.demo.model;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
#Repository
public interface ParentRepository extends JpaRepository<Parent, Long> {
#Query("select p from Parent p join fetch p.childs where p.a = ?1")
Parent findByA(String a);
}
Main-Class with the business case:
package com.example.demo;
import java.util.HashSet;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.util.Assert;
import com.example.demo.model.Child;
import com.example.demo.model.Parent;
import com.example.demo.model.ParentRepository;
#SpringBootApplication
public class DemoApplication implements CommandLineRunner {
#Autowired
ParentRepository repository;
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
#Override
public void run(String... args) throws Exception {
Child c1 = new Child();
c1.setA("a1");
c1.setB("b1");
c1.setC("c1");
Child c2 = new Child();
c2.setA("a2");
c2.setB("b2");
c2.setC("c2");
Parent p = new Parent();
p.addChild(c1);
p.addChild(c2);
p.setA("a");
repository.save(p);
// This works till now
// We will get the request for updating parent which might contain removal or addition of the child
Parent retrievedParent = repository.findByA("a");
retrievedParent.setB("b");
Child c4 = new Child();
c4.setA("a2");
c4.setB("b2");
c4.setC("c2");
Child c3 = new Child();
c3.setA("a3");
c3.setB("b3");
c3.setC("c3");
//If we know that c1 is removed and c3 is added we can use synchronize methods written in Parent
//As we don't know which are removed and which are added also as we won't get the id from request passing them
// directly to set to let hibernate handle it as equals and Hashcode is already written.
Set<Child> childrens = new HashSet<>();
childrens.add(c3);
childrens.add(c4);
retrievedParent.setChilds(childrens);
Parent persistedParent = repository.save(retrievedParent);
for (Child child : persistedParent.getChilds()) {
Assert.notNull(child.getParent(), "Parent must not be null");
//For child 3 it is failing
}
}
}
With above code, it is unable to set the parent id for child entity 4, if we print SQL logs we can observe that child with id 1 is deleted and child with id 3 is inserted which is expected.
As a workaround I am iterating all child entries and if the parent is not set then setting manually. I don't want this extra update statement.
Other approaches tried, removing all child entries using synchronized removeChild method and then add remaining one by one using synchronized addChild method . This is causing unique constraint failure exception.
What is required?
Setting the parent instead of workaround in the insert statement when it is executed.
The problem comes from this part:
Set<Child> childrens = new HashSet<>();
childrens.add(c3);
childrens.add(c4);
retrievedParent.setChilds(childrens);
You never need to rewrite a managed collection.
Now, based on your design:
If we know that c1 is removed and c3 is added we can use synchronize
methods written in Parent.
As we don't know which are removed and
which are added also as we won't get the id from request passing them
directly to set to let hibernate handle it as equals and Hashcode
is already written.
If the client sends you a collection of entries, you need to do the matching yourself, meaning that you need to:
add new elements
remove elements that are no longer needed
update the existing ones

JSON return nested arrays instead of objects [Spring boot + JPA + MySQL + REST]

ISSUE
Hello Guys please help me to solve this.
I ve started building a REST API and got a problem when testing the URLs that I've made. Example: when I send request to get the list of one object, the request work fine but the data syntax returned by JSON is ugly: I got in result nested arrays instead of one global array containing json Objects inside it. Check my code please, I have 2 entities now that one of them depend on the other, I used #OneToMany to make relationships between them and no error has occured. Thanks in advance.
SOLUTION
The problem is: my query was returning a list of lists by default, so I had to modify my query by adding a constructor call. check this links please: using new keyword in HQL query
Also I added #JsonIgnore annotation to ignore some properties in my entities to prevent their show. Now the data is shown as formatted as I want :D thanks for your help. Check the new result here
Update
Hello again, I realized recently, that is bad to use #JsonIgnore annotation to prevent some properties from being send in the Json response, and the best way to customize which properties to send is to use DTOs class. Thanks again kj007
Entity 1
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import lombok.Data;
#Data
#Table(name = "x_assureurs") // this is the table name in DB
#Entity(name = "Assureurs") // This tells Hibernate to make a table out of this class
public class Assureurs {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "n_assureur")
private String id;
#Column(name = "nom_assureur")
private String name;
#OneToMany(mappedBy="assureur",fetch = FetchType.LAZY)
private List<Contrats> contrats;
}
Entity 2
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import lombok.Data;
#Data
#Table(name = "contrats") // this is the table name in DB
#Entity(name = "Contrats") // This tells Hibernate to make a table out of this class
public class Contrats {
#Id
#Column(name = "id")
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
#Column(name = "num_contrat")
private String num;
#Column(name = "nom_police")
private String nomPolice;
#ManyToOne
#JoinColumn(name = "courtier")
private Courtiers courtier;
#ManyToOne
#JoinColumn(name = "assureur")
private Assureurs assureur;
}
Repository
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import tn.igase.gestdoc.models.entities.Assureurs;
// This will be AUTO IMPLEMENTED by Spring into a Bean called assureurRepository
#Repository
public interface AssureurRepository extends JpaRepository<Assureurs, String> {
// CONSTANTS
String FIND_ALL_BY_CONTRATS = "SELECT DISTINCT(contrat.assureur.id) as n_assureur, assureur.name \n"
+ " FROM Contrats contrat \n" + " JOIN Assureurs assureur ON contrat.assureur.id = assureur.id ";
String BY_ONE_COURTIER = "WHERE contrat.courtier.id = :idCourtier";
// QUERIES
#Query(FIND_ALL_BY_CONTRATS)
Iterable<Assureurs> findAllByContrats();
#Query(FIND_ALL_BY_CONTRATS + BY_ONE_COURTIER)
Iterable<Object> findAllByContratsAndCourtier(#Param("idCourtier") int idCourtier);
}
Service
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import tn.igase.gestdoc.daos.AssureurRepository;
import tn.igase.gestdoc.models.entities.Assureurs;
#Service
public class AssureurService {
#Autowired
AssureurRepository assureurRepository;
public Iterable<Assureurs> findAllByContrats() {
return assureurRepository.findAllByContrats();
}
}
Controller
import java.util.ArrayList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import tn.igase.gestdoc.models.entities.Assureurs;
import tn.igase.gestdoc.service.AssureurService;
import tn.igase.gestdoc.service.ContratService;
/**
*
* Assureur controller
*
* #author fre
*/
#RestController
#RequestMapping(path = "/api/assureurs")
public class AssureurController extends MainController {
#Autowired
private AssureurService assureurService;
/**
* Revert all assureurs that all have contrats
*
* #return list
*/
#RequestMapping(path = "/all", produces=MediaType.APPLICATION_JSON_VALUE)
public Iterable<Assureurs> getAll() {
// This returns a JSON or XML with the users
Iterable<Assureurs> assureurs = new ArrayList<>();
assureurs = assureurService.findAllByContrats();
return assureurs;
}
}
Result
Check the JSON data returned here
Your current HQL will return list of objects that’s why you are seeing result like this.
you can either return entity or ID(type) from a HQL or JPA named query..not projected/custom columns.
To order to achieve your list of object you can do it via couple of ways..
As HQL will retrun list of objects you can parse the object according to your need in your service class method.
#Query(FIND_ALL_BY_CONTRATS)
List<Object> findAllByContrats();
2. Use DTO (Which is best way to it)
STEP1: Create DTO for projected columns you want, make sure constructure meet the parameters required from hql ..for example..
#Data
public class AssureursDTO {
private Long n_assureur;
private String name;
public AssureursDTO(Long n_assureur, String name) {
this.n_assureur = n_assureur;
this.name = name;
}
}
STEP 2: define your HQL like this by passing full package path of DTO, use yours
String FIND_ALL_BY_CONTRATS = "SELECT DISTINCT new com.example.demomysql21.entity.AssureursDTO(assureur.id as n_assureur, assureur.name) \n"
+ " FROM Contrats contrat \n" + " JOIN Assureurs assureur ON contrat.assureur.id = assureur.id";
STEP 3: Now it will return you LIST
#Query(FIND_ALL_BY_CONTRATS)
List<AssureursDTO> findAllByContrats();

Nullable #ManyToOne not accepting null value [duplicate]

This question already has an answer here:
Set parent target to null if source is null in MapStruct
(1 answer)
Closed 4 years ago.
I have a many-to-one relationship that I want to be nullable.
Here's the parent:
#Entity
#Table(name = "C_CUSTOMER")
class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#OneToMany (fetch = FetchType.LAZY, mappedBy="user", cascade = CascadeType.REMOVE)
private List<Profile> profiles;
}
And the child:
#Entity
#Table(name = "C_PROFILE")
class Profile {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#ManyToOne(optional = true)
#JoinColumn(nullable = true, name = "user_id", referencedColumnName = "id", insertable = false, updatable = false)
private User user;
}
When I try to save a profile without a userId, the following error is threw:
org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : fr.entity.Profile.user -> fr.entity.User
If the field userId has a value, then it works smoothly.
I tried a bunch of answers from other questions on SO, but so far nothing has worked. The easiest way would be to ditch the relation in the entity files and use Integer customerId, but that's not really satisfactory for me, 'cause it would mean that the cascade deletion wouldn't work anymore.
Thanks in advance.
EDIT
The problem comes from a entity <-> dto mapping. I added an answer about it, will probably edit if I come across a workaround.
Tried your code on my machine. Below are my entity classes:
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import lombok.Data;
#Entity
#Table(name = "C_PROFILE")
#Data
public class Profile {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
#ManyToOne(optional = true)
#JoinColumn(nullable = true, name = "user_id", referencedColumnName = "id", insertable = false, updatable = false)
private User user;
}
User class:
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import lombok.Data;
#Entity
#Table(name = "C_CUSTOMER")
#Data
class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String email;
#OneToMany (fetch = FetchType.LAZY, mappedBy="user", cascade = CascadeType.REMOVE)
private List<Profile> profiles;
}
Repository:
import org.springframework.data.jpa.repository.JpaRepository;
import io.polarisdev.returns.model.Profile;
public interface ProfileRepository extends JpaRepository<Profile, String> {
}
Code to save the Profile without user:
Profile p = new Profile();
p.setName("Name");
profileRepository.save(p);
So, after some digging inside the generated files: there's no solution to it (for now). The problem comes from a mapping between entity <-> dto, using mapstruct: having not mentioned this step, no wonder it wasn't working like it should for me... Well, the profile mapper contains this:
protected User profileToUser(Profile profile) {
if ( profile == null ) {
return null;
}
User user = new User();
User.setId(profile.getUserId());
return user;
}
Meaning I need to reassign a null value before saving the Profile. There is apparently a ticket about this on the github, so wait and see. Seems like there's some suggestions too to make it work, so if I come across one that work, i'll edit this answer (unless someone answer with a solution before).
EDIT
After a few researches on the mapstruct git and SO, I came across a solution that work, though it's not really elegant.
#AfterMapping
public Profile doAfterMapping(#MappingTarget Profile entity) {
if (entity != null && entity.getUser().getId() == null) {
entity.setUser(null);
}
return entity;
}
So... I'll put the question in duplicate. Thanks a bunch to the ones that helped me (though I wouldn't have need to post if I had been more vigilant).

Spring JPA + CRUD - custom query doesn't allow _ characters?

I'm having troubles in creating a custom query within spring, because my Entity contains an "_" character in it's parameter's name: "game_date".
My table has a column named "game_date" as well.
I have created following method:
List<Games> findByGame_dateAndOpponent(#Param("game_date") Date game_date, #Param("opponent") String opponent);
but when I start my app, it's crashing with exception of kind: "org.springframework.data.mapping.PropertyReferenceException: No property gamedate found for type Games!". After changing a parameter name to the "gameDate" both in Entity and Query method, it stopped complaining, and is actually returning expected entries. But at the same time, it doesn't return values from the column "game_date", in the search queries, which is a simple regular column of a Date type. I have no idea what's going on with all this thing.
DB I'm using is MySql.
Here comes the code itself:
Entity:
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
#Entity
#Table(name = "games")
public class Games {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id_game")
private int id;
#Column(name = "game_date", columnDefinition = "DATE")
#Temporal(TemporalType.DATE)
private Date gameDate;
public Date getGame_date() {
return gameDate;
}
public void setGame_date(Date _game_date) {
this.gameDate = _game_date;
}
}
And a repository:
import java.sql.Date;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
#RepositoryRestResource
public interface GamesRepository extends CrudRepository< Games , Integer > {
List< Games > findById( #Param( "id" ) int id );
List< Games > findAll( );
List<Games> findByGameDateAndOpponent(#Param("game_date") Date game_date, #Param("opponent") String opponent);
}
The underscore is a reserved keyword in Spring Data JPA. It should be enough to remove it from your property and from its getters and setters and Hibernate will do the rest:
#Entity
#Table(name = "games")
public class Games {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id_game")
private int id;
//Getter and setters for id
#Column(name = "game_date")
private Date gameDate;
public Date getGameDate() {
return gameDate;
}
public void setGameDate(Date gameDate) {
this.gameDate = gameDate;
}
}
Also, in general, try to use java naming convention for variable and field names, which is mixed case with lowercase first.
See also:
Spring Data JPA repository methods don't recognize property names with underscores

#OneToMany SQLIntegrityConstraintViolationException error upon insert (Hibernate)

I am having trouble inserting records with a #OneToMany relationship using Hibernate and Spring MVC. I can successfully insert records without adding anything to the #OneToMany collection. However, upon adding a collection record, it fails stating that there is a SQLIntegrityConstraintViolationException.
My current code for the mapping (annotation-style) is as follows:
Contact.java
package mil.navy.navsupbsc.entity;
import java.util.Collection;
import java.util.LinkedHashSet;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.persistence.Transient;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
#Entity
#Table(name = "CONTACT2")
public class Contact extends Auditable {
public Contact() {
}
// Create with mandatory fields
public Contact(long id, Salutation salutation, String firstName,
String middleInitial, String lastName,
MilitaryCivilianInformation milCivInfo) {
this.setContactId(id);
this.setSalutation(salutation);
this.setFirstName(firstName);
this.setMiddleInitial(middleInitial);
this.setLastName(lastName);
this.setMilitaryCivilianInformation(milCivInfo);
}
/**
*
*/
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue
#SequenceGenerator(name = "CONTACT_SEQ")
#Column(name = "CONTACT_ID")
private Long contactId;
#Fetch(FetchMode.SUBSELECT)
#OneToMany(cascade = { CascadeType.ALL, CascadeType.REMOVE }, mappedBy = "contact", orphanRemoval = true)
private Collection<Email> emails = new LinkedHashSet<Email>();
/**
* #return the emails
*/
public Collection<Email> getEmails() {
for (Email email : emails) {
email.getEmailType();
}
return emails;
}
/**
* #param emails
* the emails to set
*/
public void setEmails(Collection<Email> emails) {
this.emails.clear();
for (Email email : emails) {
this.addEmail(email);
}
}
public void addEmail(Email email) {
email.setContact(this);
this.getEmails().add(email);
}
[...more Getters / Setters and fields]
}
Email.java
package mil.navy.navsupbsc.entity;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import org.codehaus.jackson.annotate.JsonBackReference;
/**
* Implements Auditing Properties
*/
#Entity
#Table(name = "EMAIL")
public class Email extends Auditable {
private static final long serialVersionUID = -4833322552325183301L;
#Id
#GeneratedValue
#SequenceGenerator(name = "EMAIL_SEQ")
#Column(name = "EMAIL_ID")
private long emailId;
#JsonBackReference
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "CONTACT_FK")
private Contact contact;
[More fields]
public Contact getContact() {
return contact;
}
public void setContact(Contact contact) {
this.contact = contact;
}
public long getEmailId() {
return emailId;
}
public void setEmailId(long emailId) {
this.emailId = emailId;
}
[more getters / setters]
}
ContactDAOImpl (I've tried many variations of this with no success)
public void saveContact(Contact contact) {
Session session = sessionFactory.getCurrentSession();
Collection<Email> emailCollection = new LinkedHashSet<Email>();
emailCollection = contact.getEmails();
Contact contactToSave;
long contactId;
if (contact.getContactId() == 0 || contact.getContactId() == null) {
contactToSave = new Contact((long) 0, contact.getSalutation(),
contact.getFirstName(), contact.getMiddleInitial(),
contact.getLastName(),
contact.getMilitaryCivilianInformation());
session.save(contactToSave);
session.flush();
for (Email email : emailCollection) {
// email.setContact(contactToSave);
contactToSave.addEmail(email);
}
session.saveOrUpdate(contactToSave);
session.flush();
session.clear();
}
Any help with this is much appreciated. I had a previous version of this that updated records correctly, but can't seem to work out the Save new records. I also originally used the contact that I passed in from the web service, but I attempted to create a new record in the DAO to eliminate potential problems in the latest variation of my code.
Also, I know that there are many similar questions, but I have tried many of the answers with no success (hence the new question).
Thank you for your help!
UPDATE
I checked to see what ID the data layer returns after the initial save and verified (unsuccessfully) that the same ID was saved in the database. The returned ID is different than the saved ID. For example, the latest save showed the Contact ID as '1129' with the returned contact after the initial save. I did a retrieve from the database with contact ID '1129' - and it successfully returned the contact. After closing the transaction, I viewed the data directly in the database. The database showed '193' as the Contact ID instead of '1129'. Any ideas??
I figured out the issue I was having.
Prior to creating the Hibernate entity definition, I created a sequence and trigger in the database directly. I also specified the same sequence in the entity definition within the JAVA hibernate code. However, my syntax for generating the sequence was not complete.
Since the syntax for generating the sequence was incomplete, the JAVA code was using the default HIBERNATE_SEQUENCE generator, rather than the sequence I created. Hibernate returned the sequence value from HIBERNATE_SEQUENCE (creating the ID prior to the database insert). However, immediately prior to inserting the value into the database, Oracle ran my custom sequence and assigned the Contact ID to the newly generated value.
Consequently, when I tried to add a value to the collection, the Insert statement attempted to use the sequence value generated by Hibernate as the foreign key, rather than the sequence value generated by the database.
In order to ensure that the Hibernate code and database were in sync, I removed the ContactID trigger from the database. Based on another question in StackOverflow (Hibernate and Oracle Sequence), I updated the JAVA Hibernate code for the Contact.java ContactID declaration to:
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "CONTACT_SEQ")
#SequenceGenerator(name = "CONTACT_SEQ", sequenceName = "contact_seq", allocationSize = 1)
#Column(name = "CONTACT_ID")
private Long contactId;
Thankfully this fixed the problem. The JAVA Hibernate code now creates the ID through the sequence I specified. The database then successfully inserts those values into the database!
I was also able to simplify my save to:
public void saveContact(Contact contact) {
Session session = sessionFactory.getCurrentSession();
if (contact.getContactId() == 0 || contact.getContactId() == null) {
session.save(contact);
session.flush();
}
}
I'm sure that I'll need to change it again though to account for updating the contact - probably using saveOrUpdate() instead of save().

Categories

Resources