The database table structure is the following:
id INT
extId VARCHAR
name VARCHAR
parent INT (references self.id)
Here is the entity
#Entity
#Table(name = "categories")
public class Category
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private int id;
#ManyToOne
#JoinColumn(name = "parent", referencedColumnName = "id")
private Category parent;
#org.hibernate.annotations.OrderBy(clause = "name ASC")
#OneToMany(fetch = FetchType.EAGER, mappedBy = "parent", cascade = CascadeType.ALL)
private Set<Category> children = new HashSet<>();
#Column(name = "extId")
private String extId;
#Column(name = "name")
private String name;
public void addChild(Category child)
{
child.setParent(this);
this.children.add(child);
}
//getters and setters ...
}
In the very beginning there is only one entity:
{
id: 0
extId: ''
name: 'Category'
parent: null
children: Set<Category>{/*empty*/}
}
This entity is fetched in the beginning of program and assigned to another class
Later this class performs addition of new Category as a child to existing root (the one that was fetched in the beginning) property
Addition of child is done like this:
Session session = HibernateSessionFactory.getInstance().getFactory().openSession();
//gather data
Category child = new Category();
//set name
//set extId
this.rootCategory.addChild(child);
Transaction tx = session.beginTransaction();
session.save(this.rootCategory);
tx.commit();
session.close();
After this instead of expected result in database:
Root(id(0), extId(''), parent(null), name('root'))
\-— Child(id(10), extId('art-0'), parent(0), name('child'))
I get the following result
Root(id(0), extId(''), parent(null), name('root'));
Root(id(10), extId(''), parent(null), name('root'))
\-— Child(id(11), extId('art-0'), parent(10), name('child'))
Notes:
New Session is created for each action and this session is acquired via Singleton SessionFactory
If I refresh() root entity before adding a child -- everything is OK, no duplicates
If I perform child addition immediately after fetching root entity -- no duplicates
What could be the reason of this behavior (assuming two different sessions)? And how can it be fixed?
After fetching from db at the beginning of your program, rootCategory becomes a detached object.
Later when you want to use it in another session, you need to reattach it to this session. Instead of session.save(), you can use session.update()
this.rootCategory.addChild(child);
Transaction tx = session.beginTransaction();
session.update(this.rootCategory);
tx.commit();
session.close();
If you are closing the session after fetching rootCategory then the rootCategory object becomes a detached object according to the hibernate lifecycle and using Session.save() on it will create a new row for it. Therefore you either need to fetch, add the child and save the rootCategory object in the same session or use refresh to tell hibernate that it is not a new object but one that is already saved.
Related
Let say I have an app to handle a collection of books.
My app allow to add a new book to the library. When creating the book, user can select the Author in the list, and if the author doesn't exist yet, he's able to add him to the list, providing his name to a form field.
When the form is filled, data are sent to a WS, something like
{
"name" : "The Book name"
"author" : {
"name" : "author's name"
}
}
Then I map json into my entity which would be
Book :
#Entity
#Table(name = "book")
public class Book{
#Id
#Column(name = "id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "name")
private String name;
#ManyToOne(fetch = FetchType.LAZY)
private Author author;
}
Author
#Entity
#Table(name = "author")
public class Author{
#Id
#Column(name = "id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "name")
private String name;
#OneToMany(mappedBy = "author", cascade = { CascadeType.ALL })
private List<Book> books;
}
This will not work as if user tries to add a new author, when I'll try to .save() I'll get an error :
org.hibernate.TransientPropertyValueException: object references an
unsaved transient instance
Is there a way to handle the case with Spring-Data-Jpa, or do I have to check manually that I got an author id in the json, and if not - meaning that this is a new author - mannually run the author creation and then save the new book?
Thx!
As you're guessing, and as the Javadoc says, cascade operations that must be cascaded to the target of the association". However, be sure you understand that the mappedBy defines the owning entity of the relationship. The owning entity is the entity that actually does the persisting operations, unless overridden by a cascade setting. In this case Child is the owning entity.
#Entity
public class Parent {
#Id #GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#OneToMany(mappedBy="parent")
private Set<Child> children;
The cascade setting on the Parent works when you create a Set of children and set it into the Parent and then save the Parent. Then the save operation will cascade from the Parent to the children. This is a more typical and the expected use case of a cascade setting. However, it does cause database operations to happen auto-magically and this is not always a good thing.
A Cascade setting on the child will happen when the child is persisted, so you could put a cascade annotation there, but read on ...
#Entity
public class Child {
#Id #GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#ManyToOne(cascade=CascadeType.ALL)
private Parent parent;
You will persist both the parent and the child by persisting the child.
tx.begin();
Parent p = new Parent();
Child c = new Child();
c.setParent(p);
em.persist(c);
tx.commit();
and when you delete the child it will delete both the parent and the child.
tx.begin();
Child cFound = em.find(Child.class, 1L);
em.remove(cFound);
tx.commit();
em.clear();
this is where you have problems. What happens if you have more than one child?
em.clear();
tx.begin();
p = new Parent();
Child c1 = new Child();
Child c2 = new Child();
c1.setParent(p);
c2.setParent(p);
em.persist(c1);
em.persist(c2);
tx.commit();
All well and nice until you delete one of the children
em.clear();
tx.begin();
cFound = em.find(Child.class, 2L);
em.remove(cFound);
tx.commit();
then you will get an integrity constraint violation when the cascade propagates to the Parent but there is still a second Child in the database. Sure you could cure it by deleting all the children in a single commit but that's getting kind of messy isn't it?
Conceptually people tend to think that propagation goes from Parent to Child and so it is very counterintuitive to have it otherwise. Further, what about a situation where you don't want to delete the author just because the store sold all his or her books? In this case you might be mixing cascade, sometimes from child to parent and in other cases from parent to child.
Generally I think it is better to be very precise in your database code. It's much easier to read, understand, and maintain code that specifically saves the parent first then the child or children than to have an annotation somewhere else that I may or may not be aware of that is doing additional database operations implicitly.
There is class called Parent and i have properties called attributes and details.One parent can have may properties and similarly one parent can have many details.
class Parent{
#Id
#GeneratedValue
int parentId;
#OneToMany(mappedBy = "attributeComposite.parentId", cascade = CascadeType.ALL)
private Set<Attributes> attributes = new HashSet<>();
#OneToMany(mappedBy = "detailsComposite.parentId", cascade = CascadeType.ALL)
private Set<Details> attributes = new HashSet<>();
}
Now i have entered data like parent have 2 attributes and 4 details.I want to retrieve the data from criteria query which has parentId 1.So i am querying like this
Criteria criteria = session.createCriteria(Parent.class);
criteria = criteria.add(Restrictions.idEq(1));
criteria = criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
tempMapping = (Parent) criteria.uniqueResult();
Which is returning only one parent with parent id 1.But it has 4 attributes instead of 2 and 4 details.I think its joining the child data by joining each other. So how to get the complete parent entity data with out changing anything in childrens?
I have two tables with 'one to many' relationship. I use Jpa + Spring JpaRepository. Sometimes I have to get object from Database with internal object. Sometimes I dont't have to. Repositories always return object with internal objects.
I try to get 'Owner' from Database and I always get Set books; It's OK. But when I read fields of this internal Book , I get LazyInitializationException. How to get null instead of Exception?
#Entity
#Table(name = "owners")
#NamedEntityGraph(name = "Owner.books",
attributeNodes = #NamedAttributeNode("books"))
public class Owner implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "owner_id", nullable = false, unique = true)
private Long id;
#Column(name = "owner_name", nullable = false)
private String name;
#OneToMany(fetch = FetchType.LAZY,mappedBy = "owner")
private Set<Book> books= new HashSet<>(0);
public Worker() {
}
}
#Entity
#Table(name = "books")
#NamedEntityGraph(name = "Book.owner",
attributeNodes = #NamedAttributeNode("owner"))
public class Book implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "book_id", unique = true, nullable = false)
private Long id;
#Column(name = "book_name", nullable = false, unique = true)
private String name;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "owner_id")
private Owner owner;
public Task() {
}
}
public interface BookRepository extends JpaRepository<Book,Long>{
#Query("select t from Book t")
#EntityGraph(value = "Book.owner", type = EntityGraph.EntityGraphType.LOAD)
List<Book> findAllWithOwner();
#Query("select t from Book t where t.id = :aLong")
#EntityGraph(value = "Book.owner", type = EntityGraph.EntityGraphType.LOAD)
Book findOneWithOwner(Long aLong);
}
You are getting LazyInitializationException because you are accessing the content of the books Set outside the context of a transaction, most likely because it's already closed. Example:
You get an Owner from the database with your DAO or Spring Data repository, in a method in your Service class:
public Owner getOwner(Integer id) {
Owner owner = ownerRepository.findOne(id);
// You try to access the Set here
return owner;
}
At this point you have an Owner object, with a books Set which is empty, and will only be populated when someone wants to access its contents. The books Set can only be populated if there is an open transaction. Unfortunately, the findOne method has opened and already closed the transaction, so there's no open transaction and you will get the infamous LazyInitializationException when you do something like owner.getBooks().size().
You have a couple of options:
Use #Transactional
As OndrejM said you need to wrap the code in a way that it all executes in the same transaction. And the easiest way to do it is using Spring's #Transactional annotation:
#Transactional
public Owner getOwner(Integer id) {
Owner owner = ownerRepository.findOne(id);
// You can access owner.getBooks() content here because the transaction is still open
return owner;
}
Use fetch = FetchType.EAGER
You have fetch = FecthType.LAZY in you #Column definition and that's why the Set is being loaded lazily (this is also the fetch type that JPA uses by default if none is specified). If you want the Set to be fully populated automatically right after you get the Owner object from the database you should define it like this:
#OneToMany(fetch = FetchType.EAGER, mappedBy = "owner")
private Set<Book> books= new HashSet<Book>();
If the Book entity is not very heavy and every Owner does not have a huge amount of books it's not a crime to bring all the books from that owner from the database. But you should also be aware that if you retrieve a list of Owner you are retrieving all the books from all those owners too, and that the Book entity might be loading other objects it depends on as well.
The purpose of LazyInitializationException is to to raise an error when the loaded entity has lost connection to the database but not yet loaded data which is now requested. By default, all collections inside an entity are loaded lazily, i.e. at the point when requested, usually by calling an operation on them (e.g. size() or isEmpty()).
You should wrap the code that calls the repository and then works with the entity in a single transaction, so that the entity does not loose connection to DB until the transaction is finished. If you do not do that, the repository will create a transaction on its own to load the data, and close the transaction right after. Returned entity is then without transaction and it is not possible to tell, if ots collections have some elements or not. Instead, LazyInitializationException is thrown.
This problem seems occur often, but I couldn't find a working solution for my case. Problem is that Hibernate leaves the foreign key of one foreign key column empty (null). Strange thing is that I use the same setup of these two affected tables in another program and it works fine. Only difference is that I now added 2 other tables.
I checked the MySQL output (via show_sql option) and Hibernate inserts the parent table before the child. So the key should be there.
Parent:
public class Page {
#Id
#GeneratedValue
#Column(name="page_id")
private Integer id;
//****** THIS IS THE CHILD
#OneToMany(orphanRemoval=true, mappedBy="pageId", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private Set<Tag> tag = new HashSet<Tag>();
//****** THESE ARE THE NEW TABLES
#OneToOne(cascade=CascadeType.ALL)
private Video video;
#ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinColumn(name="creator_id")
private Creator creator;
Child:
public class Tag {
#Id
#GeneratedValue
private Integer id;
#Index(name = "tag")
#Column
private String tag;
#ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinColumn(name="page_id")
private Page pageId;
How it is saved (saveOrUpdate is necessary):
public static void save(SessionFactory sessionFactory, Page page) {
Session session = sessionFactory.openSession();
session.beginTransaction();
session.saveOrUpdate(page);
session.getTransaction().commit();
session.close();
}
Every suggestion are highly appreciated!
Are you managing both sides of the association? Meaning that you add the Tag to the Set<Tag> in Page and set the Page field on the Tag?
Before passing Page to the save method you should be managing the relationship as follows:
Page page = new Page();
Tag tag = new Tag();
//managing the both sides of the relationship
tag.setPage(page); //Setting Tags page field
page.getTags().add(tag); //Adding tag to the `Set<Tag>`
I've some probleme with merge. My update method works in this way:
void update(Parent parent) {
evict(parent);
merge(parent);
}
My classes:
Parent {
Long id;
List<Children> childrens;
#OneToMany(targetEntity =ChildrenImpl.class, fetch=FetchType.LAZY)
#JoinColumn(name="PARENT")
#org.hibernate.annotations.Cascade(value = org.hibernate.annotations.CascadeType.ALL)
List<Children> getChildrens(){...}
#Id
Long getId() {...}
}
Children{
Parent parent;
#ManyToOne(targetEntity = ParentImpl.class, fetch = FetchType.LAZY)
#org.hibernate.annotations.Cascade(value = org.hibernate.annotations.CascadeType.ALL)
#JoinColumn(name = "PARENT", nullable = false)
Parent getParent(){...}
}
When i create a new Parent(transient) object and add new childrens and trying updated(evict & merge) then logs show me this after flush hibernate session:
INSERT PARENT //everythings here is ok.
INSERT CHILDREN // but without parent id(id=null)
Order is good but children doesn't have parent id in insert. Everything works fine when Parent is persisted in database, then childrens always have a good id.
Any ideas what should I do to get id from transient object(from persisted is ok).
Regards
KZ.
You cannot get an id from a transient object, by definition a transient object does not have an id.
why are you doing a merge if you create a new object? When you create a new object, you should save it. If you change the values of an existing object, you should update it. You should be merging only if an object that was persistent becomes detached, and you need to reattach it.