Quarkus Hibernate with Panache Next
Introducing a new version of Panache
…which was never called Panache, by the way, it was called Hibernate ORM with Panache, or Hibernate Reactive with Panache, and this new version, is a new module, designed to unify both, and is currently called Hibernate with Panache (although the name could change).
Anyway, first a disclaimer: this is a new extension, which is experimental, which means everything about it can (and probably will) change: the extension name, the package names, the class names and even the API. We are releasing it because we feel it’s in a good shape to be tested and discussed by the community, and we hope to get better feedback before we commit to anything like names or API.
Please report feedback either on Zulip or via GitHub issues.
What was wrong with the old Panache APIs?
Mostly it’s fine, but it has a few drawbacks that are definitely annoying and dated:
-
We have two competing and incompatible APIs depending on whether we want to use blocking or non-blocking database drivers. The recent support of mixing Hibernate ORM and Hibernate Reactive in Quarkus means people no longer want to be blocked having to choose which entity type to extend, between the blocking
PanacheEntityand its non-blocking counterpart. -
Both Hibernate ORM with Panache and its Hibernate Reactive counterpart only support managed entities, and we want to add support for stateless entities (using
StatelessSession), but we cannot do that with the current Panache API. -
We want to add support for Jakarta Data, which allows defining type-safe queries using annotations, which are checked at build time, but the current active record version of Panache entities does not easily support this.
-
We currently have a split between active record and repository modes of operations, which is not satisfactory, and can be confusing to new users.
-
The static methods of active record entities have a hole in their types which require they be either assigned to known types (not
var) or have their type arguments explicitly specified. -
Method pointers cannot be used on the static methods of active record entities, causing confusion.
I could spend a long time explaining each of these issues, and the many things we experimented on to fix them, but you’re probably more interested in what we came up with.
So, what’s this new API?
It’s a new Quarkus extension, and it lets you specify your entity in a way which allows you to use it using a blocking JDBC driver, or a Reactive driver, as a managed entity, or as a stateless entity, with type-safe queries, or non-type-safe queries. It’s very flexible and it’s still clearly a continuation of the old Panache APIs, with a dash of Jakarta Data and all mixed in a way that we hope you will like.
The documentation has a lot of information as to how to set it up, and all the options, so I will focus on some bite-sized samples for this blog post. Here’s your first entity with type-safe queries:
@Entity
public class Cat extends PanacheEntity {
public String name;
public LocalDate birth;
public Breed breed;
public enum Breed {
CUTE, HAIRLESS;
}
public interface Repo extends PanacheRepository<Cat> {
@Find
Cat findByName(String name);
@HQL("where breed = CUTE")
List<Cat> findCute();
@HQL("delete from Cat where name = :name")
long deleteByName(String name);
@HQL("delete from Cat where breed = HAIRLESS")
long deleteHairless();
}
}
Here’s what you can notice right away:
-
Your entity extends
PanacheEntity(a familiar type, but in a new package), which means: by default it’s blocking and managed. -
Its operations are defined in a repository that is conveniently nested in the entity. No more mixing instance entity methods and
staticmethods, and yet you retain the close proximity of these operations. -
You can define type-safe operations with
@Findand@HQL -
You still have all the Panache type-unsafe operations in the
PanacheRepositorysupertype.
You could use it like this:
@Inject
Cat.Repo repo;
@Transactional
public void method() {
Cat cat = new Cat();
cat.name = "Lucky";
cat.birth = LocalDate.of(2015, 01, 12);
cat.breed = Cat.Breed.CUTE;
// Persist the cat
cat.persist();
// Make a change, no need to update it
cat.name = "Luckynou";
// Find our cat
cat = repo.findByName("Luckynou");
// Find cute cats
List<Cat> cuteCats = repo.findCute();
// Delete our cat
cat.delete();
// Delete queries
repo.deleteByName("Lucky");
repo.deleteHairless();
}
But if you find injecting the repository disruptive, you can also access it using the generated metamodel accessor
Cat_.repo(), which is super handy for completion and API discovery.
Here’s where it gets better
Want a reactive version of your entity for stateless sessions?
@Entity
public class Cat extends WithId.AutoLong implements PanacheEntity.Reactive.Stateless {
public String name;
public LocalDate birth;
public Breed breed;
public enum Breed {
CUTE, HAIRLESS;
}
public interface Repo extends PanacheRepository.Reactive.Stateless<Cat, Long> {
@Find
Uni<Cat> findByName(String name);
@HQL("where breed = CUTE")
Uni<List<Cat>> findCute();
@HQL("delete from Cat where name = :name")
Uni<Integer> deleteByName(String name);
@HQL("delete from Cat where breed = HAIRLESS")
Uni<Integer> deleteHairless();
}
}
It’s a bit longer, at the moment, but bear with us. Here’s the run-down:
-
You extend
WithId.AutoLongif you want a generatedLongidentifier. -
You implement
PanacheEntity.Reactive.Statelessto get reactive stateless operations by default. -
Your repository implements
PanacheRepository.Reactive.Statelessto get reactive stateless operations by default. -
Your queries return
Unifor non-blocking operation.
Otherwise, it’s the same API. Pick which mode of operation you want your entities to be based on by default. Mix and match modes for different entities if you want.
Here’s where it gets really better
Let’s say you declared your entity to be blocking and managed, but you want to also access it for a reactive and stateless part of your API.
You can always access every mode of operation on your entity or repository using supertype methods:
public class Code {
// By default, the Cat is blocking and managed
@Transactional
public void blockingManagedMethod() {
Cat cat = new Cat();
cat.name = "Lucky";
cat.birth = LocalDate.of(2015, 01, 12);
cat.breed = Cat.Breed.CUTE;
// Persist the cat
cat.persist();
// Make a change, no need to update it
cat.name = "Luckynou";
}
// But we can always call statelessReactive() and use reactive and stateless operations
@WithTransaction
public Uni<Void> reactiveStatelessMethod(Long catId) {
Cat cat = new Cat();
cat.name = "Lucky";
cat.birth = LocalDate.of(2015, 01, 12);
cat.breed = Cat.Breed.CUTE;
// Insert the cat
return cat.statelessReactive().insert()
.chain(() -> {
// Make a change, we need to update it
cat.name = "Luckynou";
return cat.statelessReactive().update();
});
}
}
You can even define multiple repositories for your different modes of operations:
@Entity
public class Cat extends PanacheEntity {
public String name;
public LocalDate birth;
public Breed breed;
public enum Breed {
CUTE, HAIRLESS;
}
public interface Repo extends PanacheRepository<Cat> {
@Find
Cat findByName(String name);
}
public interface TheOtherRepo extends PanacheRepository.Reactive.Stateless<Cat, Long> {
@Find
Uni<Cat> findByName(String name);
}
}
Merry Christmas
Just in time for the holidays, we’re merging this to main so it will be in a release near you very soon.
Give it a try, read the documentation, and give us feedback either on Zulip or via GitHub issues.