Adventures in Java Land: JPA disconnected entities

- Toni Schmidbauer Toni Schmidbauer ( Lastmod: 2024-05-08 ) - 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:

/java/images/swagger_post_500.png

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 case newSpeaker.persist() the entity is stored in the database and in the managed 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() or entitymanager.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:

/java/images/speaker_object_debugger.png

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:

/java/images/speaker_object_entitymanager_debugger.png

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 Id is already assigned to an entity object, Hibernate assumes that this is an entity in the DETACHED state (if the Id is auto-generated). For an entity to be persisted to the database it has to be transferred in the MANAGED state by calling entitymanager.merge()

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.