I'm having some difficulties to put a specific scenario to work with Hibernate...
Considering the snippet above (simplified model), Entities A and D already exist. They will receive new Children B and E, and E has also new C Childrens. Then I call the "merge" method in Entity Manager.
#Entity
public class A {
#OneToMany(mappedBy = "a", fetch = FetchType.Lazy, cascade = CascadeType.All)
private List<B> bList;
}
#Entity
public class B {
#ManyToOne(fetch=FetchType.LAZY)
#JoinCollumn(name="ID_A")
private A a;
#OneToMany(mappedBy = "b", fetch = FetchType.Lazy, cascade = CascadeType.All)
private List<E> eList;
}
#Entity
public class C {
#ManyToOne(fetch=FetchType.LAZY)
#JoinCollumn(name="ID_E")
private E e;
}
#Entity
public class D {
#OneToMany(mappedBy = "d", fetch = FetchType.Lazy, cascade = CascadeType.All)
private List<E> bList;
}
// The "Double Parented" Children
#Entity
public class E {
#ManyToOne(fetch=FetchType.LAZY)
#JoinCollumn(name="ID_B")
private B b;
#ManyToOne(fetch=FetchType.LAZY)
#JoinCollumn(name="ID_D")
private D d;
#OneToMany(mappedBy = "e", fetch = FetchType.Lazy, cascade = CascadeType.All)
private List<C> cList;
}
public class Example {
public void newChildren() {
A a = getExistingAfromDatabase(); // defined elsewhere
E e = new E(); // then fills some attributes...
B b = new B(); // then fills some attributes...
b.getEs().add(e);
e.setB(b);
a.getBs().add(b);
b.setA(a);
edit(a);
}
public void edit(A myEntity) {
EntityTransaction trans = null;
EntityManager eMngr = getEntityManager(); // this is defined in another place
try {
trans = eMngr.getTransaction();
trans.begin();
eMngr.merge(myEntity);
trans.commit();
} catch (Exception e) {
if (trans != null && trans.isActive())
trans.rollback();
throw new DAOException("Error! " + e.getMessage(), e);
}
}
}
Whichever Entity I choose to merge first (A or D), I got the same exception: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: E.b -> B
Is Hibernate able to handle this with Cascade? If so, what have I done wrong?
Thanks a lot!
Make sure you always set both the Parent and the Child, so taking A and B you should have this kind of utility method in your base class (e.g. A):
public void addChild(B child) {
child.setA(this);
this.bList.add(child);
}
This utilities should prevent you from getting this type of exceptions.
Related
I have the following problem: I have three connected classes. I have annotated them but I am getting wrong results (described below):
#Entityd
#Table(name = "ClassA")
public class ClassA{
#Id
#GeneratedValue
private Long id = 0L;
...
#OneToMany(fetch = FetchType.EAGER,cascade=CascadeType.ALL)
#Fetch(FetchMode.SELECT)
#Column(name = "ClassBList")
private List<ClassB> listB;
...
}
#Entity
#Table(name="ClassB")
public class ClassB {
#Id
#GeneratedValue
private Long id = 0L;
...
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#Fetch(FetchMode.SELECT)
#Column(name = "ClassCList")
private List<ClassC> listC;
...
}
#Entity
#Table(name="ClassC")
public class ClassC {
#Id
#GeneratedValue()
private Long id = 0L;
...
#ElementCollection
private List<String> listD;
...
}
When I work with this structure for the first ClassA I create,save and load everything is ok. For a new instance of ClassA which I save to repo and load again, I suddenly have the strings of the first ClassA in listD.
The result I need is that every class is "independently" saved. So the collections of each class should hold unique (each one with its own id and sublists) objects.
What would be the best way (annotations) to model this classes in Java 8 with Spring Boot 2.2.0.M5 and javax.persistence-api 2.2 ?
EDIT:
I have now removed class B and rewrote classA to:
#Entity
#Table(name = "ClassA")
public class ClassA{
#Id
#GeneratedValue
private Long id = 0L;
...
#OneToMany(fetch = FetchType.LAZY, orphanRemoval = true, cascade = CascadeType.ALL)
#MapKey(name = "type")
private Map<String,Set<ClassC>> classCmap;
...
}
This is giving me an error like:
org.hibernate.AnnotationException: Use of #OneToMany or #ManyToMany targeting an unmapped class
How can I model/solve/annotate this?
If you don't need to query data based on listD, I would suggest to keep the list as text in the database and use a converter:
#Converter
public class ListDConverter implements AttributeConverter<List<String>, String> {
private ObjectMapper objectMapper = new ObjectMapper();
#Override
public String convertToDatabaseColumn(List<String> listD) {
try {
return objectMapper.writeValueAsString(listD);
} catch(IOException e){
return null;
}
}
#Override
public List<String> convertToEntityAttribute(String stringListD) {
if(stringListD == null){
return Collections.emptyList();
}
try {
return objectMapper.readValue(stringListD, new TypeReference<List<String>>() {});
}catch(IOException e){
return Collections.emptyList();
}
}
}
and in your ClassC entity class :
#Convert(converter = ListDConverter.class)
private List<String> listD;
Why do I like this approach :
No extra table and joins => better performance
Easier to read listD in the database
#ElementCollection describes a table. So your code is probably creating a "listD" table with one column of type string, with no primary key.
Also, do you really want to use the SELECT fetch mode? That's going to generate 1 + b + b*c queries when you could just implement your data as sets (since you have unique identifiers) and use JOIN, which would result in one and only one query.
See this site for an explanation on how to use #ElementCollection.
I have a many to one relationship: A *<-->1 B and I want to deserialize A from a JSON having B's primary key (B exists in db with that primary key):
{
"b": 1
}
I have tried the following:
#Entity
#Table(name = "table_a")
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class A implements Serializable {
#JsonIgnore
#ManyToOne(fetch=FetchType.LAZY)
#JoinColumn(name = "b", unique = true, nullable = false)
private B b;
}
and
#Entity
#Table(name = "table_b")
public class B implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
#OneToMany(mappedBy = "b")
private List<A> a = new ArrayList<>();
}
but object A is created with b = null. How can I deserialize A with b property correctly instantiated from db?
Note: I am using Jackson version 2.6.1.
You have several options and here is similar question :
#JsonCreator factory in B class (More info)
Custom deserializer
Custom ObjectIdResolver for #JsonIdentityInfo like
private class MyObjectIdResolver implements ObjectIdResolver {
private Map<ObjectIdGenerator.IdKey,Object> _items = new HashMap<>();
#Override
public void bindItem(ObjectIdGenerator.IdKey id, Object pojo) {
if (!_items.containsKey(id)) _items.put(id, pojo);
}
#Override
public Object resolveId(ObjectIdGenerator.IdKey id) {
Object object = _items.get(id);
return object == null ? getById(id) : object;
}
protected Object getById(ObjectIdGenerator.IdKey id){
Object object = null;
try {
//can resolve object from db here
//objectRepository.getById((Integer)idKey.key, idKey.scope)
object = id.scope.getConstructor().newInstance();
id.scope.getMethod("setId", int.class).invoke(object, id.key);
} catch (Exception e) {
e.printStackTrace();
}
return object;
}
#Override
public ObjectIdResolver newForDeserialization(Object context) {
return new MyObjectIdResolver();
}
#Override
public boolean canUseFor(ObjectIdResolver resolverType) {
return resolverType.getClass() == getClass();
}
}
And use it like this:
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,
resolver = MyObjectIdResolver.class,
property = "id", scope = B.class)
public class B {
// ...
}
Here is your case related gist demo more broad github project with some serialization thoughts
How can I pass the test? (was working before migrate my code to use repositories). The bs are stored in the database after save, but the object are not updated. What I have to do to achieve it?
Given these classes:
#Entity
public class A {
#Id
private String id;
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinColumn(name = "aId")
private Set<B> bs= new HashSet<B>();
...
}
#Entity
public class B {
#Id
#GeneratedValue
private int id;
private String aId;
private String foo;
...
}
And Repository:
#RepositoryDefinition(domainClass = A.class, idClass = String.class)
public interface ARepository {
...
void save(A a);
...
}
This test fail:
// "a" saved and flushed
B b = new B();
b.setAId(a.getId());
a.getBs().add(b);
ARepository.save(a);
assertTrue(b.getId() > 0);
repository.save() does persist (if the provided argument is transient) or merge (otherwise).
Since a is not transient, merge is performed, meaning that there is no persist operation that could be cascaded to bs.
You either have to save b explicitly or add b to a new a before the a is saved, so that persist is cascaded properly.
Probably, the reason is that B object is not in persisted state yet. As soon as it will be saved - you shouldn't get errors.
Should look like this:
// "a" saved and flushed
B b = new B();
BRepository.save(b)
b.setAId(a.getId());
a.getBs().add(b);
ARepository.save(a);
assertTrue(b.getId() > 0);
Also could you please provide stacktrace? Would be really helpful.
I have two entities:
parent :
public class UserGroup{
#OneToMany(fetch = FetchType.EAGER, mappedBy = "userGroup", cascade = CascadeType.ALL)
private List<User> users;
}
and child:
public class User{
#ManyToOne(fetch = FetchType.EAGER)
#Fetch(FetchMode.SELECT)
#JoinColumn(name = "user_group_id")
private UserGroup userGroup;
}
when i am trying to save UserGroup with one User in list of users, with this method:
#Transactional
public E save(E e) {
em.persist(e);
em.flush();
em.refresh(e);
return e;
}
my parent and child is getting saved, but user_group_id in child object is null.
Is there any solution?
By considering you are giving UserGroup object along with list of User to save method: so your code should be:
em.save(userGroup);
for(User user : UserGroup.getUsers())
{
user.setuser_group_id(userGroup.getUserGroupId());
em.save(user);
}
You have a bidirectional relationship. The correct way of saving it is putting both references in the entities.
You should do:
userGroup.getUsers().add(user);
user.setUserGroup(userGroup);
entityManager.persist(userGroup);
In your parent setter method of child, do this.
public void setChildren(Collection<Child> children) {
this.children = children;
for(Child child: this.children) {
child.setParent(this)
}
}
This should solve.
I have two entity classes that are in #OneToOne relation. The example code are as follow:
public class A {
#Id
private int id;
private String name;
#JoinColumn(name = "B_ID", referencedColumnName = "id")
#OneToOne(cascade=CascadeType.ALL)
private B b;
//setters and getters
}
public class B {
#Id
private int id;
private String name;
#OneToOne(mappedBy="b")
private A a;
//setter and getters
}
my question here is "Can I use setA(A a) method in class B. I mean like this . .
em.getTransaction().begin();
A aa = new A();
aa.setId(1);
aa.setName("JJ");
em.persist(aa);
B bb = new B();
bb.setId(1);
bb.setName("CC");
bb.setA(aa);
em.persist(bb);
em.getTransaction().commit();
When I tried like this, the foreign_key field in table A (B_ID) was saved as null.
Please help me.
Here , you have specified mappedBy in class B above private A a;. In a bidirectional relationship , mappedBy means that I am not the owner. So It means that A is the owner of the relationship.
In table of A , you will have a foreignkey for table of B. As A is the owner, A is suppose to cascade operations to B. Ideally you should try a.setB() and then persist a.
Try below:
em.getTransaction().begin();
//first create B.
B bb = new B();
bb.setId(1);
bb.setName("CC");
em.persist(bb);
//create A with B set in it.
A aa = new A();
aa.setId(1);
aa.setName("JJ");
aa.setB(bb);
em.persist(aa);
em.getTransaction().commit();
Or
em.getTransaction().begin();
//first create B.
B bb = new B();
bb.setId(1);
bb.setName("CC");
// no need to persist bb.
//create A with B set in it.
A aa = new A();
aa.setId(1);
aa.setName("JJ");
aa.setB(bb);
em.persist(aa); // because of cascade all , when you persist A ,
// B will also be persisted.
em.getTransaction().commit();
Use #Cascade({CascadeType.SAVE_UPDATE}) to cascade changes
public class B {
#Id
private int id;
private String name;
#OneToOne(mappedBy="b")
#Cascade({CascadeType.SAVE_UPDATE})
private A a;
//setter and getters
}
you need to add aa.setB(bb) before em.persist(bb)
em.getTransaction().begin();
A aa = new A();
aa.setId(1);
aa.setName("JJ");
em.persist(aa);
B bb = new B();
bb.setId(1);
bb.setName("CC");
aa.setB(bb);//this line should be added
bb.setA(aa);
em.persist(bb);
em.getTransaction().commit();
I had also the same problem. A class had already Setter Method for class B. My Problem was solved with setter in class B. In Class B i put method setter like this.
public void setA(A a){
this.a=a;
a.setB(this);
}
.