diff --git a/docs/src/main/asciidoc/hibernate-orm-panache.adoc b/docs/src/main/asciidoc/hibernate-orm-panache.adoc index 50520be3c429c..8859677edd6dc 100644 --- a/docs/src/main/asciidoc/hibernate-orm-panache.adoc +++ b/docs/src/main/asciidoc/hibernate-orm-panache.adoc @@ -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(); @@ -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); @@ -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; } @@ -791,6 +792,45 @@ PanacheQuery 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 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 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]. @@ -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. @@ -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 diff --git a/docs/src/main/asciidoc/hibernate-reactive-panache.adoc b/docs/src/main/asciidoc/hibernate-reactive-panache.adoc index 7ba7845fcb963..d9baf896d6cee 100644 --- a/docs/src/main/asciidoc/hibernate-reactive-panache.adoc +++ b/docs/src/main/asciidoc/hibernate-reactive-panache.adoc @@ -206,7 +206,7 @@ Uni 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 deleteOperation = person.delete(); @@ -372,7 +372,7 @@ Uni 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 deleteOperation = personRepository.delete(person); @@ -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; } @@ -674,6 +675,44 @@ PanacheQuery 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 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 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. @@ -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. @@ -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 diff --git a/extensions/panache/hibernate-orm-panache-common/runtime/src/main/java/io/quarkus/hibernate/orm/panache/common/runtime/CommonPanacheQueryImpl.java b/extensions/panache/hibernate-orm-panache-common/runtime/src/main/java/io/quarkus/hibernate/orm/panache/common/runtime/CommonPanacheQueryImpl.java index a68413ec23a02..894357ec8a09e 100644 --- a/extensions/panache/hibernate-orm-panache-common/runtime/src/main/java/io/quarkus/hibernate/orm/panache/common/runtime/CommonPanacheQueryImpl.java +++ b/extensions/panache/hibernate-orm-panache-common/runtime/src/main/java/io/quarkus/hibernate/orm/panache/common/runtime/CommonPanacheQueryImpl.java @@ -82,6 +82,35 @@ public CommonPanacheQueryImpl project(Class 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. diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/CommonPanacheQueryImpl.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/CommonPanacheQueryImpl.java index 2b92fdb1b38d9..3623f2ee25cdf 100644 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/CommonPanacheQueryImpl.java +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/CommonPanacheQueryImpl.java @@ -69,6 +69,35 @@ public CommonPanacheQueryImpl project(Class 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. diff --git a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/Cat.java b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/Cat.java index 67eb6d1599da7..312e0fbaccdcc 100644 --- a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/Cat.java +++ b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/Cat.java @@ -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; diff --git a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/CatProjectionBean.java b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/CatProjectionBean.java new file mode 100644 index 0000000000000..e02da61409fbe --- /dev/null +++ b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/CatProjectionBean.java @@ -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; + } +} diff --git a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java index 90ff34b95be9f..bb85ca68e3681 100644 --- a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java +++ b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java @@ -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 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 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(); diff --git a/integration-tests/hibernate-reactive-panache/src/main/java/io/quarkus/it/panache/reactive/Cat.java b/integration-tests/hibernate-reactive-panache/src/main/java/io/quarkus/it/panache/reactive/Cat.java index 8e7b3d16b9524..131c750bcd7ba 100644 --- a/integration-tests/hibernate-reactive-panache/src/main/java/io/quarkus/it/panache/reactive/Cat.java +++ b/integration-tests/hibernate-reactive-panache/src/main/java/io/quarkus/it/panache/reactive/Cat.java @@ -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() { diff --git a/integration-tests/hibernate-reactive-panache/src/main/java/io/quarkus/it/panache/reactive/CatProjectionBean.java b/integration-tests/hibernate-reactive-panache/src/main/java/io/quarkus/it/panache/reactive/CatProjectionBean.java new file mode 100644 index 0000000000000..797dc61a80cbd --- /dev/null +++ b/integration-tests/hibernate-reactive-panache/src/main/java/io/quarkus/it/panache/reactive/CatProjectionBean.java @@ -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; + } +} diff --git a/integration-tests/hibernate-reactive-panache/src/main/java/io/quarkus/it/panache/reactive/TestEndpoint.java b/integration-tests/hibernate-reactive-panache/src/main/java/io/quarkus/it/panache/reactive/TestEndpoint.java index 6a8931ea36564..fb50b9a4287b4 100644 --- a/integration-tests/hibernate-reactive-panache/src/main/java/io/quarkus/it/panache/reactive/TestEndpoint.java +++ b/integration-tests/hibernate-reactive-panache/src/main/java/io/quarkus/it/panache/reactive/TestEndpoint.java @@ -1612,17 +1612,70 @@ public Uni testProjection() { public Uni testProjection2() { String ownerName = "Julie"; String catName = "Bubulle"; + Double catWeight = 8.5d; CatOwner catOwner = new CatOwner(ownerName); return catOwner.persist() - .chain(() -> new Cat(catName, catOwner).persist()) + .chain(() -> new Cat(catName, catOwner, catWeight).persist()) .chain(() -> Cat.find("name", catName) .project(CatDto.class) . firstResult()) - .map(cat -> { + .invoke(cat -> { Assertions.assertEquals(catName, cat.name); Assertions.assertEquals(ownerName, cat.ownerName); - return "OK"; - }); + }) + .chain(() -> Cat.find("select c.name, c.owner.name as ownerName from Cat c where c.name = :name", + Parameters.with("name", catName)) + .project(CatProjectionBean.class) + . singleResult()) + .invoke(catView -> { + Assertions.assertEquals(catName, catView.name); + Assertions.assertEquals(ownerName, catView.ownerName); + Assertions.assertNull(catView.weight); + }) + .chain(() -> Cat.find("select 'fake_cat', 'fake_owner', 12.5 from Cat c") + .project(CatProjectionBean.class) + . firstResult()) + .invoke(catView -> { + Assertions.assertEquals("fake_cat", catView.name); + Assertions.assertEquals("fake_owner", catView.ownerName); + Assertions.assertEquals(12.5d, catView.weight); + }) + // The spaces at the beginning are intentional + .replaceWith(() -> Cat.find( + " SELECT c.name, cast(null as string), SUM(c.weight) from Cat c where name = :name group by name ", + Parameters.with("name", catName)) + .project(CatProjectionBean.class)) + .invoke(projectionQuery -> projectionQuery + . firstResult() + .invoke(catView -> { + Assertions.assertEquals(catName, catView.name); + Assertions.assertNull(catView.ownerName); + Assertions.assertEquals(catWeight, catView.weight); + }) + .replaceWith(() -> projectionQuery.count() + .invoke(count -> Assertions.assertEquals(1L, count)))) + // The spaces at the beginning are intentional + .replaceWith(() -> Cat.find( + " SELECT disTINct c.name, cast(null as string), SUM(c.weight) from Cat c where name = :name group by name ", + Parameters.with("name", catName)) + .project(CatProjectionBean.class)) + .invoke(projectionQuery -> projectionQuery + . firstResult() + .invoke(catView -> { + Assertions.assertEquals(catName, catView.name); + Assertions.assertNull(catView.ownerName); + Assertions.assertEquals(catWeight, catView.weight); + }) + .replaceWith(() -> projectionQuery.count() + .invoke(count -> Assertions.assertEquals(1L, count)))) + .invoke(() -> { + PanacheQueryException exception = Assertions.assertThrows(PanacheQueryException.class, + () -> Cat.find("select new FakeClass('fake_cat', 'fake_owner', 12.5) from Cat c") + .project(CatProjectionBean.class)); + Assertions.assertTrue( + exception.getMessage().startsWith("Unable to perform a projection on a 'select new' query")); + }) + .replaceWith("OK"); } @ReactiveTransactional diff --git a/integration-tests/hibernate-reactive-postgresql/src/main/java/io/quarkus/it/hibernate/reactive/postgresql/HibernateReactiveTestEndpoint.java b/integration-tests/hibernate-reactive-postgresql/src/main/java/io/quarkus/it/hibernate/reactive/postgresql/HibernateReactiveTestEndpoint.java index 64f5e3ab99847..6e9d059efaa5b 100644 --- a/integration-tests/hibernate-reactive-postgresql/src/main/java/io/quarkus/it/hibernate/reactive/postgresql/HibernateReactiveTestEndpoint.java +++ b/integration-tests/hibernate-reactive-postgresql/src/main/java/io/quarkus/it/hibernate/reactive/postgresql/HibernateReactiveTestEndpoint.java @@ -20,7 +20,7 @@ public class HibernateReactiveTestEndpoint { @Inject Mutiny.Session mutinySession; - // Injecting a Vert.x Pool is not required, it us only used to + // Injecting a Vert.x Pool is not required, it's only used to // independently validate the contents of the database for the test @Inject PgPool pgPool;