Defining JPA entities with Hibernate is really easy and well-documented, especially in Java, but what if we wanted to do it in Kotlin?
Let’s assume we have a database like the following:
Each Post
has a title and each Tag
has a name
.
We can map these two entities in Java with the following code:
@Entity @Table(name = "post") public class Post { @Id @GeneratedValue @Column(name = "id") private Long id; @Column(name = "title") private String title; @ManyToMany @JoinTable(name = "post_tag", joinColumns = @JoinColumn(name = "post_id"), inverseJoinColumns = @JoinColumn(name = "tag_id") ) private Set<Tag> tags = new HashSet<>(); public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public Set<Tag> getTags() { return tags; } public void setTags(Set<Tag> tags) { this.tags = tags; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Post tag = (Post) o; return Objects.equals(id, tag.id); } @Override public int hashCode() { return Objects.hash(id); } }
@Entity @Table(name = "tag") public class Tag { @Id @GeneratedValue @Column(name = "id") private Long id; @Column(name = "name") private String name; @ManyToMany(mappedBy = "tags") private Set<Post> posts = new HashSet<>(); public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Set<Post> getPosts() { return posts; } public void setPosts(Set<Post> posts) { this.posts = posts; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Tag tag = (Tag) o; return Objects.equals(id, tag.id); } @Override public int hashCode() { return Objects.hash(id); } }
What about Kotlin?
The first idea we might have is to map the two entities into simple data classes.
Let’s try…
@Entity @Table(name = "post") data class Post( @get:Id @get:GeneratedValue @get:Column(name = "id") val id: Long, @get:Column(name = "title") val title: String, @get:ManyToMany @get:JoinTable( name = "post_tag", joinColumns = [JoinColumn(name = "post_id")], inverseJoinColumns = [JoinColumn(name = "tag_id")] ) val tags: Set<Tag> = HashSet() )
@Entity @Table(name = "tag") data class Tag( @get:Id @get:GeneratedValue @get:Column(name = "id") val id: Long, @get:Column(name = "name") val name: String, @get:ManyToMany(mappedBy = "tags") val posts: Set<Post> = HashSet() )
In this way, we can avoid boilerplate code, but according to the Spring Official Guide you shouldn’t use data classes.
We don’t use
data classes
withval
properties because JPA is not designed to work with immutable classes or the methods generated automatically bydata classes
. If you are using other Spring Data flavor, most of them are designed to support such constructs so you should use classes likedata class User(val login: String, …)
when using Spring Data MongoDB, Spring Data JDBC, etc.
So, how can we define entities in Kotlin?
In order to define working entities, following the Hibernate User Guide is highly recommended. These are some rules we are required to respect:
- An entity class must have a no-argument constructor, which may be public, protected, or package visibility. It may define additional constructors as well.
- An entity class needs not to be a top-level class.
- Technically Hibernate can persist final classes or classes with final persistent state accessor (getter/setter) methods. However, it is generally not a good idea as doing so will stop Hibernate from being able to generate proxies for lazy-loading the entity.
- Hibernate does not restrict the application developer from exposing instance variables and referencing them from outside the entity class itself. The validity of such a paradigm, however, is debatable at best.
Don’t forget that each accessor method should be declared open
, since by default Kotlin classes are final.
Why can’t we use final classes?
A central feature of Hibernate is the ability to load lazily certain entity instance variables (attributes) via runtime proxies. This feature depends upon the entity class being non-final or else implementing an interface that declares all the attribute getters/setters. You can still persist final classes that do not implement such an interface with Hibernate, but you will not be able to use proxies for fetching lazy associations, therefore limiting your options for performance tuning. For the very same reason, you should also avoid declaring persistent attribute getters and setters as final.
In conclusion, here is how to define working JPA entities with Hibernate in Kotlin.
@Entity @Table(name = "post") open class Post { @get:Id @get:GeneratedValue @get:Column(name = "id") open var id: Long? = null @get:Column(name = "title") open var title: String? = null @get:ManyToMany @get:JoinTable( name = "post_tag", joinColumns = [JoinColumn(name = "post_id")], inverseJoinColumns = [JoinColumn(name = "tag_id")] ) open var tags: Set<Tag> = HashSet() override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || javaClass != other.javaClass) return false val that = other as Post if (id != that.id) return false return true } override fun hashCode(): Int { return if (id != null) id.hashCode() else 0 } }
@Entity @Table(name = "tag") open class Tag { @get:Id @get:GeneratedValue @get:Column(name = "id") open var id: Long? = null @get:Column(name = "name") open var name: String? = null @get:ManyToMany(mappedBy = "tags") var posts: Set<Post> = HashSet() override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || javaClass != other.javaClass) return false val that = other as Tag if (id != that.id) return false return true } override fun hashCode(): Int { return if (id != null) id.hashCode() else 0 } }
Bonus
Extra 1
What if we didn’t want to declare all entity classes and their members as open
?
We can use the Kotlin allopen
plugin as described here.
Extra 2
Defining JPA entities are strongly correlated with JPA criteria queries. The next step is to learn how to use these entities in a type-safe way, while building robust criteria queries. The following article will teach you that.