Simplified Hibernate ORM with Panache and Kotlin
Hibernate ORM is the de facto standard Jakarta Persistence (formerly known as JPA) implementation and is well-known in the Java ecosystem. Hibernate ORM with Panache offers a new layer atop this familiar framework. This guide will not dive in to the specifics of either as those are already covered in the Hibernate ORM with Panache guide. In this guide, we’ll cover the Kotlin specific changes needed to use Hibernate ORM with Panache in your Kotlin-based Quarkus applications.
When using the Kotlin version of Hibernate ORM with Panache, note that the
PanacheEntity , PanacheQuery and PanacheRepository are in a different
package: io.quarkus.hibernate.orm.panache.kotlin .
|
First: an example
As we saw in the Hibernate with Panache guide, it allows us to extend the functionality in our entities and repositories (also known as DAOs) with some automatically provided functionality. When using Kotlin, the approach is very similar to what we see in the Java version with a slight change or two. To Panache-enable your entity, you would define it something like:
@Entity
class Person: PanacheEntity() {
lateinit var name: String
lateinit var birth: LocalDate
lateinit var status: Status
}
As you can see our entities remain simple. There is, however, a slight difference from the Java version. The Kotlin language doesn’t support the notion of static methods in quite the same way as Java does. Instead, we must use a companion object:
@Entity
class Person : PanacheEntity() {
companion object: PanacheCompanion<Person> { (1)
fun findByName(name: String) = find("name", name).firstResult()
fun findAlive() = list("status", Status.Alive)
fun deleteStefs() = delete("name", "Stef")
}
lateinit var name: String (2)
lateinit var birth: LocalDate
lateinit var status: Status
}
1 | The companion object holds all the methods not related to a specific instance allowing for general management and querying bound to a specific type. |
2 | Here there are options, but we’ve chosen the lateinit approach. This
allows us to declare these fields as non-null knowing they will be properly
assigned either by the constructor (not shown) or by hibernate loading data
from the database. |
These types differ from the Java types mentioned in those tutorials. For
Kotlin support, all the Panache types will be found in the
io.quarkus.hibernate.orm.panache.kotlin package. This subpackage allows
for the distinction between the Java and Kotlin variants and allows for both
to be used unambiguously in a single project.
|
In the Kotlin version, we’ve simply moved the bulk of the
active
record pattern
functionality to the companion object
. Apart from this
slight change, we can then work with our types in ways that map easily from
the Java side of world.
完整源码
We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.
Clone the Git repository: git clone https://github.com/quarkusio/quarkus-quickstarts.git
, or download
an archive.
The solution is located in the hibernate-orm-panache-kotlin-quickstart
directory.
Setting up and configuring Hibernate ORM with Panache and Kotlin
To get started using Hibernate ORM with Panache and Kotlin, you can, generally, follow the steps laid out in the Java tutorial. The biggest change to configuring your project is the Quarkus artifact to include. You can, of course, keep the Java version if you need but if all you need are the Kotlin APIs then include the following dependency instead:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache-kotlin</artifactId> (1)
</dependency>
1 | Note the addition of -kotlin on the end. Generally you’ll only need this
version but if your project will be using both Java and Kotlin code, you can
safely include both artifacts. |
implementation("io.quarkus:quarkus-hibernate-orm-panache-kotlin") (1)
1 | Note the addition of -kotlin on the end. Generally you’ll only need this
version but if your project will be using both Java and Kotlin code, you can
safely include both artifacts. |
Using the repository pattern
Defining your entity
When using the repository pattern, you can define your entities as regular Jakarta Persistence entities.
@Entity
class Person {
@Id
@GeneratedValue
var id: Long? = null;
lateinit var name: String
lateinit var birth: LocalDate
lateinit var status: Status
}
Defining your repository
When using Repositories, you get the exact same convenient methods as with
the active record pattern, injected in your Repository, by making them
implement PanacheRepository
:
@ApplicationScoped
class PersonRepository: PanacheRepository<Person> {
fun findByName(name: String) = find("name", name).firstResult()
fun findAlive() = list("status", Status.Alive)
fun deleteStefs() = delete("name", "Stef")
}
All the operations that are defined on PanacheEntityBase
are available on
your repository, so using it is exactly the same as using the active record
pattern, except you need to inject it:
@Inject
lateinit var personRepository: PersonRepository
@GET
fun count() = personRepository.count()
Most useful operations
Once you have written your repository, here are the most common operations you will be able to perform:
// creating a person
var person = Person()
person.name = "Stef"
person.birth = LocalDate.of(1910, Month.FEBRUARY, 1)
person.status = Status.Alive
// persist it
personRepository.persist(person)
// note that once persisted, you don't need to explicitly save your entity: all
// modifications are automatically persisted on transaction commit.
// check if it's persistent
if(personRepository.isPersistent(person)){
// delete it
personRepository.delete(person)
}
// getting a list of all Person entities
val allPersons = personRepository.listAll()
// finding a specific person by ID
person = personRepository.findById(personId) ?: throw Exception("No person with that ID")
// finding all living persons
val livingPersons = personRepository.list("status", Status.Alive)
// counting all persons
val countAll = personRepository.count()
// counting all living persons
val countAlive = personRepository.count("status", Status.Alive)
// delete all living persons
personRepository.delete("status", Status.Alive)
// delete all persons
personRepository.deleteAll()
// delete by id
val deleted = personRepository.deleteById(personId)
// set the name of all living persons to 'Mortal'
personRepository.update("name = 'Mortal' where status = ?1", Status.Alive)
All list
methods have equivalent stream
versions.
val persons = personRepository.streamAll();
val namesButEmmanuels = persons
.map { it.name.toLowerCase() }
.filter { it != "emmanuel" }
The stream methods require a transaction to work.
|
For more examples, please consult the Java
version for complete details. Both APIs are the same and work identically
except for some Kotlin-specific tweaks to make things feel more natural to
Kotlin developers. These tweaks include things like better use of
nullability and the lack of Optional
on API methods.