Peter
Peter

Reputation: 333

Kotlin inheritance and JPA

I'm trying to implement inheritance with Kotlin and JPA. My abstract base class (annotated with @Entity) holds the ID (annotated with @Id and @GeneratedValue) and other metadata, like createDate, etc. I'm getting several errors from Hibernate, one for each field except the ID:

org.hibernate.tuple.entity.PojoEntityTuplizer - HHH000112: Getters of lazy classes cannot be final: com.example.BaseEntity.createDate

As I've read I need to include the open keyword for each property.

I have 3 questions regarding this:

  1. Why do I have to do that in the superclass, and don't need in subclass? I'm not overriding those properties.
  2. Why isn't it complaining about the ID?
  3. It seems to work without the open keyword, then why is the error logged?

Edit:

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
abstract class BaseEntity(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0,
    val createdAt: Instant = Instant.now()
)

@Entity
class SubClass(
    val someProperty: String = ""
) : BaseEntity()

I'm using the JPA plugin for Gradle, which I believe creates the noarg constructor, that's why I don't have to specify everything nullable.

Thank you!

Upvotes: 2

Views: 3834

Answers (1)

Denis Zavedeev
Denis Zavedeev

Reputation: 8297

The logged error has to do with lazy loading.

Hibernate extends entities at runtime to enable it. It is done by intercepting an access to properties when an entity is loaded lazily.

Kotlin has flipped the rules and all classes are final by default there. It is the reason why we're advised to add an open keyword.

If a property is not open hibernate cannot intercept access to it because final methods cannot be overridden. Hence the error.

Why isn't it complaining about the ID?

Because @Id is always loaded. There is no need to intercept access to it.

It seems to work without the open keyword, then why is the error logged?

The key word here is seems. It may introduce subtle bugs.

Consider the following @Entity:

@Entity
public class Book {
  @Id
  private Long id;
  private String title;

  public final Long getId() {
    return id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  public final String getTitle() {
    return title;
  }

  public void setTitle(String title) {
    this.title = title;
  }
}

And the @Test:

@Test
public void test() {
  EntityManager entityManager = entityManagerFactory.createEntityManager();
  entityManager.getTransaction().begin();
  // signal here
  Book book = new Book();
  book.setId(1L);
  book.setTitle("myTitle");  
  entityManager.persist(book);
  // noise
  entityManager.getTransaction().commit();
  entityManager.close();

  entityManager = entityManagerFactory.createEntityManager();
  entityManager.getTransaction().begin();
  // signal
  Book reference = entityManager.getReference(Book.class, 1L);
  String title = reference.getTitle();
  assertNull(title); // passes

  entityManager.getTransaction().commit();
  entityManager.close();
}

This test passes but it should not (and fails if getTitle is not final). This would be hard to notice

Why do I have to do that in the superclass, and don't need in subclass? I'm not overriding those properties.

Looks like Hibernate gives up when it sees final @Entity.

Add open to SubClass and you will the precious:

2019-05-02 23:27:27.500 ERROR 5609 --- [           main] o.h.tuple.entity.PojoEntityTuplizer      : HHH000112: Getters of lazy classes cannot be final: com.caco3.hibernateanswer.SubClass.someProperty 

See also:

PS

Did you forget to include @MappedSuperclass on BaseEntity?

Without the annotation it should fail with something like:

org.hibernate.AnnotationException: No identifier specified for entity: com.caco3.hibernateanswer.SubClass

Upvotes: 3

Related Questions