Reputation: 769
So far my understand was that cascading only makes sense from the parent to the child. Now I'm wondering: does this also applies to OneToOne relationships?
I'm asking because I found in our code many (unidirectional) OneToOne relationships with cascading from child to parent. I tested it with "persist" and it seems to work - meaning the transient child is persisted together with the transient parent. While looking in the literature I found examples of such a cascading strategy. For instance on baeldung:
// CHILD
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id")
private Long id;
//...
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "address_id", referencedColumnName = "id")
private Address address;
}
//PARENT
@Entity
@Table(name = "address")
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id")
private Long id;
//...
@OneToOne(mappedBy = "address")
private User user;
}
In some other articles, the cascading is done the other way around. For instance on Vlad Mihalcea's blog.:
//CHILD
@Entity(name = "PostDetails")
@Table(name = "post_details")
public class PostDetails {
@Id
@GeneratedValue
private Long id;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id")
private Post post;
}
//PARENT
@Entity(name = "Post")
@Table(name = "post")
public class Post {
@Id
@GeneratedValue
private Long id;
@OneToOne(mappedBy = "post", cascade = CascadeType.ALL,
fetch = FetchType.LAZY, optional = false)
private PostDetails details;
}
So at the end are they both two valid options? I'm quite confused.
Upvotes: 3
Views: 1376
Reputation: 11551
Firstly, I am not a fan of CASCADE
. Why? Simply because if you don't know what is going on, why use a thing?
I can't answer your question because it depends on what you think is correct. But I can help shed some light on the issues.
You need to understand the "owning entity". From the Javadocs:
If the relationship is bidirectional, the non-owning side must use the mappedBy element of the OneToOne annotation to specify the relationship field or property of the owning side.
In the first case, the User
is the owning entity. In JPA, only the owning side will persist relationships. In this case only if the Address
field is set in the User
entity will JPA try to persist the relationship.
A persist operation would be:
public void initWithOwner() {
Address a = Address.builder().build();
userRepo.save(User.builder().address(a).build());
}
Which results in:
Hibernate: call next value for hibernate_sequence
Hibernate: call next value for hibernate_sequence
Hibernate: insert into address (id) values (?)
Hibernate: insert into users (address_id, id) values (?, ?)
No problem there. But what if the the Address
is already persisted?
public void initWithOwnerTransient() {
try {
Address a = addressRepo.save(Address.builder().build());
userRepo.save(User.builder().address(a).build());
} catch (Exception e) {
System.out.println("SAVE ERROR: " + e.getMessage());
}
}
Results in:
Hibernate: call next value for hibernate_sequence
Hibernate: insert into address (id) values (?)
Hibernate: call next value for hibernate_sequence
SAVE ERROR: detached entity passed to persist: com.example.jpaplay.Address; nested exception is org.hibernate.PersistentObjectException: detached entity passed to persist: com.example.jpaplay.Address
Using Cascade
has problems because it's tring to insert both but the current address already exists. You can work around it:
private void initWithOwnerTransientFix() {
try {
User u = userRepo.save(User.builder().build());
Address a = addressRepo.save(Address.builder().build());
u.setAddress(a);
u = userRepo.save(u);
} catch (Exception e) {
System.out.println("SAVE ERROR: " + e.getMessage());
}
}
Which results in this lovely bit of SQL:
Hibernate: call next value for hibernate_sequence
Hibernate: insert into users (address_id, id) values (?, ?)
Hibernate: call next value for hibernate_sequence
Hibernate: insert into address (id) values (?)
Hibernate: select user0_.id as id1_1_1_, user0_.address_id as address_2_1_1_, address1_.id as id1_0_0_ from users user0_ left outer join address address1_ on user0_.address_id=address1_.id where user0_.id=?
Hibernate: select address0_.id as id1_0_0_ from address address0_ where address0_.id=?
Hibernate: select user0_.id as id1_1_1_, user0_.address_id as address_2_1_1_, address1_.id as id1_0_0_ from users user0_ left outer join address address1_ on user0_.address_id=address1_.id where user0_.address_id=?
Hibernate: update users set address_id=? where id=?
Or perhaps you could delete the existing address record first if it exists, but then you have to check first and so on. Not a bad solution. Be sure to do it in a transaction.
Or you could avoid using cascade altogether. You have to save both the Address
and User
yourself, or use an already saved instance, which also means you have to check first, but at least you "know" what you are doing.
private void initWithOwnerNoCascade() {
try {
AddressNoCascade a = addressNoCascadeRepo.save(AddressNoCascade.builder().build());
userNoCascadeRepo.save(UserNoCascade.builder().address(a).build());
} catch (Exception e) {
System.out.println("SAVE ERROR: " + e.getMessage());
}
}
Results in:
Hibernate: call next value for hibernate_sequence
Hibernate: insert into address (id) values (?)
Hibernate: call next value for hibernate_sequence
Hibernate: insert into usersnocascade (address_id, id) values (?, ?)
So, which side should you put the Cascade
on? If you are going to insist on using it and making your life that much more complicated, it should go on the owning side of the relationship, in this case the User
.
Or, just leave it out and be happy.
Upvotes: 2