I am using Hibernate 4.1 and I’m getting the following exception whenever I try and store a list of transient tags that have been added to a collection for an object.
org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.myapplication.domain.Tag
at org.hibernate.engine.internal.ForeignKeys.getEntityIdentifierIfNotUnsaved(ForeignKeys.java:249)
at org.hibernate.type.EntityType.getIdentifier(EntityType.java:459)
at org.hibernate.type.ManyToOneType.nullSafeSet(ManyToOneType.java:132)
at org.hibernate.persister.collection.AbstractCollectionPersister.writeElement(AbstractCollectionPersister.java:811)
Now, I need to explain things a bit. My Tag class has a collection of synonyms. Before I added this relationship, I never got this exception and everything worked fine:
<class name="com.myapplication.domain.Tag">
<id name="id" column="tag_id" type="long">
<generator class="native"/>
</id>
<property name="term" index="termIndex" unique="true" not-null="true" />
<property name="description" />
<property name="approved" />
<many-to-one name="masterTag" column="master_tag_id"
class="com.myapplication.domain.Tag" />
<bag name="synonyms">
<key column="master_tag_id" />
<one-to-many class="com.myapplication.domain.Tag" />
</bag>
</class>
Here is how an object maps tags. The Tags class is just a component class. Tags.list is the actual collection that Hibernate is persisting.
<component name="tags">
<bag name="list" table="user_to_tag">
<key column="user_id" />
<many-to-many class="com.myapplication.domain.Tag" column="tag_id"/>
</bag>
</component>
The tags I am trying to save in the database actually don’t contain any synonyms at all, so I don’t believe it has anything to do with transient synonym tags. I don’t want these to cascade, which is why I have not given it a cascade attribute on the <bag> tag.
User’s will typically insert tags into the system via a list of strings. I did it this way because I want to create tags or link to existing tags at the same time on the fly whenever the object containing tags is saved. Maybe this is more trouble than it’s worth? If I only let them link to tags that exist, then I know I can solve my problem. Perhaps the complexity of dealing with on-the-fly tag creation in Hibernate is a bit more than it can deal with?
This is the method I am using to save new/existing tags all in one go. Whenever an object is saved to the database, it first calls into this method to persist the tags collection:
public void saveAll(Tags tags) {
List<Tag> tagsToOverwrite = new ArrayList<Tag>();
Iterator<Tag> i = tags.getList().iterator();
while(i.hasNext()) {
Tag tag = i.next();
if(stopListService.hasWord(tag.getTerm())) {
i.remove();
} else {
if(tag.isTransient()) {
Tag dbTag = findByTerm(tag.getTerm());
if(dbTag == null) {
save(tag);
} else {
tagsToOverwrite.add(dbTag);
}
}
}
}
tags.overwriteAll(tagsToOverwrite);
}
For some reason, it fails on Tag dbTag = findByTerm(tag.getTerm()); That’s when it throws the TransientObjectException. This method is just a simple query:
public Tag findByTerm(final String term) {
Query query = getCurrentSession().createQuery(
"from Tag tag " +
"where tag.term = :term "
);
return (Tag) query
.setParameter("term", term.toLowerCase())
.setCacheable(true)
.uniqueResult(); // This is where the exception gets thrown specifically
}
I am not sure why this method would throw this exception, because all I want to do is simply check to see if it’s in the database or not before I save it. Why is complaining about objects not being saved when all I am doing is querying it?
If the tags list only contains a single tag, the method works. It only fails when the number of transient tags to save is 2 or more. For example, this fails:
@Test
public void saveShouldPersistTransientTags() {
User user = userRepository.find(1);
// makes list of 3 tags that are transient (id = 0)
user.getTags().setTagsAsString("training learning business");
userRepository.save(user);
flush();
user = userRepository.find(1);
assertEquals(3, user.getTags().getList().size());
}
Here is the UserRepository.save() method:
@Override
public void save(User user) {
tagRepository.saveAll(user.getTags());
super.save(user);
}
As you can see, I am trying to save all of the transient tags first (or look them up in the database) before the user is saved. Yet, Hibernate seems to want to persists the user.tags.list collection during the call tagRepository.saveAll(user.getTags()); in the location where the exception is being thrown anyway, even though I never asked it to.
Of course, if I remove the synonyms collection from the Hibernate mapping, this test passes without any problems. It only fails when I enable the synonym collection, and it only fails when it gets to the learning tag (the training tag is not found in the database, and is persisted just fine.)
Any idea as to why the synonym collection is causing this problem?
If I can’t figure this out soon, in the interest of saving time, I am highly tempted to just use JDBC for this, as Hibernate’s complexity in cases like this can’t be such a good thing. I think this may even be a bug given the behaviour. It still doesn’t make any sense to me.
When you are ready to query Hibernate may look at its managed objects. Those that are dirty it will attempt to flush to the database so the query is hitting an up-to-date datastore. Somewhere in your code you are adding unmanaged (probably new?) synonyms to the collection, then when Hibernate tries to ‘help’ you by flushing the dirty object you get that exception.