Skip to content

Commit

Permalink
Merge pull request #26510 from DavideD/panache-project
Browse files Browse the repository at this point in the history
Improve Panache projection with Hibernate ORM and Reactive
  • Loading branch information
gsmet authored Aug 10, 2022
2 parents dc89f85 + 4b45773 commit 8e4ee68
Show file tree
Hide file tree
Showing 11 changed files with 315 additions and 16 deletions.
50 changes: 45 additions & 5 deletions docs/src/main/asciidoc/hibernate-orm-panache.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ person.persist();
// 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
// check if it is persistent
if(person.isPersistent()){
// delete it
person.delete();
Expand Down Expand Up @@ -395,7 +395,7 @@ 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
// check if it is persistent
if(personRepository.isPersistent(person)){
// delete it
personRepository.delete(person);
Expand Down Expand Up @@ -772,6 +772,7 @@ If in the DTO projection object you have a field from a referenced entity, you c
public class Dog extends PanacheEntity {
public String name;
public String race;
public Double weight;
@ManyToOne
public Person owner;
}
Expand All @@ -791,6 +792,45 @@ PanacheQuery<DogDto> query = Dog.findAll().project(DogDto.class);
----
<1> The `ownerName` DTO constructor's parameter will be loaded from the `owner.name` HQL property.

It is also possible to specify a HQL query with a select clause. In this case, the projection class must have a constructor
matching the values returned by the select clause:

[source,java]
----
import io.quarkus.runtime.annotations.RegisterForReflection;
@RegisterForReflection
public class RaceWeight {
public final String race;
public final Double weight;
public RaceWeight(String race) {
this(race, null);
}
public RaceWeight(String race, Double weight) { // <1>
this.race = race;
this.weight = weight;
}
}
// Only the race and the average weight will be loaded
PanacheQuery<RaceWeight> query = Person.find("select d.race, AVG(d.weight) from Dog d group by d.race).project(RaceWeight.class);
----
<1> Hibernate ORM will use this constructor. When the query has a select clause, it is possible to have multiple constructors.

[WARNING]
====
It is not possible to have a HQL `select new` query and `.project(Class)` at the same time - you need to pick one approach.
For example, this will fail:
[source,java]
----
PanacheQuery<RaceWeight> query = Person.find("select new MyView(d.race, AVG(d.weight)) from Dog d group by d.race).project(AnotherView.class);
----
====

== Multiple Persistence Units

The support for multiple persistence units is described in detail in xref:hibernate-orm.adoc#multiple-persistence-units[the Hibernate ORM guide].
Expand All @@ -806,8 +846,8 @@ Make sure to wrap methods modifying your database (e.g. `entity.persist()`) with
CDI bean method `@Transactional` will do that for you and make that method a transaction boundary. We recommend doing
so at your application entry point boundaries like your REST endpoint controllers.

JPA batches changes you make to your entities and sends changes (it's called flush) at the end of the transaction or before a query.
This is usually a good thing as it's more efficient.
JPA batches changes you make to your entities and sends changes (it is called flush) at the end of the transaction or before a query.
This is usually a good thing as it is more efficient.
But if you want to check optimistic locking failures, do object validation right away or generally want to get immediate feedback, you can force the flush operation by calling `entity.flush()` or even use `entity.persistAndFlush()` to make it a single method call. This will allow you to catch any `PersistenceException` that could occur when JPA send those changes to the database.
Remember, this is less efficient so don't abuse it.
And your transaction still has to be committed.
Expand Down Expand Up @@ -1149,7 +1189,7 @@ public class PanacheFunctionalityTest {
When it comes to writing Hibernate ORM entities, there are a number of annoying things that users have grown used to
reluctantly deal with, such as:

- Duplicating ID logic: most entities need an ID, most people don't care how it's set, because it's not really
- Duplicating ID logic: most entities need an ID, most people don't care how it is set, because it is not really
relevant to your model.
- Traditional EE patterns advise to split entity definition (the model) from the operations you can do on them
(DAOs, Repositories), but really that requires a split between the state and its operations even though
Expand Down
49 changes: 44 additions & 5 deletions docs/src/main/asciidoc/hibernate-reactive-panache.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ Uni<Void> persistOperation = person.persist();
// 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
// check if it is persistent
if(person.isPersistent()){
// delete it
Uni<Void> deleteOperation = person.delete();
Expand Down Expand Up @@ -372,7 +372,7 @@ Uni<Void> persistOperation = 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
// check if it is persistent
if(personRepository.isPersistent(person)){
// delete it
Uni<Void> deleteOperation = personRepository.delete(person);
Expand Down Expand Up @@ -655,6 +655,7 @@ If in the DTO projection object you have a field from a referenced entity, you c
public class Dog extends PanacheEntity {
public String name;
public String race;
public Double weight;
@ManyToOne
public Person owner;
}
Expand All @@ -674,6 +675,44 @@ PanacheQuery<DogDto> query = Dog.findAll().project(DogDto.class);
----
<1> The `ownerName` DTO constructor's parameter will be loaded from the `owner.name` HQL property.

It is also possible to specify a HQL query with a select clause. In this case, the projection class must have a constructor
matching the values returned by the select clause:

[source,java]
----
import io.quarkus.runtime.annotations.RegisterForReflection;
@RegisterForReflection
public class RaceWeight {
public final String race;
public final Double weight
public RaceWeight(String race) {
this(race, null);
}
public RaceWeight(String race, Double weight) { // <1>
this.race = race;
this.weight = weight;
}
}
// Only the race and the average weight will be loaded
PanacheQuery<RaceWeight> query = Person.find("select d.race, AVG(d.weight) from Dog d group by d.race).project(RaceWeight.class);
----
<1> Hibernate Reactive will use this constructor. When the query has a select clause, it is possible to have multiple constructors.

[WARNING]
====
It is not possible to have a HQL `select new` query and `.project(Class)` at the same time - you need to pick one approach.
For example, this will fail:
[source,java]
----
PanacheQuery<RaceWeight> query = Person.find("select new MyView(d.race, AVG(d.weight)) from Dog d group by d.race).project(AnotherView.class);
----
====

== Multiple Persistence Units

Hibernate Reactive in Quarkus currently does not support multiple persistence units.
Expand All @@ -689,8 +728,8 @@ NOTE: You cannot use `@Transactional` with Hibernate Reactive for your transacti
and your annotated method must return a `Uni` to be non-blocking. Otherwise, it needs be called from a non-`VertxThread` thread
and will become blocking.

JPA batches changes you make to your entities and sends changes (it's called flush) at the end of the transaction or before a query.
This is usually a good thing as it's more efficient.
JPA batches changes you make to your entities and sends changes (it is called flush) at the end of the transaction or before a query.
This is usually a good thing as it is more efficient.
But if you want to check optimistic locking failures, do object validation right away or generally want to get immediate feedback, you can force the flush operation by calling `entity.flush()` or even use `entity.persistAndFlush()` to make it a single method call. This will allow you to catch any `PersistenceException` that could occur when JPA send those changes to the database.
Remember, this is less efficient so don't abuse it.
And your transaction still has to be committed.
Expand Down Expand Up @@ -1030,7 +1069,7 @@ public class PanacheFunctionalityTest {
When it comes to writing Hibernate Reactive entities, there are a number of annoying things that users have grown used to
reluctantly deal with, such as:

- Duplicating ID logic: most entities need an ID, most people don't care how it's set, because it's not really
- Duplicating ID logic: most entities need an ID, most people don't care how it is set, because it is not really
relevant to your model.
- Dumb getters and setters: since Java lacks support for properties in the language, we have to create fields,
then generate getters and setters for those fields, even if they don't actually do anything more than read/write
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,35 @@ public <T> CommonPanacheQueryImpl<T> project(Class<T> type) {
throw new PanacheQueryException("Unable to perform a projection on a named query");
}

String lowerCasedTrimmedQuery = query.trim().toLowerCase();
if (lowerCasedTrimmedQuery.startsWith("select new ")) {
throw new PanacheQueryException("Unable to perform a projection on a 'select new' query: " + query);
}

// If the query starts with a select clause, we generate an HQL query
// using the fields in the select clause:
// Initial query: select e.field1, e.field2 from EntityClass e
// New query: SELECT new org.acme.ProjectionClass(e.field1, e.field2) from EntityClass e
if (lowerCasedTrimmedQuery.startsWith("select ")) {
int endSelect = lowerCasedTrimmedQuery.indexOf(" from ");
String trimmedQuery = query.trim();
// 7 is the length of "select "
String selectClause = trimmedQuery.substring(7, endSelect);
String from = trimmedQuery.substring(endSelect);
StringBuilder newQuery = new StringBuilder("select ");
// Handle select-distinct. HQL example: select distinct new org.acme.ProjectionClass...
String lowerCasedTrimmedSelect = selectClause.trim().toLowerCase();
boolean distinctQuery = lowerCasedTrimmedSelect.startsWith("distinct ");
if (distinctQuery) {
// 9 is the length of "distinct "
selectClause = lowerCasedTrimmedSelect.substring(9).trim();
newQuery.append("distinct ");
}

newQuery.append("new ").append(type.getName()).append("(").append(selectClause).append(")").append(from);
return new CommonPanacheQueryImpl<>(this, newQuery.toString(), "select count(*) " + from);
}

// We use the first constructor that we found and use the parameter names,
// so the projection class must have only one constructor,
// and the application must be built with parameter names.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,35 @@ public <T> CommonPanacheQueryImpl<T> project(Class<T> type) {
throw new PanacheQueryException("Unable to perform a projection on a named query");
}

String lowerCasedTrimmedQuery = query.trim().toLowerCase();
if (lowerCasedTrimmedQuery.startsWith("select new ")) {
throw new PanacheQueryException("Unable to perform a projection on a 'select new' query: " + query);
}

// If the query starts with a select clause, we generate an HQL query
// using the fields in the select clause:
// Initial query: select e.field1, e.field2 from EntityClass e
// New query: SELECT new org.acme.ProjectionClass(e.field1, e.field2) from EntityClass e
if (lowerCasedTrimmedQuery.startsWith("select ")) {
int endSelect = lowerCasedTrimmedQuery.indexOf(" from ");
String trimmedQuery = query.trim();
// 7 is the length of "select "
String selectClause = trimmedQuery.substring(7, endSelect);
String from = trimmedQuery.substring(endSelect);
StringBuilder newQuery = new StringBuilder("select ");
// Handle select-distinct. HQL example: select distinct new org.acme.ProjectionClass...
String lowerCasedTrimmedSelect = selectClause.trim().toLowerCase();
boolean distinctQuery = lowerCasedTrimmedSelect.startsWith("distinct ");
if (distinctQuery) {
// 9 is the length of "distinct "
selectClause = lowerCasedTrimmedSelect.substring(9).trim();
newQuery.append("distinct ");
}

newQuery.append("new ").append(type.getName()).append("(").append(selectClause).append(")").append(from);
return new CommonPanacheQueryImpl<>(this, newQuery.toString(), "select count(*) " + from);
}

// We use the first constructor that we found and use the parameter names,
// so the projection class must have only one constructor,
// and the application must be built with parameter names.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public class Cat extends PanacheEntity {
@ManyToOne
CatOwner owner;

Double weight;

public Cat(String name, CatOwner owner) {
this.name = name;
this.owner = owner;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.quarkus.it.panache;

import io.quarkus.runtime.annotations.RegisterForReflection;

@RegisterForReflection
public class CatProjectionBean {

private final String name;

private final String ownerName;

private final Double weight;

public CatProjectionBean(String name, String ownerName) {
this(name, ownerName, null);
}

public CatProjectionBean(String name, String ownerName, Double weight) {
this.name = name;
this.ownerName = ownerName;
this.weight = weight;
}

public String getName() {
return name;
}

public String getOwnerName() {
return ownerName;
}

public Double getWeight() {
return weight;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1205,11 +1205,53 @@ public String testProjection() {
CatOwner catOwner = new CatOwner("Julie");
catOwner.persist();
Cat bubulle = new Cat("Bubulle", catOwner);
bubulle.weight = 8.5d;
bubulle.persist();

CatDto catDto = Cat.findAll().project(CatDto.class).firstResult();
Assertions.assertEquals("Julie", catDto.ownerName);

CatProjectionBean fieldsProjection = Cat.find("select c.name, c.owner.name as ownerName from Cat c")
.project(CatProjectionBean.class).firstResult();
Assertions.assertEquals("Julie", fieldsProjection.getOwnerName());

PanacheQueryException exception = Assertions.assertThrows(PanacheQueryException.class,
() -> Cat.find("select new FakeClass('fake_cat', 'fake_owner', 12.5 from Cat c)")
.project(CatProjectionBean.class).firstResult());
Assertions.assertTrue(exception.getMessage().startsWith("Unable to perform a projection on a 'select new' query"));

CatProjectionBean constantProjection = Cat.find("select 'fake_cat', 'fake_owner', 12.5 from Cat c")
.project(CatProjectionBean.class).firstResult();
Assertions.assertEquals("fake_cat", constantProjection.getName());
Assertions.assertEquals("fake_owner", constantProjection.getOwnerName());
Assertions.assertEquals(12.5d, constantProjection.getWeight());

PanacheQuery<CatProjectionBean> projectionQuery = Cat
// The spaces at the beginning are intentional
.find(" SELECT c.name, cast(null as string), SUM(c.weight) from Cat c where name = :name group by name ",
Parameters.with("name", bubulle.name))
.project(CatProjectionBean.class);
CatProjectionBean aggregationProjection = projectionQuery.firstResult();
Assertions.assertEquals(bubulle.name, aggregationProjection.getName());
Assertions.assertNull(aggregationProjection.getOwnerName());
Assertions.assertEquals(bubulle.weight, aggregationProjection.getWeight());

long count = projectionQuery.count();
Assertions.assertEquals(1L, count);

PanacheQuery<CatProjectionBean> projectionDistinctQuery = Cat
// The spaces at the beginning are intentional
.find(" SELECT disTINct c.name, cast(null as string), SUM(c.weight) from Cat c where name = :name group by name ",
Parameters.with("name", bubulle.name))
.project(CatProjectionBean.class);
CatProjectionBean aggregationDistinctProjection = projectionDistinctQuery.singleResult();
Assertions.assertEquals(bubulle.name, aggregationDistinctProjection.getName());
Assertions.assertNull(aggregationDistinctProjection.getOwnerName());
Assertions.assertEquals(bubulle.weight, aggregationDistinctProjection.getWeight());

long countDistinct = projectionDistinctQuery.count();
Assertions.assertEquals(1L, countDistinct);

Cat.deleteAll();
CatOwner.deleteAll();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,20 @@ public class Cat extends PanacheEntity {
@ManyToOne
CatOwner owner;

Double weight;

public Cat(CatOwner owner) {
this.owner = owner;
this(null, owner, null);
}

public Cat(String name, CatOwner owner) {
this(name, owner, null);
}

public Cat(String name, CatOwner owner, Double weight) {
this.name = name;
this.owner = owner;
this.weight = weight;
}

public Cat() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.quarkus.it.panache.reactive;

import io.quarkus.runtime.annotations.RegisterForReflection;

@RegisterForReflection
public class CatProjectionBean {

public final String name;

public final String ownerName;

public final Double weight;

public CatProjectionBean(String name, String ownerName) {
this(name, ownerName, null);
}

public CatProjectionBean(String name, String ownerName, Double weight) {
this.name = name;
this.ownerName = ownerName;
this.weight = weight;
}
}
Loading

0 comments on commit 8e4ee68

Please sign in to comment.