Monday, August 9, 2010

JPA Puzzle. When your entity misses some fields.

Today I encountered quite interesting problem. It wasted few hours of my life, so maybe if you read this you can save some. Example is simplified as much as possible.
@Entity
@Table(name = "PERSONS")
@NamedNativeQueries({
    @NamedNativeQuery(
        name = "getPersonBasic",
        query = "SELECT p.name, p.surname FROM persons where p.surname = ?",
        resultClass = Person.class),
    @NamedNativeQuery(
        name = "getPersonDetails",
        query = "SELECT p.name, p.surname, p.age, p.street, p.city FROM persons where p.surname = ?",
        resultClass = Person.class)
})
public class Person {
    
    private String name;

    @Id
    private String surname;

    private int age;

    private String street;

    private String city;

}

Now, in my application I am getting Person with surname "Stawicki" without details, by "getPersonBasic" query. Then I need detailed person in the same transaction, so I execute "getPersonDetails". And... I am getting Person without age, street and city. This three fields are null. Why? Do you already know? If not, look below for answer.













The problem is in cache, and in @Id which is only on surname field. First person is retrieved without details and stored in cache. Then we try to get person with details, but JPA doesn't understand difference between our queries. It queries the database, and identifies that if it builds entity from resulting ResultSet, it is going to have the same @Id as entity which is already in cache. If @Id is the same, logically it is the same entity. So it just returns it without building whole entity from ResultSet.

4 comments:

Tomasz Nurkiewicz said...

Very interesting, but isn't this persistence provider issue rather than the JPA itself? Will Hibernate (or EclipseLink or whatever you are using) behave differently? Whatever the root cause is, it looks like a bug in ORM framework.

BTW small mistake: NamedQueryNative ->NamedNativeQuery.

Unknown said...

Thanks Tomasz for pointing the mistake.

I don't think it is persistence provider issue. In fact, it is logical behavior. Persistent provider cannot tell the difference between entities if @Id is the same. It's one entity for it.

Henryk Konsek said...

Query cache == problems. I prefer to disable it by default. Then I enable turn the cache on for selected queries with Query.setHint().

Sławek Sobótka said...

Problem is conceptual. If we want to query for some properties only, than we should use DTO as a data container.

Using Entities as a data container causes dangerous context - some code may merge entity with null values. So we will delete some values form DB.

This approach leads to "leaky abstraction" problem. User of Entity must know its origin to be sure if he can or can not merge such an Entity.