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