quarkusio / quarkus

Quarkus: Supersonic Subatomic Java.
https://quarkus.io
Apache License 2.0
13.55k stars 2.62k forks source link

Support new Hibernate 6.3 syntax for type-safe queries #36168

Open FroMage opened 11 months ago

FroMage commented 11 months ago

Description

Hibernate 6.3 comes with experimental support for mapping HQL/SQL queries to methods, as well as finder methods, whose HQL can be inferred from the method signature.

Hibernate 6.3 Examples

interface BookRepository {
 @HQL("from Book where author = :author and language = :language")
 public List<Book> findBooks1(String author, String language);

 // the same, as inferred from the parameters and return type
 @Find
 public List<Book> findBooks2(String author, String language);
}

This will use Java compiler plugins (aka APT) to generate a BookRepository_ class with the implementation of the methods, as well as type-check the query and verify that all parameters exist and are of the right type at compile-time.

Here is an example of the generate code (when placed in the entity, so mixed with the entity metamodel):

@StaticMetamodel(Book.class)
@Generated("org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor")
public abstract class Book_ extends io.quarkus.hibernate.orm.panache.PanacheEntity_ {

    public static final String AUTHOR = "author";
    public static final String LANGUAGE = "language";
    static final String FIND_BOOKS1_String_String = "from Book where author = :author and language = :language";

    public static volatile SingularAttribute<Book, String> author;
    public static volatile SingularAttribute<Book, String> language;
    public static volatile EntityType<Book> class_;

    public static List<Book> findBooks2(@Nonnull EntityManager entityManager, String author, String language) {
        var builder = entityManager.getEntityManagerFactory().getCriteriaBuilder();
        var query = builder.createQuery(Book.class);
        var entity = query.from(Book.class);
        query.where(
                author==null
                    ? entity.get(Book_.author).isNull()
                    : builder.equal(entity.get(Book_.author), author), 
                language==null
                    ? entity.get(Book_.language).isNull()
                    : builder.equal(entity.get(Book_.language), language)
        );
        return entityManager.createQuery(query).getResultList();
    }

    public static List<Book> findBooks1(@Nonnull EntityManager entityManager, String author, String language) {
        return entityManager.createQuery(FIND_BOOKS1_String_String, Book.class)
                .setParameter("author", author)
                .setParameter("language", language)
                .getResultList();
    }
}

As you can see, this needs an EntityManager, which is not convenient, but also defines type-safe references to the attributes as String or SingularAttribute objects.

Current Hibernate ORM with Panache examples

The previous queries can currently be written as in Hibernate ORM with Panache as:

@Entity
public class Book extends PanacheEntity {
 // …

 public static List<Book> findBooks(String author, String language){
  return list("author = ?1 and language = ?2", author, language);
 }
}

This isn't necessarily more verbose, but it is not type-safe, and it's really hard to check the HQL at build-time (though we've had some success in the past).

Proposal

We would like to add support for the new type-safe queries to Panache entities and repositories, in addition to the current API, as follows:

public class BookRepository extends PanacheRepository<Book> {
 @HQL("from Book where author = :author and language = :language")
 public native List<Book> findBooks1(String author, String language);

 // the same, as inferred from the parameters and return type
 @Find
 public native List<Book> findBooks2(String author, String language);
}

@Entity
public class Book extends PanacheEntity {
 // …

 @HQL("from Book where author = :author and language = :language")
 public static native List<Book> findBooks1(String author, String language);

 // the same, as inferred from the parameters and return type
 @Find
 public static native List<Book> findBooks2(String author, String language);
}

As usual, we have to use the native trick to allow non-abstract classes to have generated method bodies. These methods will be static native for entities, and native for repositories.

These generated methods will be type-safe, and their implementation will forward to the generates Book_ and BookRepository_ classes.

Challenges

There are quite a number of challenges and questions to solve in order to support this, including:

Implementation ideas

No response

quarkus-bot[bot] commented 11 months ago

/cc @Sanne (hibernate-orm), @gsmet (hibernate-orm), @yrodiere (hibernate-orm)

gavinking commented 5 months ago

@gavinking mentionned adding support for other types of methods, such as for insert/delete/persist, in line with the new Jakarta Data proposal

The need for this is now much more acute, since Jakarta Data is almost here. What used to be called the "JPA Static Metamodel Generator" (#29068) is now so much more.

I have tested Hibernate Data Repositories with Quarkus, and it works perfectly (modulo a bug in how Quarkus handles @Transactional).

But, in IntelliJ, the processor does not get run on code changes when in dev mode, and so I have to explicitly request a build by hitting ⌘\ every time. That's not so awful, but users aren't going to know to do it, and it's something they're not used to needing in Quarkus.

gavinking commented 5 months ago

In fairness, I just tried a Maven project and it does work there, apparenlty perfectly, I guess because of the work @FroMage already did.

gavinking commented 5 months ago

In fairness, I just tried a Maven project and it does work there, apparenlty perfectly, I guess because of the work @FroMage already did.

Scratch that, actually in Maven it's way worse, because if I try to add a @Query method, it works at first and then I get into a crazy state where mvn just fails with:

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.12.1:compile (default-compile) on project data-demo-quarkus-mvn: 
Fatal error compiling: java.lang.NoClassDefFoundError: org/jboss/jandex/IndexView: org.jboss.jandex.IndexView -> [Help 1]

and I have to delete the method to recover.

This doesn't happen with Gradle.

gavinking commented 5 months ago

Scratch that, actually in Maven it's way worse

This turned out to be a problem on my side. The HibernateProcessor was missing some dependencies that apparently aren't (explicitly) needed for Gradle, but are for Maven. After adding them, @Query works fine in Quarkus.

https://github.com/hibernate/hibernate-orm/pull/8033

FroMage commented 5 months ago

In fairness, I just tried a Maven project and it does work there, apparenlty perfectly, I guess because of the work @FroMage already did.

Right. And for Gradle it's not done yet: https://github.com/quarkusio/quarkus/issues/38228

codespearhead commented 1 week ago

I wonder how the fact that Hibernate 6.6.0.Final includes a complete implementation of the Jakarta Data 1.0 Release impacts this issue. Hibernate 6.6.0.Final and is available since Quarkus 3.14.0:

Hibernate 6.6 includes a complete implementation of the Jakarta Data 1.0 Release. As discussed here, our implementation:

  • is based on compile-time code generation via an annotation processor, enabling unprecedented compile-time type safety, and
  • is backed by Hibernate’s StatelessSession, which has been enhanced especially to meet the needs of Jakarta Data.

Hibernate 6.6 is certified as a compatible implementation.

yrodiere commented 1 week ago

Jakarta Data is different from Hibernate ORM's own syntax. Jakarta Data focuses on stateless sessions, while Hibernate ORM's type-safe queries work with an EntityManager (and perhaps stateless sessions, I don't remember).

@FroMage is working on an integration in Panache, that would hopefull unify all this. But AFAIK it's not ready yet.

In the meantime you should already be able to use Jakarta Data in Quarkus, as it's a purely build-time (annotation processor) thing.