Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Panache 2 #46096

Open
FroMage opened this issue Feb 5, 2025 · 4 comments
Open

Panache 2 #46096

FroMage opened this issue Feb 5, 2025 · 4 comments
Assignees
Labels
area/panache kind/enhancement New feature or request

Comments

@FroMage
Copy link
Member

FroMage commented Feb 5, 2025

Description

This all started in #36168 with adding support for ORM's type-safe annotated queries to Panache a lifetime ago. The original plan was to support them via static native methods on the entities and repositories.

Since then, Jakarta Data (JD) got added, with their own type-safe annotations, but also their own repo type, orthogonal to the Panache repository classes. And with a focus on stateless sessions, which were not supported in ORM/Panache.

I pivoted to a different kind of API which would allow us to mix managed and stateless sessions as we wanted. This required a lot of trial and error. This is what I demoed to the team last summer.

Since then, #44473 started supporting mixing ORM and HR in the same application, allowing users to mix and match blocking and reactive operations using the same entities. Which, again, was absolutely not supported in ORM/HR/Panache.

Thus I pivoted once more to try to apply the same API to unify blocking and reactive in what became known as Panache 2.

Some of the problems with Panache 1

  • We have two separate extensions ORM/Panache and HR/Panache, with their own entity superclass with similar operations that do not share a common type. This means we cannot have a Panache entity that supports both types of operations.
  • We have a split in style between placing operations on the entities and in a repository
  • Testing and mocking Panache entities requires a custom module
  • Some of the signatures of Panache entity operations are problematic when used in for loops or in HR method chaining
  • It does not support stateless operations
  • It does not support mixing blocking and reactive operations
  • Its requirement of using classes for repositories (as opposed to interfaces) runs counter to the type-safe methods of ORM and JD
  • Has its own Query type that was improving on the JPA query type, but since then, ORM added SelectionQuery and MutationQuery and others that may be worth exposing or switching to
  • Has its own Sort and Page types that since then got added to both ORM and JD (so now we have 3, great)

Panache 2

  • Supports mixing stateless, managed, blocking and reactive in a single API
  • Supports defining generic entities, or specialised entities (stateless/managed/blocking/reactive) and in every case, the option to switch to another mode of operation via an API
  • Unifies entity operations and repository operations by using nested interfaces for operations
  • Solves the UX issue of repository injection by obtaining the repository instances via generated static methods on the metamodel
  • Supports type-unsafe repositories out of the box for every entity, and every mode of operation (stateless/managed/blocking/reactive)
  • Supports type-safe repositories in addition, or mixed with type-unsafe

Examples

An old-style managed blocking entity with type-unsafe operations

@Entity
public class MyEntity extends PanacheManagedBlockingEntity {
  public String name;
}

class Example {
  @Transactional
  public void example(){
    MyEntity entity = new MyEntity();
    entity.name = "Stef";
    entity.persist();

    // obtain the default generated repository instance (can also be injected)
    // same type-unsafe operations as Panache 1
    MyEntity_.managedBlocking().find("name", "Stef");
    MyEntity_.managedBlocking().delete("name", "Stef");
  }
}

Same, but defining the operations in the repository

Most times, we should provide a type-safe API for our operations, rather than let any entity user do HQL.

@Entity
public class MyEntity extends PanacheManagedBlockingEntity {
  public String name;

  public interface Queries extends PanacheManagedBlockingRepository<MyEntity> {
    public default List<MyEntity> findByName(String name){
      return find("name", name);
    }
    public default long deleteByName(String name){
      return delete("name", name);
    }
  }
}

class Example {
  @Transactional
  public void example(){
    MyEntity entity = new MyEntity();
    entity.name = "Stef";
    entity.persist();

    // obtain repository instance (can also be injected)
    MyEntity_.queries().findByName("Stef");
    MyEntity_.queries().deleteByName("Stef");
  }
}

Adding type-safe queries

So far, we're only using features on-par with Panache 1. Now let's translate our type-unsafe queries to type-safe queries:

@Entity
public class MyEntity extends PanacheManagedBlockingEntity {
  public String name;

  public interface Queries {
    @Find
    public List<MyEntity> findByName(String name);
    @Delete
    public long deleteByName(String name);
  }
}

class Example {
  @Transactional
  public void example(){
    MyEntity entity = new MyEntity();
    entity.name = "Stef";
    entity.persist();

    // obtain repository instance (can also be injected)
    MyEntity_.queries().findByName("Stef");
    MyEntity_.queries().deleteByName("Stef");
  }
}

Naturally, you can mix and match type-safe and type-unsafe queries.

Overriding the operation nature

We've only seen managed blocking operations, but let's see how we can override the type of operation while keeping the default mode.

@Entity
public class MyEntity extends PanacheManagedBlockingEntity {
  public String name;
}

class Example {
  @Transactional
  public void exampleStateless(){
    MyEntity entity = new MyEntity();
    entity.name = "Stef";
    entity.statelessBlocking().persist();

    // obtain generated repository instance (can also be injected)
    MyEntity_.statelessBlocking().find("name", "Stef");
    MyEntity_.statelessBlocking().delete("name", "Stef");
  }
  @WithTransaction
  public Uni<Void> exampleStatelessReactive(){
    MyEntity entity = new MyEntity();
    entity.name = "Stef";
    return entity.statelessReactive().persist()
     .chain((ignore) -> MyEntity_.statelessReactive().find("name", "Stef"))
     .chain((ignore) -> MyEntity_.statelessReactive().delete("name", "Stef"));
  }
}

Out of the box, you get .managedBlocking(), .managedReactive(), .statelessBlocking() and .statelessReactive() operations on both the entity and the metamodel, so you can override the default mode of operation for your entity.

Changing the default mode of operation

Naturally we can default to stateless and reactive:

@Entity
public class MyEntity extends PanacheStatelessReactiveEntity {
  public String name;
}

class Example {
  @WithTransaction
  public Uni<Void> exampleStatelessReactive() {
    MyEntity entity = new MyEntity();
    entity.name = "Stef";
    return entity.persist()
     .chain((ignore) -> MyEntity_.statelessReactive().find("name", "Stef"))
     .chain((ignore) -> MyEntity_.statelessReactive().delete("name", "Stef"));
  }
}

Multiple operation modes

You can also add APIs for all the operation modes you want to support, in any kind of mix and match between stateless/managed, reactive/blocking, type-safe/type-unsafe, jakarta data, whatever… All these nested interfaces will get you a static method on the metamodel to access it if you don't want to inject it.

@Entity
public class MyEntity extends PanacheManagedBlockingEntity {
  public String name;

  public interface TypeSafeQueries {
    @Find
    public List<MyEntity> findByName(String name);
    @Delete
    public long deleteByName(String name);
  }

  public interface TypeUnsafeQueries extends PanacheManagedBlockingRepository<MyEntity> {
    public default void findByName(String name){
      return find("name", name);
    }
    public default void deleteByName(String name){
      return delete("name", name);
    }
  }

  public interface StatelessReactiveQueries extends PanacheStatelessReactiveRepository<MyEntity> {
    public default Uni<List<MyEntity>> findByName(String name){
      return find("name", name);
    }
    public default Uni<Long> deleteByName(String name){
      return delete("name", name);
    }
  }

  // this is stateless blocking  
  public interface JakartaData extends CrudRepository<MyEntity> {
    @Find
    public List<MyEntity> findByName(String name);
    @Delete
    public long deleteByName(String name);
  }
}

Status

  • API is mostly stabilised
  • This is a new module for now, not sure we can retrofit it in the old module
  • Needs tests
  • Needs hibernate-processor changes to be sent upstream
  • Needs support for mixing ORM and HR to land
  • Needs to finish support for the reactive mode
  • Needs support for stateless sessions in HR/Panache (Hibernate Reactive with Panache: support stateless sessions #46091)
  • Needs docs
  • Later: needs support from other extensions that support Panache 1 (rest-data, renarde, make a list of others)

Implementation ideas

No response

@FroMage FroMage added area/panache kind/enhancement New feature or request labels Feb 5, 2025
@FroMage FroMage self-assigned this Feb 5, 2025
Copy link

quarkus-bot bot commented Feb 5, 2025

/cc @loicmathieu (panache)

@yrodiere
Copy link
Member

yrodiere commented Feb 5, 2025

This looks like a great improvement over the current situation! Thanks a lot for all this work.

I think the challenge will be to make it look simple -- it's definitely simpler than what we have now, but could feel overwhelming for new users. With that in mind, here are a few suggestions... Mostly cosmetic/naming ones.


public class MyEntity extends PanacheManagedBlockingEntity {

Do we really want to force users to make that "managed blocking" choice? I suspect some will not need the .persist() method on the entity itself -- especially if they use both blocking and stateless...

You could maybe have a PanacheEntity class without any methods, and subclasses that expose these blocking/reactive managed/stateless methods?

Another suggestion, if you're going to have many declinations of the same class, you may want to go for a more "discoverable" structure with nested classes like this:

@MappedSuperclass
public class PanacheEntity {
    @Id
    @GeneratedValue
    public Long id;

    @MappedSuperclass
    public static class ManagedBlocking extends PanacheEntity {
        // Relevant methods...
    }
    @MappedSuperclass
    public static class StatelessBlocking extends PanacheEntity {
        // Relevant methods...
    }
    @MappedSuperclass
    public static class ManagedReactive extends PanacheEntity {
        // Relevant methods...
    }
    @MappedSuperclass
    public static class StatelessReactive extends PanacheEntity {
        // Relevant methods...
    }
}

Then:

public class MyEntity extends PanacheEntity.ManagedBlocking {

This has the advantage of not making it seem that an entity is "stateless" -- that adjective mostly makes sense when applied to the session, so PanacheStatelessBlockingEntity is a bit confusing.


@Entity
public class MyEntity extends PanacheManagedBlockingEntity {
  public String name;
}

class Example {
  @Transactional
  public void example(){
    MyEntity entity = new MyEntity();
    entity.name = "Stef";
    entity.persist();

    // obtain the default generated repository instance (can also be injected)
    // same type-unsafe operations as Panache 1
    MyEntity_.managedBlocking().find("name", "Stef");
    MyEntity_.managedBlocking().delete("name", "Stef");
  }
}

We may want to shorten these managedBlocking things? managed vs managedRx maybe? Though the managed one may lead to errors in reactive applications that didn't notice it's blocking...


@Entity
public class MyEntity extends PanacheManagedBlockingEntity {
  public String name;

  public interface Queries extends PanacheManagedBlockingRepository<MyEntity> {
    public default List<MyEntity> findByName(String name){
      return find("name", name);
    }
    public default long deleteByName(String name){
      return delete("name", name);
    }
  }
}

I'd suggest leaving out unnecessary public keywords in code examples (documentation/quickstarts), so it appears slightly less verbose:

@Entity
public class MyEntity extends PanacheManagedBlockingEntity {
  public String name;

  public interface Queries extends PanacheManagedBlockingRepository<MyEntity> {
    default List<MyEntity> findByName(String name) {
      return find("name", name);
    }
    default long deleteByName(String name) {
      return delete("name", name);
    }
  }
}

That being said, I suspect this code, in Kotlin, with single-expression functions, could feel absolutely wonderful...


Relatedly, if we're going to call these "repositories"... shouldn't we suggest naming conventions that better match that? E.g.

@Entity
public class MyEntity extends PanacheManagedBlockingEntity {
  public String name;

  public interface Repo extends PanacheManagedBlockingRepository<MyEntity> {
    default List<MyEntity> findByName(String name) {
      return find("name", name);
    }
    default long deleteByName(String name) {
      return delete("name", name);
    }
  }
}

Then code could access it with MyEntity_.repo(), which makes more sense IMO (since queries() would still expose things like persist(), right?).


@Entity
public class MyEntity extends PanacheManagedBlockingEntity {
  public String name;

  public interface Queries {
    @Find
    public List<MyEntity> findByName(String name);
    @Delete
    public long deleteByName(String name);
  }
}

Following my suggestions above:

@Entity
public class MyEntity extends PanacheManagedBlockingEntity {
  public String name;

  public interface Repo {
    @Find
    List<MyEntity> findByName(String name);
    @Delete
    long deleteByName(String name);
  }
}

Overriding the operation nature

We've only seen managed blocking operations, but let's see how we can override the type of operation while keeping the default mode.

@Entity
public class MyEntity extends PanacheManagedBlockingEntity {
  public String name;
}

class Example {
  @Transactional
  public void exampleStateless(){
    MyEntity entity = new MyEntity();
    entity.name = "Stef";
    entity.statelessBlocking().persist();

    // obtain generated repository instance (can also be injected)
    MyEntity_.statelessBlocking().find("name", "Stef");
    MyEntity_.statelessBlocking().delete("name", "Stef");
  }
  @WithTransaction
  public Uni<Void> exampleStatelessReactive(){
    MyEntity entity = new MyEntity();
    entity.name = "Stef";
    return entity.statelessReactive().persist()
     .chain((ignore) -> MyEntity_.statelessReactive().find("name", "Stef"))
     .chain((ignore) -> MyEntity_.statelessReactive().delete("name", "Stef"));
  }
}

Out of the box, you get .managedBlocking(), .managedReactive(), .statelessBlocking() and .statelessReactive() operations on both the entity and the metamodel, so you can override the default mode of operation for your entity.

I fear this will be a lot of freedom, and will end up being confusing... Don't get me wrong, you improved the situation. But now all the options are unified, it's very clear there is a lot of options :D

I don't have a suggestion to reduce the chance of users being confused, though; this may be something we'll need to live with.


Changing the default mode of operation

Naturally we can default to stateless and reactive:

@Entity
public class MyEntity extends PanacheStatelessReactiveEntity {
  public String name;
}

class Example {
  @WithTransaction
  public Uni<Void> exampleStatelessReactive() {
    MyEntity entity = new MyEntity();
    entity.name = "Stef";
    return entity.persist()
     .chain((ignore) -> MyEntity_.statelessReactive().find("name", "Stef"))
     .chain((ignore) -> MyEntity_.statelessReactive().delete("name", "Stef"));
  }
}

Shouldn't we expose the default repository with a default name, so that people who only care about the default repo can use it more easily?

E.g. we could generate a repo() method that returns the default repository -- unless there is an inner class named Repo in the entity, in which case that would be returned.


Multiple operation modes

You can also add APIs for all the operation modes you want to support, in any kind of mix and match between stateless/managed, reactive/blocking, type-safe/type-unsafe, jakarta data, whatever… All these nested interfaces will get you a static method on the metamodel to access it if you don't want to inject it.

@Entity
public class MyEntity extends PanacheManagedBlockingEntity {
  public String name;

  public interface TypeSafeQueries {
    @Find
    public List<MyEntity> findByName(String name);
    @Delete
    public long deleteByName(String name);
  }

  public interface TypeUnsafeQueries extends PanacheManagedBlockingRepository<MyEntity> {
    public default void findByName(String name){
      return find("name", name);
    }
    public default void deleteByName(String name){
      return delete("name", name);
    }
  }

  public interface StatelessReactiveQueries extends PanacheStatelessReactiveRepository<MyEntity> {
    public default Uni<List<MyEntity>> findByName(String name){
      return find("name", name);
    }
    public default Uni<Long> deleteByName(String name){
      return delete("name", name);
    }
  }

  // this is stateless blocking  
  public interface JakartaData extends CrudRepository<MyEntity> {
    @Find
    public List<MyEntity> findByName(String name);
    @Delete
    public long deleteByName(String name);
  }
}

Great example to show how powerful this is, but... if it ends up in the docs, I think it could use some meaningful naming conventions/recommendations :)


* Needs hibernate-processor changes to be sent upstream

Please, pretty please don't hardcode Panache class names in these PRs :X We'd really need this stuff to use the serviceloader so that Panache contributes whatever it needs to contribute. See https://hibernate.atlassian.net/browse/HHH-18159 , though that can be solved later -- I'd just like to avoid making the problem worse :)

Also, just so you now, we now have a build of Quarkus running on every PR to maintenance branches (e.g. ORM 6.6 testing against Quarkus 3.15), so adding a dependency from ORM to Quarkus/Panache2 may not be necessary... Not sure.

* Needs support for mixing ORM and HR to land

For the record (and for others reading this), this is work in progress:

#13425

#44473

@loicmathieu
Copy link
Contributor

Very interesting; I agree with @yrodiere that it first looks complicated.
Having a lot of options is great but user may find it hard to understand what they are needed.

I noticed you use MyEntity_ when calling generated methods, aren't these methods generated on a different class? This has a high risk of being not easy to discover.

We may want to shorten these managedBlocking things? managed vs managedRx maybe? Though the managed one may lead to errors in reactive applications that didn't notice it's blocking...

Another solution is to use grouping, this is more verbose but easier to use (and on par with Mutiny that uses it a lot).

So maybe MyEntity_.managed().blocking(), MyEntity_.stateless().reactive(), ...

I also need some time to investigate what would need to be done to port existing MongoDB with Panache to these concept so we it didn't stay behind.
I assume the existing Panache 1 extensions would be deprecated.

@Serkan80
Copy link

Serkan80 commented Feb 6, 2025

I was gonna suggest the same as @yrodiere, remove the “blocking” name everywhere. It’s less verbose and by having an reactive alternative, it suggests that the default one is blocking.

And currently the blocking version of Panache also doesn’t use this term.

Btw, great job and this was something I was looking forward for.

And a question that comes to my mind: will this affect the resource usage of the app, since everything is included in 1 jar ? Let’s say that you don’t use any of the reactive classes, will Quarkus clean these up during the build phase ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/panache kind/enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

4 participants