jakartaee / persistence

https://jakartaee.github.io/persistence/
Other
196 stars 58 forks source link

Introduce application-provided custom Generators #342

Open jgrassel opened 2 years ago

jgrassel commented 2 years ago

During the discussions for #319 I had thrown out the idea of application provided id generators. While an application can always simply assign an id value to any new entity it constructs, there could be use-cases where it is desirable for the identity to be generated when the persistence provider is about to INSERT the row into the database (the typical behavior for the standard identity generators <= JPA 3.0), which typically occurs during a flush to the database or transaction commit.

Callbacks might be able to do this job some of the time, however given the rule that they should not invoke EntityManager and query operations [JPA 2.2: 3.5.2] means that they cannot call EnityManager.unwrap(Connection.class) in order to get at the underlying current connection, in order to access the database with a Connection already enlisted with the current transaction. There is also the fact that the spec grants vendors a great deal of freedom to decide when a lifecycle callback can be invoked, where a generator should be called specifically when the identity needs to be generated (ie, so if a new application persists a new entity, then later decides to roll back the transaction, a potentially expensive generator call is not exercised, nor is the generated namespace wasted.)

Therefore, I would like to propose the addition of custom generators which can be defined by applications that have a need for them. A custom generator could follow much of the same declarative format as entity listeners, and could conceivably even benefit from being bean managed which could make this a very powerful feature.

An example generator could be as follows:

Class WidgetUUIDGenerator:

import java.util.UUID;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.annotation.Resource;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

import jakarta.persistence.CustomGenerator;
import jakarta.persistence.IDGenerator;
import jakarta.persistence.PersistenceUnitSetup;

@ApplicationScoped
@CustomGenerator(name="WidgetUUIDGenerator", generates=UUID.class)
public class WidgetUUIDGenerator {
   /*
    * Everything here is optional
    */

   @Resource
   private PersistenceUnitSetup puSetup; // Persistence unit configuration

   @Resource
   private DataSource injectableDataSource; // Generator may need to talk to a database

   @Inject
   private MyService myService;  // Some CDI injected dependency

   @PostConstruct
   public void initialize() {
      // Initialization Code
   }

   @PreDestroy
   public void shutdown() {
      // Perform cleanup such as releasing resources
   }

   /*
    * Everything here is required, as the generator should define at least one
    * method annotated with @IDGenerator and ideally there should be a default
    * generator with no arguments.
    *
    * Each generator method returns a value of the same type identified in the
    * @CustomGenerator annotation's generates value.
    */

   @IDGenerator
   public UUID generate() {
      // Default @IDGenerator

      UUID newID;
      // ...
      return newID;
   }

   @IDGenerator
   public UUID generate(AnotherWidgetEntity entity) {
      // @IDGenerator specific for entities of type AnotherWidgetEntity

      UUID newID;
      // ...
      return newID;
   }
}

Class WidgetEntity:

import jakarta.persistence.*;

@Entity
public class WidgetEntity {
   @Id 
   @GeneratedValue(strategy=CUSTOM, generator="WidgetUUIDGenerator")
   private UUID widgetId;

   // etc
}

Class AnotherWidgetEntity:

import jakarta.persistence.*;

@Entity
public class AnotherWidgetEntity {
   @Id 
   @GeneratedValue(strategy=CUSTOM, generator="WidgetUUIDGenerator")
   private UUID widgetId;

   // etc
}

In the example above, a custom generator by the name WidgetUUIDGenerator is defined to generate new identities of type UUID. It requires for there to be at least one method annotated with @IDGenerator which returns the same type as declared by @CustomGenerator's generates value. We could even overload the generator method with a parameter which defines which specific type of entity, to make it easier to customize generation by entity class if that is desired.

By making it optional for the generator class to accept CDI bean management (taking the precedence established by callback and converters), this could be a very powerful capability.

The @PostConstruct and @PreDestroy annotated methods are optional elements, which would be important if the custom generator needs to initialize resources on creation and dispose of them cleanly when the custom generator itself is deactivated when the EntityManagerFactory is closed.

One last thing is the PersistenceUnitSetup class. Given that an application may be composed of multiple persistence units, it may be important for a custom generator to be able to distinguish which persistence unit it is servicing. That is the role of the PersistenceUnitSetup class, which exposes some, if not all, of the content of the PersistenceUnitInfo. I considered simply using the PersistenceUnitInfo class, but opted not to since that resides in jakarta.persistence.spi and referencing a spi class might not be good form. I've presently left it undefined at the moment, but the most likely minumum content it should provide is the persistence unit name and the persistence unit properties associated with the persistence unit.

dazey3 commented 2 years ago

While an application can always simply assign an id value to any new entity it constructs, there could be use-cases where it is desirable for the identity to be generated when the persistence provider is about to INSERT the row into the database (the typical behavior for the standard identity generators <= JPA 3.0), which typically occurs during a flush to the database or transaction commit.

I think this is the key for me. What usecases can benefit from such a feature that cannot already be addressed with "custom" application code? It could be that we want to define that boilerplate, custom application generator code with our own API, but I'd like some ideas on what circumstances users would find this feature useful.

jgrassel commented 1 year ago

@dazey3 The problem with using callbacks is because of the variability as to when they get called. For example, the "pre-persist" callback. From the spec:

The PrePersist and PreRemove callback methods are invoked for a given entity before the respective
EntityManager persist and remove operations for that entity are executed. For entities to which the
merge operation has been applied and causes the creation of newly managed instances, the PrePersist
callback methods will be invoked for the managed instance after the entity state has been copied to it.
These PrePersist and PreRemove callbacks will also be invoked on all entities to which these operations
are cascaded. The PrePersist and PreRemove methods will always be invoked as part of the
synchronous persist, merge, and remove operations.

The thing here is, is that exactly when the prePersist callback is called is vendor specific. It could be invoked before em.persist(myNewEntity) returns control back to the application, or it could be deferred to whenever the new entity is flushed to the database, whether from an implicit or explicit flush or as part of the before-completion phase of transaction commit.

Now, the JPA spec doesn't exactly define when the persistence context fulfills generating identities for entities using @GeneratedValue but in my experience with OpenJPA and EclipseLink, it waits until flush/tx commit -- which is why calling the getter method for the identity field for those entities usually returns null until that point in the persistence context lifecycle has been reached. Thus, having a custom generator that executes at points consistent with the provided generators may be advantageous.

That and while the callback contract does say "A lifecycle callback method may modify the non-relationship state of the entity on which it is invoked.", it may be iffy in a vendor specific way for a callback to make changes to the entity's identity field. By having a custom generator, even if it feels a lot like a PrePersist callback, feels a lot clearer in purpose, and can be laser targeted for use with the @GeneratedValue annotation.

gavinking commented 1 year ago

The one thing I would like to mention here is my proposal here that generators by applied via meta-annotating an annotation type, rather than by explicitly specifying the generator class in the @GeneratedValue annotation.

So instead of the code above, we would have:

Annotation WidgetGenerator

@Target({FIELD,METHOD})
@Retention(RUNTIME)
@Generator(WidgetUUIDGenerator.class)
public @interface GeneratedWidgetUUID {}

Class WidgetEntity:

import jakarta.persistence.*;

@Entity
public class WidgetEntity {
   @Id 
   @GeneratedWidgetUUID
   private UUID widgetId;

   // etc
}

Class AnotherWidgetEntity:

import jakarta.persistence.*;

@Entity
public class AnotherWidgetEntity {
   @Id 
   @GeneratedWidgetUUID
   private UUID widgetId;

   // etc
}

Which cleans up the usage-side of the code and raises the level of abstraction slightly.