SpringBoot CascadeType ALL vs MERGE and detached entities - spring-boot

I have the following entities:
#Entity
#Getter #Setter #NoArgsConstructor #RequiredArgsConstructor
public class Link extends Auditable {
#Id
#GeneratedValue
private Long id;
#NonNull
private String title;
#NonNull
private String url;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "link")
private List<Comment> comments = new ArrayList<>();
#Transient
#Setter(AccessLevel.NONE)
private String userAlias ;
public String getUserAlias() {
if(user == null)
return "";
return user.getAlias();
}
#ManyToOne
private User user;
public Long getUser() {
if(user == null)
return -1L;
return user.getId();
}
public void addComment(Comment c) {
comments.add(c);
c.setLink(this);
}
}
#Entity
#Getter #Setter #NoArgsConstructor #RequiredArgsConstructor
public class Comment extends Auditable{
#Id
#GeneratedValue
private Long id;
#NonNull
private String comment;
#ManyToOne(fetch = FetchType.LAZY)
private Link link;
public Long getLink() {
return link.getId();
}
}
If I create a comment and a link, associate the link to the comment and then save that works.
Eg:
Link link = new Link("Getting started", "url");
Comment c = new Comment("Hello!");
link.addComment(c);
linkRepository.save(link);
However, if I save the comment first:
Link link = new Link("Getting started", "url");
Comment c = new Comment("Hello!");
commentRepository.save(c);
link.addComment(c);
linkRepository.save(link);
I get
Caused by: org.hibernate.AnnotationException: #OneToOne or #ManyToOne on uk.me.dariosdesk.dariodemo.domain.Comment.link references an unknown entity: uk.me.dariosdesk.dariodemo.domain.Link
at org.hibernate.cfg.ToOneFkSecondPass.doSecondPass(ToOneFkSecondPass.java:97) ~[hibernate-core-5.4.0.Final.jar:5.4.0.Final]
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.processEndOfQueue(InFlightMetadataCollectorImpl.java:1815) ~[hibernate-core-5.4.0.Final.jar:5.4.0.Final]
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.processFkSecondPassesInOrder(InFlightMetadataCollectorImpl.java:1759) ~[hibernate-core-5.4.0.Final.jar:5.4.0.Final]
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.processSecondPasses(InFlightMetadataCollectorImpl.java:1646) ~[hibernate-core-5.4.0.Final.jar:5.4.0.Final]
at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:287) ~[hibernate-core-5.4.0.Final.jar:5.4.0.Final]
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:903) ~[hibernate-core-5.4.0.Final.jar:5.4.0.Final]
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:934) ~[hibernate-core-5.4.0.Final.jar:5.4.0.Final]
Changing the cascade type from ALL to MERGE seems to fix the problem and accept both implementations. (Ie: Adding a pre-existing comment or creating both and then saving via the link)
1) Why is this?
2) Is there anything I should be aware of in using MERGE rather than ALL?

Repository save method checks if entity exist. For new entity persist is called, for persisted entity merge is called.
#Transactional
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
In 2nd use-case Link is new entity, therefore persist() is called. With CascadeType.ALL persist() is cascaded to Comment entity. Comment is already persisted and needs to be merged, persist() fails.
If you use CascadeType.MERGE persist() is not cascaded down to Comment. It does not fail.

Related

How to correctly describe entities with many-to-many relationship, Spring Boot JPA

I have those entities:
#Entity
#Getter
#Setter
#AllArgsConstructor
#NoArgsConstructor
public class Tender {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(nullable = false, updatable = false)
private Long id;
private String source;
private String sourceRefNumber;
private String link;
private String title;
#Column(columnDefinition="TEXT")
private String description;
private String field;
private String client;
private LocalDate date;
private LocalDate deadline;
#ManyToMany
private List<Cpv> cpv;
}
And CPV:
#Entity
#Getter
#Setter
#AllArgsConstructor
#NoArgsConstructor
public class Cpv {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String code;
private String description;
}
Each Tender can have list of Cpv-s.
In my DB I have already list of all CPV codes with description, so when I add new Tender to DB, it should add record to tender_cpv table with tender_id and cpv_id.
But when I'm using this method in my TenderServiceImpl to set Cpv id-s from DB I got error after that when try to save Tender:
#Override
public Tender addNewTender(Tender tender) {
if(tender.getCpv() != null) {
for(Cpv cpv : tender.getCpv()) {
cpv = cpvRepository.findCpvByCode(cpv.getCode());
}
}
tenderRepository.save(tender);
return tender;
}
org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.supportportal.domain.Cpv;
I understand that somewhere in the description of the entities a mistake was made, because earlier I did not have a database with all the CPV codes and before saving the tender I saved all the CPVs, but now I need to redo the logic to use the existing CPV database.
Please advise how can I change the entity description.
addNewTender method changes solved my problem:
#Override
public Tender addNewTender(Tender tender) {
if(tender.getCpv() != null) {
List<Cpv> dbCpvs = new ArrayList<>();
for(Cpv cpv : tender.getCpv()) {
dbCpvs.add(cpvRepository.findCpvByCode(cpv.getCode()));
}
tender.setCpv(dbCpvs);
}
tenderRepository.save(tender);
return tender;
}
In order for the existing entities from the database to bind to the new object, we had to first get each of them from the database and bind to the new entity.

Cascading of persist, does not create identifiaction

Having this entities:
User.java:
#Entity
#NoArgsConstructor
#Getter
#Setter
public class User {
#Id #GeneratedValue
private Long id;
private String username;
#OneToMany(mappedBy = "owner", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#MapKey(name = "friend_id")
private Map<User, Friendship> friends = new HashMap<>();
}
Friendship.java:
#Entity
#Data
#IdClass(Friendship.class)
public class Friendship implements Serializable {
#Id
private Long owner_id;
#Id
private Long friend_id;
private String level;
#ManyToOne(cascade = CascadeType.ALL)
#MapsId("owner_id")
private User owner;
#ManyToOne(cascade = CascadeType.ALL)
#MapsId("friend_id")
private User friend;
}
and DemoApplication.java:
#Bean
public CommandLineRunner loadData(UserRepository userRepo){
return new CommandLineRunner() {
#Override
public void run(String... args) throws Exception {
User owner = new User();
owner.setUsername("owner");
User f1 = new User();
f1.setUsername("f1");
User f2 = new User();
f2.setUsername("f2");
Friendship fs1 = new Friendship();
fs1.setOwner(owner);
fs1.setFriend(f1);
Friendship fs2 = new Friendship();
fs2.setOwner(owner);
fs2.setFriend(f2);
owner.getFriends().put(f1, fs1);
owner.getFriends().put(f2, fs2);
userRepo.saveAndFlush(owner);
}
};
}
I get error:
A different object with the same identifier value was already associated with the session : [com.example.demo.model.Friendship#Friendship(owner_id=null, friend_id=null, level=null, owner=com.example.demo.model.User#2b036135, friend=com.example.demo.model.User#a9e28af9)]
Which means both Users f1 and f2, are having null in Long id. The indeed have, when the object is created, but I thought the mapping had specified CascadeType.ALL and #GeneratedValue so the if should be created.
But I had try to set the ids myself:
...
f1.setUsername("f1");
f1.setId(1L);
User f2 = new User();
f2.setUsername("f2");
f2.setId(2L);
...
But now I got
detached entity passed to persist: com.example.demo.model.User
So I guess I should let the creation of primary keys on JPA. But as you saw from above, it does not that even with Cascading. So what now?
Try adding this under your #Id annotation
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)

JPA doesn't fetch the updated data

I am facing a very weird issue in JPA entity manager. I have tow Entities
1) Incident
2) Country
Country is master and Incident is child with ManyToOne.
Incident.java
#Entity
#Table(name = "Incident")
public class Incident {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "incidentID")
private Integer incidentID;
#Column(name = "incidentTitle")
private String incidentTitle;
#ManyToOne
#JoinColumn(name = "countryID")
private Country country;
#Transient
#ManyToOne
#JoinColumn(name = "countryID")
public Country getCountry() {
return country;
}
public void setCountry(Country country) {
this.country = country;
}
// Getter and setters
}
Country.Java
#Entity
#Table(name="Country")
public class Country {
#Id
#Column(name="id")
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
#Column(name = "name")
private String name;
#OneToMany(mappedBy = "country", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Incident> incident;
#OneToMany
#JoinColumn(
name="countryID",nullable=false)
public List<Incident> getIncident() {
return incident;
}
public void setIncident(List<Incident> incident) {
this.incident = incident;
}
//getter and setter
}
RepositoryImpl.java
#Repository
#Transactional
public class IncidentRepositoryImpl implements IncidentRepository{
#PersistenceContext
private EntityManager em;
#Autowired
public void setEntityManager(EntityManagerFactory sf) {
this.em = sf.createEntityManager();
}
#Override
public Incident addIncident(Incident incident) {
try {
em.getTransaction().begin();
em.persist(incident);
em.getTransaction().commit();
return incident;
} catch (HibernateException e) {
return null;
}
}
public Incident findById(int id) {
Incident incident = null;
incident = (Incident) em.find(Incident.class, id);
return incident;
}
}
When i add Incident, incident added successfully with countryID in Incident table, But when i try to fetch the same incident, country name comes null. But when i take restart of server or redeploy the application country name also comes. Hope there is cache issue with JAP entity manager. I try to use em.refresh(incident) in findById method, then country name comes successfully. But this refresh method is very expensive call.
Please suggest some alternate solution, how to update jpa cache automatically.
On your EntityManager em, add
#PersistenceContext(type = PersistenceContextType.TRANSACTION)
private EntityManager em;
With PersistenceContextType.TRANSACTION, Spring takes control of the life cycle of EntityManager

Spring Data JPA. Delete not reflected in mysql database

I am using Spring JPA to store a many-to-many relationship between User and Service with the table Acquisition. Since the bridge table contains additional columns I modelled it as having two many-to-one relationships. Both are bidirectional. Additionally the Acquisition entity has a one-to-one relationship with ServiceConfiguration.
There is no problem with saving or retrieving any of these entities. However when I try to delete the acquisition like this:
#Override
#Transactional
public void removeUsersServiceAcquisition(Long serviceId, User user) {
Service service = getService(serviceId);
Acquisition acquisition = findAcquisitionByServiceAndUser(service, user);
acquisitionRepository.delete(acquisition.getId());
log.info("\n retrieved acquisition {} ", acquisitionRepository.findOne(acquisition.getId()));
}
The change is not reflected in the database. The subsequent find within the above method returns null. But later in the code and in the database the record exists. There are no exceptions being thrown.
#Entity
#Table(name="ACQUISITION")
public class Acquisition implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="id")
public Long getId() {
return id;
}
#ManyToOne
#JoinColumn(name="user_id")
public User getUser() {
return user;
}
#ManyToOne
#JoinColumn(name="service_id")
public Service getService() {
return service;
}
#OneToOne(mappedBy="acquisition")
public ServiceConfiguration getConfiguration() {
return configuration;
}
}
#Entity
#Table(name="USER")
public class User implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="id")
public Long getId() {
return id;
}
#OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval=true)
public Set<Acquisition> getAcquisitions() {
return acquisitions;
}
}
#Entity
#Table(name="SERVICE")
public class Service implements Serializable{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="id")
public Long getId() {
return id;
}
#OneToMany(mappedBy="service", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval= true)
public Set<Acquisition> getAcquisitions() {
return acquisitions;
}
}
#Entity
#Table(name="SERVICE_CONFIGURATION")
public class ServiceConfiguration implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="id")
public Long getId() {
return id;
}
#OneToOne
#JoinColumn(name="acquisition_id")
public Acquisition getAcquisition() {
return acquisition;
}
public void setAcquisition(Acquisition acquisition) {
this.acquisition = acquisition;
}
}
Here is what I did to get this to work.
The remove method changed to first remove the acquisition from both relationships in which it participated:
#Transactional
public void removeUsersServiceAcquisition(Long serviceId, User user) {
Service service = getService(serviceId);
Acquisition acquisition = findAcquisitionByServiceAndUser(service, user);
service.getAcquisitions().remove(acquisition);
user.getAcquisitions().remove(acquisition);
acquisitionRepository.delete(acquisition.getId());
log.info("\n retrieved acquisition {} ", acquisitionRepository.findOne(acquisition.getId()));
}
This resulted in "no Session" Hibernate exception.
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.netstellar.sitesuite.serviceregistry.site.model.User.acquisitions, could not initialize proxy - no Session
Which I dealt with by adding fetch property to the User mapping. Not sure if this is the only way of addressing this exception.
#OneToMany(mappedBy = "user", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval=true)
public Set<Acquisition> getAcquisitions() {
return acquisitions;
}

Why the record is posted twice in the database?

Can you tell me, why the record is posted twice in the database. I think. this happens because I use save() method. But shouldn't I save the master-entity and dependent-entity separately?
Controller method:
#RequestMapping(value = "/addComment/{topicId}", method = RequestMethod.POST)
public String saveComment(#PathVariable int topicId, #ModelAttribute("newComment")Comment comment, BindingResult result, Model model){
Topic commentedTopic = topicService.findTopicByID(topicId);
commentedTopic.addComment(comment);
// TODO: Add a validator here
if (!comment.isValid() ){
return "//";
}
// Go to the "Show topic" page
commentService.saveComment(comment);
return "redirect:../details/" + topicService.saveTopic(commentedTopic);
}
Services:
#Service
#Transactional
public class CommentService {
#Autowired
private CommentRepository commentRepository;
public int saveComment(Comment comment){
return commentRepository.save(comment).getId();
}
}
#Service
#Transactional
public class TopicService {
#Autowired
private TopicRepository topicRepository;
public int saveTopic(Topic topic){
return topicRepository.save(topic).getId();
}
}
Model:
#Entity
#Table(name = "T_TOPIC")
public class Topic {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private int id;
#ManyToOne
#JoinColumn(name="USER_ID")
private User author;
#Enumerated(EnumType.STRING)
private Tag topicTag;
private String name;
private String text;
#OneToMany(fetch = FetchType.EAGER, mappedBy = "topic", cascade = CascadeType.ALL)
private Collection<Comment> comments = new LinkedHashSet<Comment>();
}
#Entity
#Table(name = "T_COMMENT")
public class Comment
{
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private int id;
#ManyToOne
#JoinColumn(name="TOPIC_ID")
private Topic topic;
#ManyToOne
#JoinColumn(name="USER_ID")
private User author;
private String text;
private Date creationDate;
}
In this concrete case, you do not need to save the master and the client.
Saving the master or the client would be enough (with this concrete mapping)
But I think the main problem is that you do not have a good equals method in your Comment so your ORM Provider think that there are two different comments, and therefore store them twice.

Resources