diff --git a/bom/application/pom.xml b/bom/application/pom.xml index a0df2449b6e95..457e12972de2b 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -71,7 +71,8 @@ 2.1.3 3.0.1 2.1.3 - 3.1.0 + + 1.0.1 2.1.0 6.0.0 2.0.1 @@ -4390,6 +4391,11 @@ jakarta.persistence-api ${jakarta.persistence-api.version} + + jakarta.data + jakarta.data-api + ${jakarta.data-api.version} + jakarta.resource jakarta.resource-api diff --git a/docs/pom.xml b/docs/pom.xml index bc1cddaa15c54..ad1ce9884bf16 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -3230,6 +3230,17 @@ ${hibernate-validator.version} + + parse-version-jakarta-persistence + + parse-version + + validate + + jakarta-persistence + ${jakarta.persistence-api.version} + + diff --git a/docs/src/main/asciidoc/_attributes.adoc b/docs/src/main/asciidoc/_attributes.adoc index b91b5fe9eab66..5e2f6023a7a7a 100644 --- a/docs/src/main/asciidoc/_attributes.adoc +++ b/docs/src/main/asciidoc/_attributes.adoc @@ -30,6 +30,7 @@ :hibernate-orm-version-major-minor: ${hibernate-orm.majorVersion}.${hibernate-orm.minorVersion} :hibernate-search-version-major-minor: ${hibernate-search.majorVersion}.${hibernate-search.minorVersion} :hibernate-validator-version-major-minor: ${hibernate-validator.majorVersion}.${hibernate-validator.minorVersion} +:jakarta-persistence-version-major-minor: ${jakarta-persistence.majorVersion}.${jakarta-persistence.minorVersion} // . :quarkus-home-url: ${quarkus-home-url} :quarkus-org-url: https://github.com/quarkusio @@ -56,6 +57,7 @@ :hibernate-orm-dialect-docs-url: https://docs.jboss.org/hibernate/orm/{hibernate-orm-version-major-minor}/dialect/dialect.html :hibernate-search-docs-url: https://docs.jboss.org/hibernate/search/{hibernate-search-version-major-minor}/reference/en-US/html_single/ :hibernate-validator-docs-url: https://docs.jboss.org/hibernate/validator/{hibernate-validator-version-major-minor}/reference/en-US/html_single/ +:jakarta-persistence-spec-url: https://jakarta.ee/specifications/persistence/{jakarta-persistence-version-major-minor}/jakarta-persistence-spec-{jakarta-persistence-version-major-minor}#a6072 // . :amazon-services-guide: https://docs.quarkiverse.io/quarkus-amazon-services/dev/index.html :config-consul-guide: https://docs.quarkiverse.io/quarkus-config-extensions/dev/consul.html diff --git a/docs/src/main/asciidoc/hibernate-orm.adoc b/docs/src/main/asciidoc/hibernate-orm.adoc index 9f951ccbebc8f..a750316197db4 100644 --- a/docs/src/main/asciidoc/hibernate-orm.adoc +++ b/docs/src/main/asciidoc/hibernate-orm.adoc @@ -1646,3 +1646,134 @@ try { Since Quarkus has built-in exception mappers for `jakarta.validation.ConstraintViolationException`, explicitly handling these exceptions might be redundant. See the xref:validation.adoc#rest-end-point-validation[REST end point validation] section of the Hibernate Validator guide for more details. + +[[jakarta-data]] +== Static metamodel and Jakarta Data + +Both static metamodel and Jakarta Data capabilities of Hibernate ORM are available in Quarkus +through the `hibernate-jpamodelgen` annotation processor. Since it is an annotation processor, +you must configure the annotation processor in your build tool: + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + maven-compiler-plugin + + + + org.hibernate.orm + hibernate-jpamodelgen + + + + + + + +---- + +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +// Enforce the version management of your annotation processor dependencies, +// so that there's no need to define an explicit version of the hibernate-jpamodelgen +annotationProcessor enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}") +annotationProcessor 'org.hibernate.orm:hibernate-jpamodelgen' +---- + +=== Static metamodel + +The generated static metamodel allows for building queries in a type-safe manner. +Let's consider having a simple entity: + +[source,java] +---- +@Entity +public class MyEntity { + @Id + @GeneratedValue + public Integer id; + + @Column(unique = true) + public String name; +} +---- + +A query created with the help of static metamodel may look as: + +[source,java] +---- +var builder = session.getCriteriaBuilder(); +var criteria = builder.createQuery(MyEntity.class); +var e = criteria.from(MyEntity_.class); +criteria.where(e.get(MyEntity_.name).equalTo(name)); +var query = session.createQuery(criteria); +var result = query.list(); +---- + +For a more detailed overview of static metamodel, please refer to the link:{jakarta-persistence-spec-url}#a6072[Jakarta Persistence specification]. + +=== Jakarta Data + +Jakarta Data requires, besides having the `hibernate-jpamodelgen` annotation processor in place, one extra dependency to be added: + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + jakarta.data + jakarta.data-api + +---- + +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +implementation 'jakarta.data:jakarta.data-api' +---- + +With this dependency, and the annotation processor in place you could simply create your repositories as follows: + +[source,java] +---- +@Repository +public interface MyRepository extends CrudRepository { // <1> + + @Query("select e from MyEntity e where e.name like :name") // <2> + List findByName(String name); + + @Delete // <3> + void delete(String name); + +} +---- +1. To skip the boilerplate definition of CRUD operations, +we can use one of the available interfaces (e.g. `CrudRepository` or `BasicRepository`). +2. Adding custom queries with parameters is as easy as providing your query string to the `@Query` annotation. +3. If the basic CRUD operations from the Jakarta Data interfaces are not enough, +we can always add a custom one, in this case a delete operation that removes `MyEntity`s by name. + +And then the repository can be used as any other bean: + +[source,java] +---- +public class MyEntityResource { + + @Inject + MyRepository repository; + + @POST + @Transactional + public void create(MyEntity entity) { + repository.insert(entity); + } + + // ... + +} +---- + +Please refer to the corresponding https://hibernate.org/repositories/[Hibernate Data Repositories] +and https://jakarta.ee/specifications/data/1.0/jakarta-data-1.0[Jakarta Data] +guides to learn what else they have to offer. diff --git a/integration-tests/hibernate-orm-data/pom.xml b/integration-tests/hibernate-orm-data/pom.xml new file mode 100644 index 0000000000000..66d2437bc6c98 --- /dev/null +++ b/integration-tests/hibernate-orm-data/pom.xml @@ -0,0 +1,148 @@ + + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-integration-test-hibernate-orm-data + Quarkus - Integration Tests - Hibernate ORM with Jakarta Data + + + + io.quarkus + quarkus-hibernate-orm + + + jakarta.data + jakarta.data-api + + + io.quarkus + quarkus-rest + + + io.quarkus + quarkus-rest-jackson + + + io.quarkus + quarkus-jdbc-h2 + + + + + io.quarkus + quarkus-junit5 + test + + + io.quarkus + quarkus-junit5-internal + test + + + io.rest-assured + rest-assured + test + + + org.assertj + assertj-core + test + + + + + io.quarkus + quarkus-hibernate-orm-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-jdbc-h2-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-rest-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-rest-jackson-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + src/main/resources + true + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + maven-compiler-plugin + + + + org.hibernate.orm + hibernate-jpamodelgen + + + + + + + + + diff --git a/integration-tests/hibernate-orm-data/src/main/java/io/quarkus/it/hibernate/jpamodelgen/data/MyEntity.java b/integration-tests/hibernate-orm-data/src/main/java/io/quarkus/it/hibernate/jpamodelgen/data/MyEntity.java new file mode 100644 index 0000000000000..0c12769a65e80 --- /dev/null +++ b/integration-tests/hibernate-orm-data/src/main/java/io/quarkus/it/hibernate/jpamodelgen/data/MyEntity.java @@ -0,0 +1,30 @@ +package io.quarkus.it.hibernate.jpamodelgen.data; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +@Entity +public class MyEntity { + + @Id + @GeneratedValue + public Integer id; + + @Column(unique = true) + public String name; + + MyEntity() { + } + + public MyEntity(String name) { + this.name = name; + } + + @Override + public String toString() { + return "MyOrmEntity [id=" + id + ", name=" + name + "]"; + } + +} diff --git a/integration-tests/hibernate-orm-data/src/main/java/io/quarkus/it/hibernate/jpamodelgen/data/MyEntityResource.java b/integration-tests/hibernate-orm-data/src/main/java/io/quarkus/it/hibernate/jpamodelgen/data/MyEntityResource.java new file mode 100644 index 0000000000000..5572299945ca6 --- /dev/null +++ b/integration-tests/hibernate-orm-data/src/main/java/io/quarkus/it/hibernate/jpamodelgen/data/MyEntityResource.java @@ -0,0 +1,59 @@ +package io.quarkus.it.hibernate.jpamodelgen.data; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; + +import org.jboss.resteasy.reactive.RestPath; + +@ApplicationScoped +@Produces("application/json") +@Consumes("application/json") +@Path("/data/") +public class MyEntityResource { + + @Inject + MyRepository repository; + + @POST + @Transactional + public void create(MyEntity entity) { + repository.insert(entity); + } + + @GET + @Transactional + @Path("/by/name/{name}") + public MyEntity getByName(@RestPath String name) { + List entities = repository.findByName(name); + if (entities.isEmpty()) { + throw new NotFoundException(); + } + return entities.get(0); + } + + @POST + @Transactional + @Path("/rename/{before}/to/{after}") + public void rename(@RestPath String before, @RestPath String after) { + MyEntity byName = getByName(before); + byName.name = after; + repository.update(byName); + } + + @DELETE + @Transactional + @Path("/by/name/{name}") + public void deleteByName(@RestPath String name) { + repository.delete(name); + } +} diff --git a/integration-tests/hibernate-orm-data/src/main/java/io/quarkus/it/hibernate/jpamodelgen/data/MyRepository.java b/integration-tests/hibernate-orm-data/src/main/java/io/quarkus/it/hibernate/jpamodelgen/data/MyRepository.java new file mode 100644 index 0000000000000..d2c2073ec3be6 --- /dev/null +++ b/integration-tests/hibernate-orm-data/src/main/java/io/quarkus/it/hibernate/jpamodelgen/data/MyRepository.java @@ -0,0 +1,19 @@ +package io.quarkus.it.hibernate.jpamodelgen.data; + +import java.util.List; + +import jakarta.data.repository.CrudRepository; +import jakarta.data.repository.Delete; +import jakarta.data.repository.Query; +import jakarta.data.repository.Repository; + +@Repository +public interface MyRepository extends CrudRepository { + + @Query("select e from MyEntity e where e.name like :name") + List findByName(String name); + + @Delete + void delete(String name); + +} diff --git a/integration-tests/hibernate-orm-data/src/main/resources/application.properties b/integration-tests/hibernate-orm-data/src/main/resources/application.properties new file mode 100644 index 0000000000000..0c8f975c6ca56 --- /dev/null +++ b/integration-tests/hibernate-orm-data/src/main/resources/application.properties @@ -0,0 +1,2 @@ +quarkus.datasource.jdbc.max-size=8 +quarkus.hibernate-orm.database.generation=drop-and-create diff --git a/integration-tests/hibernate-orm-data/src/test/java/io/quarkus/it/hibernate/jpamodelgen/data/HibernateOrmDataInGraalIT.java b/integration-tests/hibernate-orm-data/src/test/java/io/quarkus/it/hibernate/jpamodelgen/data/HibernateOrmDataInGraalIT.java new file mode 100644 index 0000000000000..64175aee5e43a --- /dev/null +++ b/integration-tests/hibernate-orm-data/src/test/java/io/quarkus/it/hibernate/jpamodelgen/data/HibernateOrmDataInGraalIT.java @@ -0,0 +1,8 @@ +package io.quarkus.it.hibernate.jpamodelgen.data; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class HibernateOrmDataInGraalIT extends HibernateOrmDataTest { + +} diff --git a/integration-tests/hibernate-orm-data/src/test/java/io/quarkus/it/hibernate/jpamodelgen/data/HibernateOrmDataTest.java b/integration-tests/hibernate-orm-data/src/test/java/io/quarkus/it/hibernate/jpamodelgen/data/HibernateOrmDataTest.java new file mode 100644 index 0000000000000..928f783b4692e --- /dev/null +++ b/integration-tests/hibernate-orm-data/src/test/java/io/quarkus/it/hibernate/jpamodelgen/data/HibernateOrmDataTest.java @@ -0,0 +1,71 @@ +package io.quarkus.it.hibernate.jpamodelgen.data; + +import static io.restassured.RestAssured.given; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; + +@QuarkusTest +public class HibernateOrmDataTest { + private static final String ROOT = "/data"; + + @Test + public void staticMetamodel() { + // Create/retrieve + given() + .pathParam("name", "foo") + .contentType(ContentType.JSON) + .when().get(ROOT + "/by/name/{name}") + .then() + .statusCode(404); + given() + .body(new MyEntity("foo")) + .contentType(ContentType.JSON) + .when().post(ROOT) + .then() + .statusCode(204); + given() + .pathParam("name", "foo") + .contentType(ContentType.JSON) + .when().get(ROOT + "/by/name/{name}") + .then() + .statusCode(200); + + // Update + given() + .pathParam("name", "bar") + .contentType(ContentType.JSON) + .when().get(ROOT + "/by/name/{name}") + .then() + .statusCode(404); + given() + .pathParam("before", "foo") + .pathParam("after", "bar") + .contentType(ContentType.JSON) + .when().post(ROOT + "/rename/{before}/to/{after}") + .then() + .statusCode(204); + given() + .pathParam("name", "bar") + .contentType(ContentType.JSON) + .when().get(ROOT + "/by/name/{name}") + .then() + .statusCode(200); + + // Delete + given() + .pathParam("name", "bar") + .contentType(ContentType.JSON) + .when().delete(ROOT + "/by/name/{name}") + .then() + .statusCode(204); + given() + .pathParam("name", "bar") + .contentType(ContentType.JSON) + .when().get(ROOT + "/by/name/{name}") + .then() + .statusCode(404); + } +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index ee572ee589bcc..c56a7a3b3ac1f 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -230,6 +230,7 @@ hibernate-search-standalone-opensearch hibernate-orm-tenancy hibernate-orm-jpamodelgen + hibernate-orm-data hibernate-orm-envers vertx-http vertx-http-compressors diff --git a/pom.xml b/pom.xml index b69ce5df3ac26..f350a81e16186 100644 --- a/pom.xml +++ b/pom.xml @@ -72,6 +72,7 @@ 7.1.0 5.5.1 6.6.13.Final + 3.1.0 4.13.0 1.15.11 7.0.3.Final