Using oneToMany relation, but saving data in individual tables at different point of time - spring

I am working on a Spring-MVC application which has 2 tables in database and 2 domain classes. Class Person has oneTOMany relation with class Notes. I would like to add Person and notes both in database. So I googled, to find out many MVC based examples for the same problem. However they seem to assume a few things :
Data is being added in a static manner by the developer, mostly through Static void main() or another class.
Data regarding all the classes which are related is added altogether, eg : Table A has oneToMany relation, so the code will add data for both the tables in one class or one jsp file.
Other frameworks like Spring-Security at play(This point is understood).
So basically, similar examples with different names and developers is what I found. My problem is :
I don't have static void main, don't intend to use it.
I am adding data through HTML page wrapped inside JSP page.
I or the user will first register through the register form, just login later and then add notes, so I am not adding data for both tables at same time. (I have to believe this is possible by Hibernate)
Error :
org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.journaldev.spring.model.Person
org.hibernate.engine.internal.ForeignKeys.getEntityIdentifierIfNotUnsaved(ForeignKeys.java:294)
org.hibernate.type.EntityType.getIdentifier(EntityType.java:537)
org.hibernate.type.ManyToOneType.isDirty(ManyToOneType.java:311)
org.hibernate.type.ManyToOneType.isDirty(ManyToOneType.java:321)
org.hibernate.type.TypeHelper.findDirty(TypeHelper.java:294)
Person Model :
#Entity
#Table(name="person")
public class Person implements UserDetails{
private static final GrantedAuthority USER_AUTH = new SimpleGrantedAuthority("ROLE_USER");
#Id
#Column(name="id")
#GeneratedValue(strategy = GenerationType.SEQUENCE,generator = "person_seq_gen")
#SequenceGenerator(name = "person_seq_gen",sequenceName = "person_seq")
private int id;
#OneToMany(cascade = CascadeType.ALL,mappedBy = "person1")
private Set<Notes> notes1;
public Set<Notes> getNotes1() {
return notes1;
}
public void setNotes1(Set<Notes> notes1) {
this.notes1 = notes1;
}
Notes model :
#Entity
#Table(name="note")
public class Notes {
#Id
#Column(name="noteid")
#GeneratedValue(strategy = GenerationType.SEQUENCE,generator = "note_gen")
#SequenceGenerator(name = "note_gen",sequenceName = "note_seq")
private int noteId;
#ManyToOne
#JoinColumn(name = "id")
private Person person1;
public Person getPerson1() {
return person1;
}
public void setPerson1(Person person1) {
this.person1 = person1;
}
NotesDAOImpl :
#Transactional
#Repository
public class NotesDAOImpl implements NotesDAO{
private SessionFactory sessionFactory;
public void setSessionFactory(SessionFactory sf){
this.sessionFactory = sf;
}
#Override
public void addNote(Notes notes, int id) {
Session session = this.sessionFactory.getCurrentSession();
session.save(notes);
}
SQL schema :
CREATE TABLE public.person (
id INTEGER NOT NULL,
firstname VARCHAR,
username VARCHAR,
password VARCHAR,
CONSTRAINT personid PRIMARY KEY (id)
);
CREATE TABLE public.note (
noteid INTEGER NOT NULL,
sectionid INTEGER,
canvasid INTEGER,
text VARCHAR,
notecolor VARCHAR,
noteheadline VARCHAR,
id INTEGER NOT NULL,
CONSTRAINT noteid PRIMARY KEY (noteid)
);
ALTER TABLE public.note ADD CONSTRAINT user_note_fk
FOREIGN KEY (id)
REFERENCES public.person (id)
ON DELETE NO ACTION
ON UPDATE NO ACTION
NOT DEFERRABLE;
Btw, the id in addNote method is just me checking if SpringSecurity is actually sending userid, and has properly loggedin, debug purpose.
So, I am unable to add notes once user is logged in, what am I doing wrong? Or this is not possible with Hibernate. In that case, let me find a gun to shoot myself.. :P

Your code will try to save notes. But these notes will not be linked to any Person. You have to do below sequence of operation.
Find the logged in person or the person for which you want to save the notes.
Create notes object which will be in transient state.
Attach notes to the person.
If it is bidirectional relationaship, then person to notes.
Below is the code template.
#Transactional
#Repository
public class NotesDAOImpl implements NotesDAO{
private SessionFactory sessionFactory;
public void setSessionFactory(SessionFactory sf){
this.sessionFactory = sf;
}
#Override
public void addNote(Notes notes, int id) {
Session session = this.sessionFactory.getCurrentSession();
Person person = getPerson(); // this method should get logged in person or the person for whom you want to save the notes.
if (person.getNotes() == null) {
Set<Note> notes = new HashSet<Note>();
person.setNotes(notes);
}
person.getNotes().add(note);
note.setPerson(person); // If bidirectional relationship.
session.update(person); // if update does not work, try merge();
}
Also make sure you have cascade type set to MERGE in person entity on notes field.
Note: Above code is just example from your code and may have some compilation error. please correct according to your requirement.

Related

Spring boot hibernate #ManyToMany doesn't commit or returns incomplete data when I execute any method on the junction table from non-owner entity

I'm currently working on a Spring Boot project for an online shop. It's my first project with Spring Boot (and my first post here), so my coding is not the best.
Context for the questions:
My shop (for now) has a lists of products and whishlists of different users (shopping lists), which have a bidirectional #ManyToMany relation (i left here the relevant details for my question(s)):
Product.java entity:
#Entity
public class Product extends RepresentationModel\<Product\>{
#Id
#GeneratedValue
#JsonView(ProductView.DescriptionExcluded.class)
private Integer id;
#ManyToMany()
#JoinTable(
name = "Shopping_Product",
joinColumns = { #JoinColumn(name = "id", referencedColumnName = "id") },
inverseJoinColumns = { #JoinColumn(name = "list_id", referencedColumnName = "list_id") })
#JsonIgnore
private Set<ShoppingList> shoppinglists = new HashSet<>();
// Constructor, getters, setters ....
ShoppingList.java entity:
#Entity
public class ShoppingList {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#JsonView(ShoppingListView.ProductsExcluded.class)
private Integer list_id;
#JsonView(ShoppingListView.ProductsIncluded.class)
#ManyToMany(mappedBy = "shoppinglists")
private Set<Product> products = new HashSet<>();
// Constructor, getters, setters ...
I chose Product as the owner because i wanted to delete (tho it would be more fit to show something like "offer expired", but I'll stick to delete for now) the product from all existing lists when the admin takes it down from the shop, which works as expected:
ProductResource.java (controller):
#DeleteMapping("/categs/*/sub/*/products/{id}")
public ResponseEntity<String> deleteProduct(#PathVariable int id) {
Optional<Product> optional = productRepository.findById(id);
if(!optional.isPresent()) throw new NotFoundException("Product id - " + id);
Product prod = optional.get();
productRepository.delete(prod);
return ResponseEntity.ok().body("Product deleted");
}
My problems now are related to the ShoppingList entity, which is not the owner.
Any call I make to the Product resource (controller) works as expected, but anything from the other side either fails or returns incomplete results, like the following:
1.
I call retrieve all products from a list and it returns only the first object (the list has at least 2):
ShoppingListResource.java (controller):
#RestController
public class ShoppingListResource {
#Autowired
private ProductRepository productRepository;
#Autowired
private UserRepository userRepository;
#Autowired
private ShoppingListRepository shoppinglistRepository;
#GetMapping("/user/lists/{id}")
public Set<Product> getShoppinglistProducts(#PathVariable int id) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String currentPrincipalName = authentication.getName();
ShoppingList shoppingList = shoppinglistRepository.findById(id).get();
String name = shoppingList.getUser().getUsername();
if(!Objects.equals(currentPrincipalName, name)) throw new IllegalOperation("You can only check your list(s)!");
// All lists are shown for a product
// Product p = productRepository.findById(10111).get();
// Set<ShoppingList> set = p.getShoppinglists();
// set.stream().forEach(e -> log.info(e.toString()));
// Only first product is shown for a list
return shoppingList.getProducts();
This is what hibernate does on the last row (only returns 1/2 products)
Hibernate: select products0_.list_id as list_id2_3_0_,
products0_.id as id1_3_0_,
product1_.id as id1_1_1_,
product1_.description as descript2_1_1_,
product1_.name as name3_1_1_,
product1_.price as price4_1_1_,
product1_.subcat_id as subcat_i5_1_1_ from shopping_product products0_ inner join product product1_ on products0_.id=product1_.id where products0_.list_id=?
As i said above, I can delete a product and it gets removed automatically from all existing lists, but when i try the same from ShoppingList entity does nothing:
Same controller
#DeleteMapping("/user/lists/{id}")
public ResponseEntity<String> deleteShoppinglist(#PathVariable int id) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String currentPrincipalName = authentication.getName();
ShoppingList shoppingList = shoppinglistRepository.findById(id).get();
String name = shoppingList.getUser().getUsername();
if(!Objects.equals(currentPrincipalName, name)) throw new IllegalOperation("You can only delete your list(s)!");
shoppinglistRepository.delete(shoppingList);
return ResponseEntity.ok().body("Shopping list deleted");
}
Also, when i try to add/delete product from an existing list, does nothing.
This is my repo with full code, if you'd like to test directly (dev branch is up to date):
https://github.com/dragostreltov/online-store/tree/dev
You can just use admin admin as authentication (on the H2 console too). More details on the readme.
All DB data at app start is inserted from a .sql file.
I checked other similar questions and tried different methods on my ShoppingList entity (on the delete issue), like:
#PreRemove
public void removeListsFromProducts() {
for(Product p : products) {
p.getShoppinglists().remove(this);
}
}
Spring/Hibernate: associating from the non-owner side
And still doesn't work.
UPDATE:
I found out what issues I was having, I'll post an answer with the solution.
For anyone who's got the same/similar problems as I did, this is how I resolved them:
For point 1
(Hibernate only retrieves the first product from a shoppingList (Set))
I made multiple tests on my retrieve method and found out my Set was only containing 1 object, despite calling .add(product) twice.
As you can see, I'm using HashSet for both entities:
In Product (owner):
private Set<ShoppingList> shoppinglists = new HashSet<>();
In ShoppingList (mappedBy):
private Set<Product> products = new HashSet<>();
Thanks to this answer: https://stackoverflow.com/a/16344031/18646899
I learnt:
HashSet (entirely reasonably) assumes reflexivity, and doesn't check for equality when it finds that the exact same object is already in the set, as an optimization. Therefore it will not even call your equals method - it considers that the object is already in the set, so doesn't add a second copy.
In particular, if x.equals(x) is false, then any containment check would also be useless.
Taking this into account, I overwrote the hashCode() and equals() methods in Product.class and now
shoppingList.getProducts()
works as expected.
For point 2
(not being able to delete associations of non-owner entity before deleting the row from it's table)
Added lazy fetch and cascade to Product #ManyToMany:
#ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH})
And added the following methods:
In Product class:
public void addShoppinglist(ShoppingList list) {
this.shoppinglists.add(list);
list.getProducts().add(this);
}
public void removeShoppinglist(ShoppingList list) {
this.shoppinglists.remove(list);
list.getProducts().remove(this);
}
In ShoppingList class:
public void addProduct(Product product) {
this.products.add(product);
product.getShoppinglists().add(this);
}
public void removeProduct(Product product) {
this.products.remove(product);
product.getShoppinglists().remove(this);
}
Added #Transactional and modified the method inside the controller (ShoppingListResource) for deleteShoppingList:
#RestController
public class ShoppingListResource {
...
#Transactional
#DeleteMapping("/user/lists/{id}")
public ResponseEntity<String> deleteShoppinglist(#PathVariable int id) {
...
shoppingList.getProducts().stream().forEach(e -> {
e.removeShoppinglist(shoppingList);
});
shoppinglistRepository.delete(shoppingList);
return ResponseEntity.ok().body("Shopping list deleted");
}
}
And now this is working as expected, the shoppingList's associations are deleted first then the shoppingList itself.

Saving Entity with Cached object in it causing Detached Entity Exception

I'm trying to save an Entity in DB using Spring Data/Crud Repository(.save) that has in it another entity that was loaded through a #Cache method. In other words, I am trying to save an Ad Entity that has Attributes entities in it, and those attributes were loaded using Spring #Cache.
Because of that, I'm having a Detached Entity Passed to Persist Exception.
My question is, is there a way to save the entity still using #Cache for the Attributes?
I looked that up but couldn't find any people doing the same, specially knowing that I am using CrudRepository that has only the method .save(), that as far as I know manages Persist, Update, Merge, etc.
Any help is very much appreciated.
Thanks in advance.
Ad.java
#Entity
#DynamicInsert
#DynamicUpdate
#Table(name = "ad")
public class Ad implements SearchableAdDefinition {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
private User user;
#OneToMany(mappedBy = "ad", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private Set<AdAttribute> adAttributes;
(.....) }
AdAttribute.java
#Entity
#Table(name = "attrib_ad")
#IdClass(CompositeAdAttributePk.class)
public class AdAttribute {
#Id
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "ad_id")
private Ad ad;
#Id
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "attrib_id")
private Attribute attribute;
#Column(name = "value", length = 75)
private String value;
public Ad getAd() {
return ad;
}
public void setAd(Ad ad) {
this.ad = ad;
}
public Attribute getAttribute() {
return attribute;
}
public void setAttribute(Attribute attribute) {
this.attribute = attribute;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
#Embeddable
class CompositeAdAttributePk implements Serializable {
private Ad ad;
private Attribute attribute;
public CompositeAdAttributePk() {
}
public CompositeAdAttributePk(Ad ad, Attribute attribute) {
this.ad = ad;
this.attribute = attribute;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CompositeAdAttributePk compositeAdAttributePk = (CompositeAdAttributePk) o;
return ad.getId().equals(compositeAdAttributePk.ad.getId()) && attribute.getId().equals(compositeAdAttributePk.attribute.getId());
}
#Override
public int hashCode() {
return Objects.hash(ad.getId(), attribute.getId());
}
}
Method using to load Attributes:
#Cacheable(value = "requiredAttributePerCategory", key = "#category.id")
public List<CategoryAttribute> findRequiredCategoryAttributesByCategory(Category category) {
return categoryAttributeRepository.findCategoryAttributesByCategoryAndAttribute_Required(category, 1);
}
Method used to create/persist the Ad:
#Transactional
public Ad create(String title, User user, Category category, AdStatus status, String description, String url, Double price, AdPriceType priceType, Integer photoCount, Double minimumBid, Integer options, Importer importer, Set<AdAttribute> adAtributes) {
//Assert.notNull(title, "Ad title must not be null");
Ad ad = adCreationService.createAd(title, user, category, status, description, url, price, priceType, photoCount, minimumBid, options, importer, adAtributes);
for (AdAttribute adAttribute : ad.getAdAttributes()) {
adAttribute.setAd(ad);
/* If I add this here, I don't face any exception, but then I don't take benefit from using cache:
Attribute attribute = attributeRepository.findById(adAttribute.getAttribute().getId()).get();
adAttribute.setAttribute(attribute);
*/
}
ad = adRepository.save(ad);
solrAdDocumentRepository.save(AdDocument.adDocumentBuilder(ad));
return ad;
}
I don't know if you still require this answer or not, since it's a long time, you asked this question. Yet i am going to leave my comments here, someone else might get help from it.
Lets assume, You called your findRequiredCategoryAttributesByCategory method, from other part of your application. Spring will first check at cache, and will find nothing. Then it will try to fetch it from Database. So it will create an hibernate session, open a transaction, fetch the data, close the transaction and session. Finally after returning from the function, it will store the result set in cache for future use.
You have to keep in mind, those values, currently in the cache, they are fetched using a hibernate session, which is now closed. So they are not related to any session, and now at detached state.
Now, you are trying to save and Ad entity. For this, spring created a new hibernate session, and Ad entity is attached to this particular session. But the attributes object, that you fetched from the Cache are detached. That's why, while you are trying to persist Ad entity, you are getting Detached Entity Exception
To resolve this issue, you need to re attach those objects to current hibernate session.I use merge() method to do so.
From hibernate documentation here https://docs.jboss.org/hibernate/orm/3.5/javadocs/org/hibernate/Session.html
Copy the state of the given object onto the persistent object with the same identifier. If there is no persistent instance currently associated with the session, it will be loaded. Return the persistent instance. If the given instance is unsaved, save a copy of and return it as a newly persistent instance. The given instance does not become associated with the session. This operation cascades to associated instances if the association is mapped with cascade="merge".
Simply put, this will attach your object to hibernate session.
What you should do, after calling your findRequiredCategoryAttributesByCategory method, write something like
List attributesFromCache = someService.findRequiredCategoryAttributesByCategory();
List attributesAttached = entityManager.merge( attributesFromCache );
Now set attributesAttached to your Ad object. This won't throw exception as attributes list is now part of current Hibernate session.

SpringBoot: Is this correct way to save a new entry which has ManyToOne relationship?

I have two entities Person and Visit
Person has OneToMany relationship with Visit.
I was wondering if I want to save an new entry of Visit, and interm of using RestController. Is my approach correct? Or is there another way which is more efficient?
So I have the following controller which takes a VisitModel from the RequestBody, is it a correct way to call it like so?
VisitModel has the ID of person, and the needed properties for the Visit entity. I use the ID of person to look up in the personRepository for the related Person entry, whereafter I issue it to a new instance of Visit and then use the visitRepository to save it.
#RequestMapping(value="", method=RequestMethod.POST)
public String checkIn(#RequestBody VisitModel visit) {
Person person = personRepository.findById(visit.personId);
Visit newVisit = new Visit(visit.getCheckIn, person);
visitRepository.save(newVisit);
return "success";
}
The Visit entity looks as following
#Entity
public class Visit {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
#JsonProperty("check_in")
private Date checkIn;
#JsonProperty("check_out")
private Date checkOut;
#ManyToOne
#JoinColumn(name="personId")
private Person person;
public Visit(Date checkIn, Person person) {
this.checkIn = checkIn;
this.person = person;
}
public Date getCheckIn() {
return checkIn;
}
public void setCheckIn(Date checkIn) {
this.checkIn = checkIn;
}
public Date getCheckOut() {
return checkOut;
}
public void setCheckOut(Date checkOut) {
this.checkOut = checkOut;
}
public Person getPerson() {
return person;
}
}
I want to know of the following approach is correct. Or is there another way which is better?
You don't need to get a Person from the database to associate it with a Visit, of course. Because of, you need to have only id of a Person to save it in the foreign key column personId.
If you use JPA EntityManager
Person person = entityManager.getReference(Person.class, visit.personId);
for Hibernate Session
Person person = session.load(Person.class, visit.personId);
This methods just create a proxy and don't do any database requests.
With Hibernate Session I used new Person(personId) as #MadhusudanaReddySunnapu suggested. Everything worked fine.
What is the difference between EntityManager.find() and EntityManger.getReference()?
Hibernate: Difference between session.get and session.load
Yes, that seems to me to be the standard way to map a bidirectional relationship. EDIT: The personId column points to the "id" field of the Person entity.Eg:
#Id
private Long id;
UPDATE: 1: The VisitModel is a 'DTO' or Data Transfer Object. Any separate package is fine. You could consider putting them into a separate jar, so that anyone using your API (with java) can use the jar to create the data before making the call. 2) The way you save it is fine as far as I can see.

I need help for persisting into oracle database

There is a problem about generating id while persisting into database.
I added the following code to my jpa entity file, however I'm getting 0 for personid.
#Id
#Column(unique=true, nullable=false, precision=10, name="PERSONID")
#SequenceGenerator(name="appUsersSeq", sequenceName="SEQ_PERSON", allocationSize=1)
#GeneratedValue(strategy=GenerationType.SEQUENCE, generator = "appUsersSeq")
private long personid;
EjbService:
#Stateless
public class EjbService implements EjbServiceRemote {
#PersistenceContext(name = "Project1245")
private EntityManager em;
#Override
public void addTperson(Tperson tp) {
em.persist(tp);
}
}
0 is default value for long type. The id will be set after invoking select query for the related sequence, which commonly is executed when you persist the entity. Are you persisting the entity? In case yes, post the database sequence definition to check it.

JPA unable to assign a new persisted entity in a many to one relationship

I have to JPA Entities defined with a bidirectional relationship many to one, hereby:
#Entity
public class Department implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#SequenceGenerator(name="DEPARTAMENTO_ID_GENERATOR",sequenceName="DEPARTAMENTO_SEQ")
#GeneratedValue(strategy=GenerationType.SEQUENCE,generator="DEPARTAMENTO_ID_GENERATOR")
#Column(name="DEP_ID")
private long id;
#Column(name="DEP_DESC")
private String desc;
//bi-directional many-to-one association to Academico
#OneToMany(mappedBy="department")
private Set<Proffesor> proffesors;
//getters and setters
}
#Entity
#Table(name="ACADEMICOS")
public class Proffesor implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#SequenceGenerator(name="ACADEMICOS_ID_GENERATOR", sequenceName="ACADEMICOS_SEQ")
#GeneratedValue(strategy=GenerationType.SEQUENCE,generator="ACADEMICOS_ID_GENERATOR")
#Column(name="ACD_ID")
private long id;
#ManyToOne(cascade={CascadeType.PERSIST,CascadeType.MERGE})
#JoinColumn(name="ACD_DEPADSCRITO_DEP")
private Department department;
// getters and setters.
}
After in a transactional Spring service I have the next code to manipulate the entities in this way.
#Transactional (propagation=Propagation.REQUIRED)
public void createDepartmentWithExistentProffesor(String desc,Long idAvaiableProf) {
// new department
Department dep = new Department();
dep.setDesc(desc);
HashSet<Proffesor> proffesors = new HashSet<Proffesor>();
dep.setProffesors(proffesors);
// I obtain the correct attached Proffesor entity
Proffesor proffesor=DAOQueryBasic.getProffesorById(idAvaiableProf);
// I asign the relationship beetwen proffesor and department in both directions
dep.addProffesors(proffesor);
// Persists department
DAODataBasic.insertDepartment(dep);
// The id value is not correct then Exception ORA-0221
System.out.println("SERVICIO: Departamento creado con id: " + dep.getId());
}
As I said in the comments the id of the new Department persisted is not a real database id inside the transaction, then it is produced an exception
Exception in thread "main" org.springframework.orm.jpa.JpaSystemException: org.hibernate.exception.ConstraintViolationException: Could not execute JDBC batch update
........
Caused by: java.sql.BatchUpdateException: ORA-02291: integrity restiction (HIBERNATE_PRB.FK_ACD2DEP) violated - primary key don't found
I've tried in a test, persist the new departmen entity with no relationship with Proffesor and I've seen that the id of the new department persisted entity has not a valid value inside the transaction but out of the transaction already the id has a correct value.
But I need the correct value inside the transaction.
Can anybody help me?
Thank you in advance.
try this
#OneToMany(mappedBy="department",cascade = CascadeType.PERSIST)
private Set<Proffesor> proffesors;

Resources