After searching for quite a long time I'm wondering if my code is wrong or if it's simply impossible in Hibernate.
I'll use a fake example to explain my problem. Let's say I have three tables in my database, post, posttag and tag. A post can have multiple tags and a tag can be used my multiple posts, so it's a many-to-many association but there's also extra columns in the table between (posttag).
example entity relationship diagram
So, in my code, I have 3 entity and one more class for the composite key of posttag.
PostEntity:
@Entity
@Table(name = "post", catalog = "fakeExample")
public class PostEntity implements Serializable
{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "IDPOST", nullable = false)
private Integer idpost;
@OneToMany(mappedBy = "post", targetEntity = PostTagEntity.class, cascade = CascadeType.ALL, orphanRemoval = true)
private List<PostTagEntity> listOfPostTag = new ArrayList<>();
public void setIdpost(Integer idpost)
{
this.idpost = idpost;
}
public Integer getIdpost()
{
return this.idpost;
}
public void setListOfPostTag(List<PostTagEntity> listOfPostTag)
{
if (listOfPostTag != null)
{
this.listOfPostTag.clear();
this.listOfPostTag.addAll(listOfPostTag);
}
}
public List<PostTagEntity> getListOfPostTag()
{
return this.listOfPostTag;
}
}
TagEntity:
@Entity
@Table(name = "tag", catalog = "fakeExample")
public class TagEntity implements Serializable
{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "IDTAG", nullable = false)
private Integer idtag;
@OneToMany(mappedBy = "tag", targetEntity = PostTagEntity.class, cascade = CascadeType.ALL, orphanRemoval = true)
private List<PostTagEntity> listOfPostTag = new ArrayList<>();
public void setIdtag(Integer idtag)
{
this.idtag = idtag;
}
public Integer getIdtag()
{
return this.idtag;
}
public void setListOfPostTag(List<PostTagEntity> listOfPostTag)
{
if (listOfPostTag != null)
{
this.listOfPostTag.clear();
this.listOfPostTag.addAll(listOfPostTag);
}
}
public List<PostTagEntity> getListOfPostTag()
{
return this.listOfPostTag;
}
}
PostTagEntity:
@Entity
@Table(name = "posttag", catalog = "fakeExample")
public class PostTagEntity implements Serializable
{
@EmbeddedId
private PostTagEntityKey compositePrimaryKey = new PostTagEntityKey();
@Column(name = "EXTRACOLUMN")
private Integer extraColumn;
@ManyToOne
@JoinColumn(name = "IDPOST", referencedColumnName = "IDPOST", nullable = false)
@MapsId("idpost")
private PostEntity post;
@ManyToOne
@JoinColumn(name = "IDTAG", referencedColumnName = "IDTAG", nullable = false)
@MapsId("idtag")
private TagEntity tag;
public void setIdpost(Integer idpost)
{
this.compositePrimaryKey.setIdpost(idpost);
}
public Integer getIdpost()
{
return this.compositePrimaryKey.getIdpost();
}
public void setIdtag(Integer idtag)
{
this.compositePrimaryKey.setIdtag(idtag);
}
public Integer getIdtag()
{
return this.compositePrimaryKey.getIdtag();
}
public void setExtraColumn(Integer extraColumn)
{
this.extraColumn = extraColumn;
}
public Integer getExtraColumn()
{
return this.extraColumn;
}
public void setPost(PostEntity post)
{
this.post = post;
}
public PostEntity getPost()
{
return this.post;
}
public void setTag(TagEntity tag)
{
this.tag= tag;
}
public TagEntity getTag()
{
return this.tag;
}
}
PostTagEntityKey:
@Embeddable
public class PostTagEntityKey implements Serializable
{
@Column(name = "IDPOST", nullable = false)
private Integer idpost;
@Column(name = "IDTAG", nullable = false)
private Integer idtag;
public PostTagEntityKey()
{
}
public PostTagEntityKey(Integer idpost, Integer idtag)
{
this.idsalle = idsalle;
this.idequipement = idequipement;
}
public void setIdpost(Integer value)
{
this.idpost = value;
}
public Integer getIdpost()
{
return this.idpost;
}
public void setIdtag(Integer value)
{
this.idtag = value;
}
public Integer getIdtag()
{
return this.idtag;
}
public boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
if (obj == null)
{
return false;
}
if (this.getClass() != obj.getClass())
{
return false;
}
PostTagEntityKey other = (PostTagEntityKey) obj;
if (this.idpost == null ? other.idpost != null : !this.idpost.equals((Object) other.idpost))
{
return false;
}
if (this.idpost == null ? other.idpost != null
: !this.idtag.equals((Object) other.idtag))
{
return false;
}
return true;
}
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + ((idpost == null) ? 0 : idpost.hashCode());
result = prime * result + ((idtag == null) ? 0 : idtag.hashCode());
return result;
}
}
Also, I am using Spring, so here are the few class involved when I do an insert or something else. I don't think the problem come from here but just in case.
PostService:
public interface PostService
{
public List<PostEntity> findAll();
public Optional<PostEntity> findById(int var1);
public PostEntity save(PostEntity var1);
public void deleteById(int var1);
}
PostImpl:
@Service
public class PostImpl implements PostService
{
@Autowired
private PostRepository repository;
@Override
public List<PostEntity> findAll()
{
return this.repository.findAll();
}
@Override
public Optional<PostEntity> findById(int id)
{
return this.repository.findById(id);
}
@Override
public PostEntity save(PostEntity toSave)
{
return (PostEntity) this.repository.save(toSave);
}
@Override
public void deleteById(int id)
{
this.repository.deleteById(id);
}
}
PostRepository:
@Repository
public interface PostRepository extends JpaRepository<PostEntity, Integer>, JpaSpecificationExecutor<PostEntity>, PagingAndSortingRepository<PostEntity, Integer>
{
}
So when I need to insert a post, I just use something like this:
@Autowired
PostService postService;
public PostEntity createPost(PostEntity post)
{
return this.postService.save(post);
}
For me, the expected behaviour of Hibernate would be:
- when I insert a post, to insert the post and insert every postTag in listOfPostTag
- when I update a post, to remove every missing postTag in listOfPostTag, to add every new postTag in listOfPostTag, to update the change postTag in listOfPostTag and to update the post
- when I delete a post, to delete every postTag in listOfPostTag and to delete the post
However, when I try to insert a post, I have an error. And from the many tests I've done, it seems that Hibernate insert the post successfully and then tries to insert the postTags, but fails because the idPost in PostTagEntityKey is still null. I would have expected that Hibernate updated it with the id from the inserted post.
So my question is can hibernate do that in the case I described? Or do I have to do it by hand (by not using the cascade mode for insert/update)? The explanation might be that it's impossible with composite keys, bidirectional, something else or that it's just not something hibernate is supposed to do. I'd like to know if it's possible and if it is, what did I do wrong?
If it's not possible, I wonder what is the point of inserting things in cascade if you can't even do it for a thing as common as an intermediary table.
I haven't tried to code this fake example but I believe it would have the same result as I changed almost nothing from the original. Also I skipped the part where I create the postEntity because in my case it's parsed from JSON. I used the debugger and tried different things, so I'm almost sure the problem doesn't come from here. Every field is filled, even the idTag in PostTagEntityKey. It's just the idPost in PostTagEntityKey that is null because the post hasn't been inserted yet. And hibernate doesn't update it after inserting the post. I start to believe there's no way for the cascade mode to update it and maybe it's the case.
EDIT : So, thanks to the comment of @a.ghavidel, I realised I've never tried to set the post in posttag.
So I've changed the function setListOfPostTag in post entity like this :
public void setListOfPostTag(List<PostTagEntity> listOfPostTag)
{
if (listOfPostTag != null)
{
this.listOfPostTag.clear();
for(PostTagEntity postTag : listOfPostTag)
{
postTag.setPost(this);
this.listOfPostTag.add(postTag);
}
}
}
and the problem has changed as well. Before that modification, it was telling me that it couldn't insert postTag because idPost was null. And now it's telling me "org.hibernate.PersistentObjectException: detached entity passed to persist: com.fakeexample.TagEntity".
So I think, to retreive the primary key, the entity postTag needed the variable Post to be set. The List listOfPostTag in Post wasn't enough. So my original problem is now fixed I think.
My new problem is that hibernate seems to consider that the tag in postTag is "detached" and I'm not sure what that mean. In my case, the tag in postTag doesn't come from a function of hibernate, it has been parsed from json so maybe that's why. However I don't need it to be persisted, in theory I just need its id to insert the postTag and there is no cascade in postTag.
I've tried replacing CascadeType.All with CascadeType.Merge cause some people said it worked for them but when I do this it just doesn't insert any postTag when I insert a post. However it seems to work very well when I update a post.
I think I'm very close to the solution. I'm going to try a few things and I will edit this post if I find the answer.
EDIT 2 :
So, I've tried a few things and made some progress. The object is detached because it hasn't been created by hibernate. There is no problems during a merge but it can't be persisted.
A solution might be to do everything by hand in the create function...
But the proper solution would be to use the function getReference() from entityManager. It doesn't generate any unwanted select or update, it just create a proxy object and only require the id in parameter.
However the entityManager is not accessible in spring I think, but we can use the function getOne() from a repository which is mapped to the getReference() method. Basically, if I understood correctly, the function getOne() is supposed to be using lazy loading, so the object isn't loaded as long as we don't need it to be loaded.
I tried to use this function and indeed my code is now working correctly. But the problem is : the function getOne() is deprecated.
The function has been replaced by getById() but I'm really not sure it use lazy loading too because I'm already using this function a lot and not to create proxy object at all. Also, I know the attribute "fetch = FetchType.LAZY" cant be put in @OneToMany, so, if I didn't put it in my code I suppose I'm not using lazy loading and it will do a lot of unwanted select. Also I don't think I should be using lazy loading all the time neither, I heard it can be troublesome by sometime generating one select for each entity of a collection instead of a single select with lazy loading...
So I still need to make some research to know how to do it with non derprecated function.
EDIT 3 : Okay so no, the function I was using was findById and not getById. So getById is probably the solution to my problem. I'm gona check the doc to confirm and test it out.
FINAL EDIT : Well it's working for the insert but I have now a problem during the update. Basically it tells me the posttag already exists in the DB. Probably because I replace the listOfPostTag from the findById with a list I created myself by parsing it. The solution is probably to edit the objects in this list. Now that I understand how the things work in hibernate I need to review the entirety of my code to apply these principles. In the end, it seems that hibernate is not very friendly restful apis and we have to process every object so hibernate can recognize them.
To summerize, my problems were : first : consistency. I didn't set the post in postTag object. Second : attach and detach objects. I didn't used getById function to create an Object recognized by hibernate for the Taf Object. Third : updating. When I updated a post, I didn't update the objects in listOfPostTag, I just replaced it with another list, with only objects not recognized by hibernate. I should have updated the list Object by Object I guess.