Adventures in Java Land: JPA disconnected entities
- - 5 min read
An old man tries to refresh his Java skills and does DO378. He fails spectacularly at the first real example but learns a lot on the way.
The exception
There is this basic example where you build a minimal REST API for
storing speaker data in a database. Quarkus makes this quite easy. You
just have to define your database connection properties in
resources/application.properties
and off you go developing your Java
Quarkus REST service:
quarkus.datasource.db-kind=h2
quarkus.datasource.jdbc.url=jdbc:h2:mem:default
quarkus.datasource.username=admin
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.log.sql=true
So next we define our entity class to be stored in the database. I will skip the import statements and any other code not relevant for this post.
// import statements skipped
@Entity
public class Speaker extends PanacheEntity {
public UUID uuid;
public String nameFirst;
public String nameLast;
public String organization;
@JsonbTransient
public String biography;
public String picture;
public String twitterHandle;
// Constructors, getters and setters, toString and other methods skipped
....
}
We define an entity Speaker
which extends the PanacheEntity
class. Panache is a thin wrapper around Hibernate providing convince
features. For example the base class PanacheEntity
defines a
autoincrement Id
column for us. This inherited Id
column is of
importance for understanding the problem ahead of us.
So next you define your SpeakerService
class which uses the
entity. Once again I will skip the imports and any code not relevant
for understanding the problem:
// imports omitted
@ApplicationScoped
public class SpeakerService {
// other code omitted
public Speaker create(Speaker speaker) {
speaker.persist();
return speaker;
}
We focus on the create
method here because the call to
speaker.persist()
was the reason for all the headache.
But we are still in coding mode and last but not least we define our
SpeakerResource
class, again everything not relevant for
understanding the problem was removed:
// import statements omitted
@Path("/speaker")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class SpeakerResource {
@Inject
SpeakerService service;
// other code omitted
@POST
@Transactional
public Speaker create(Speaker newSpeaker) {
service.create(newSpeaker);
return newSpeaker;
}
}
The root path for our SpeakerResource
is /speaker. We inject the
SpeakerService
and define a method create()
for creating a Speaker
. We
would like to be able to send @Post
requests to this endpoint and Jsonb
or Jackson, whichever we currently prefer, will deserialize the JSON
body in a Speaker
object for us.
Splendid, time to switch from coding mode to testing.
We launch that Quarkus application in developer mode
mvn quarkus:dev
Quarkus is so friendly and provides a swagger-ui in dev mode for testing
our endpoint. Super duper lets call the create()
endpoint via Swagger:
Because we are lazy we accept the default Swagger provides for us and just click Execute.
BOOM, 500 internal server error. And a beautiful Java exception:
org.jboss.resteasy.spi.UnhandledException: javax.persistence.PersistenceException: org.hibernate.PersistentObjectException: detached entity passed to persist: org.acme.conference.speaker.Speaker
What? Detached entity what does this mean and why?
Enlightenment
Behind the scenes Hibernate uses a so called EntityManager for managing entities. An Entity can be in the following states when managed by Hibernate:
- NEW: The entity object was just created and is not persisted to the database
- MANAGED: The entity is managed by a running Session and all changes
to the entity will be propagated to the database. After call to
entitymanager.persist()
or in our casenewSpeaker.persist()
the entity is stored in the database and in themanaged
state. - REMOVED: The entity is removed from the database. And finally
- DETACHED: The Entity was detached from the EntityManager, e.g. by
calling
entitymanager.detach()
orentitymanager.close()
.
See this blog for a way better explanation what is going on with entity states.
Ok, cool but why the hell is our Speaker
entity in the DETACHED
state? It was just created and never saved to the database before!
After checking the database (was empty), I started my Java debugger of choice (IntellJ, but use whatever fit's your needs. I'm to old for IDE vs Editor and Editor vs Editor wars).
So looking at the Speaker
entity before calling persist()
revealed the following:
The Speaker
object passed into create()
has an Id of 0 and all
the internal Hibernate fields are set to null. So this seems to
indicate that this Speaker
object is currently not attached to an
EntityManager
session. This might explain the DETACHED state.
I started playing around with EntityManager
and calling merge()
on the
speaker object. The code looked like this:
@ApplicationScoped
public class SpeakerService {
@Inject
EntityManager em;
// lots of code skipped
public Speaker create(Speaker speaker) {
var newSpeaker = em.merge(speaker);
newSpeaker.persist();
return speaker;
}
Looking at the newSpeaker
object returned by calling entitymanager.merge()
in the debugger revealed the following:
newSpeaker
has an Id of 1 (hm, why no 0?) and some those special
Hibernate fields starting with $$ have a value assigned. So for me
this indicates that the object is now managed by an EntityManager
session and in the MANAGED state.
And the Id
, already assigned to the original Speaker
object,
de-serialized form JSON is actually the reason for the beautiful
exception above.
Explanation
So after a little bit of internet search magic I found an explanation for the exception:
If an For more information see the Hibernate documentation. |
We can only call persist()
if the object is in the transient state,
to quote the Hibernate documentation:
transient: the entity has just been instantiated and is not associated with a persistence context. It has no persistent representation in the database and typically no identifier value has been assigned (unless the assigned generator was used).
And reading on we also get explanation for the detached state:
detached: the entity has an associated identifier but is no longer associated with a persistence context (usually because the persistence context was closed or the instance was evicted from the context)
Just removing the Id
from the POST request will solve the issue and
the example started to work.
This is also why the Id
column is different in the Speaker
object
(deserialized from JSON) and newSpeaker
object (create by calling
entitymanager.merge()
). The Speaker
Id got passed in from JSON,
and has nothing to do with the auto generated primary key Id within
our database. After calling entitymanager.merge()
the entity is
actually associated with a database session and the Id is
auto generated.
So maybe this is basic stuff, but it took me quite a few hours to understand what was going on.
Maybe this is also a bad example. Should one expose the Id
if it is
auto generated and only used internally? Or the code just needs to
handle that caseā¦ But this needs me more learning about API design.
Copyright © 2020 - 2024 Toni Schmidbauer & Thomas Jungbauer