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

first rough proposal for events #876

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open

Conversation

gavinking
Copy link
Contributor

@gavinking gavinking commented Oct 24, 2024

See #373. [Note that the first to propose this was @hantsy.]

Here's a rough and fairly minimal idea for what this could look like.

Note that here there is an Event class for each kind of event, instead of a single LifecycleEvent with @Before @Update and @After @Insert qualifiers on the observer side. The reason for this is that we don't have a dependency on CDI, so I can't use the @Qualifier annotation to declare event qualifier types.

An event observer would look like:

void afterUpdate(@Observes PostUpdateEvent<Book> event) { ... }

I would love to be able to filter by entity type, but I don't think CDI allows filtering events by type argument. (At least it didn't in 1.0, though that was a long time ago.)

As a workaround for that, we could have @Before @Update @EntityType(Book.class) as qualifiers, but, again, that would pick up a dependency on CDI.

@gavinking
Copy link
Contributor Author

I would love to be able to filter by entity type, but I don't think CDI allows filtering events by type argument.

An intriguing possibility here would be to generate event types into the static metamodel, so that you could write:

void afterUpdate(@Observes _Book.PostUpdateEvent event) { ... }

@gavinking
Copy link
Contributor Author

I don't think CDI allows filtering events by type argument. (At least it didn't in 1.0, though that was a long time ago.)

I take that back. Apparently they relaxed this restriction, and you can now fire a parameterized event as long as the type argument is somehow resolvable, for example:

    @Inject
    Event<MyEvent<String>> event;

Or:

        beanManager.getEvent()
                .select(new TypeLiteral<MyEvent<String>>() {})
                .fire(new MyEvent<>(title));

So in fact we can use a parameterized event type, e.g. PostUpdateEvent<Book>.

Copy link
Contributor

@njr-11 njr-11 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is nicely written and I like the change to use the parameterized entity type rather than Object.

I made some minor suggestions and proposed switching the Abstract supertype for the event into an interface.

I addition to the inline review comments, there are a few other things we will need to consider.

One important item is clarifying what the user is (or is not) allowed to do with the entity instance that they get within the event. Are they allowed to modify it? And if so, do those modifications get persisted to the datastore?

The entity value returned on the Post events is a difficult question to consider. I can certainly see why you wrote it to state the value passed in the lifecycle method, because that makes the most sense for PostDeleteEvent (to avoid extra fetch of values) and is consistent with the Delete lifecycle method which only supports a void return value.
For the PostInsertEvent and PostUpdateEvent, I wonder if some implementations based on a JPA provider might inadvertently end up with the entity value updated to include an automatically generated id and autoincremented version, which would no longer match the passed in entity value. And having that information might be useful to the user as well, so I could see a case for defining these differently, but then they would be inconsistent. Not really sure what is best here, but I thought I would at least bring it up.

Comment on lines +15 to +28
public abstract class LifecycleEvent<E> {
private final E entity;

public LifecycleEvent(E entity) {
this.entity = entity;
}

/**
* The entity instance which was passed as an argument to
* the lifecycle method.
*/
public E entity() {
return entity;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LifecycleEvent could be an interface rather than an abstract class,

Suggested change
public abstract class LifecycleEvent<E> {
private final E entity;
public LifecycleEvent(E entity) {
this.entity = entity;
}
/**
* The entity instance which was passed as an argument to
* the lifecycle method.
*/
public E entity() {
return entity;
}
public interface LifecycleEvent<E> {
/**
* The entity instance which was passed as an argument to
* the lifecycle method.
*/
E entity();

api/src/main/java/jakarta/data/event/PostDeleteEvent.java Outdated Show resolved Hide resolved
Comment on lines +26 to +29
public class PostDeleteEvent<E> extends LifecycleEvent<E> {
public PostDeleteEvent(E entity) {
super(entity);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If making LifecycleEvent into an interface (earlier comment), then it PostDeleteEvent might be implemented as:

Suggested change
public class PostDeleteEvent<E> extends LifecycleEvent<E> {
public PostDeleteEvent(E entity) {
super(entity);
}
public record PostDeleteEvent<E>(E entity) implements LifecycleEvent<E> {

api/src/main/java/jakarta/data/event/PostInsertEvent.java Outdated Show resolved Hide resolved
api/src/main/java/jakarta/data/event/PreUpdateEvent.java Outdated Show resolved Hide resolved
Comment on lines +26 to +29
public class PreUpdateEvent<E> extends LifecycleEvent<E> {
public PreUpdateEvent(E entity) {
super(entity);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If switching to interface,

Suggested change
public class PreUpdateEvent<E> extends LifecycleEvent<E> {
public PreUpdateEvent(E entity) {
super(entity);
}
public record PreUpdateEvent<E>(E entity) implements LifecycleEvent<E> {

api/src/main/java/jakarta/data/repository/Delete.java Outdated Show resolved Hide resolved
api/src/main/java/jakarta/data/repository/Insert.java Outdated Show resolved Hide resolved
api/src/main/java/jakarta/data/repository/Update.java Outdated Show resolved Hide resolved
@gavinking
Copy link
Contributor Author

One important item is clarifying what the user is (or is not) allowed to do with the entity instance that they get within the event. Are they allowed to modify it? And if so, do those modifications get persisted to the datastore?

We can look at section 3.6.2 of the JPA spec for inspiration here.

For the PostInsertEvent and PostUpdateEvent, I wonder if some implementations based on a JPA provider might inadvertently end up with the entity value updated to include an automatically generated id and autoincremented version, which would no longer match the passed in entity value.

Yeah good point. Hrrm. Not sure.

@@ -115,6 +117,11 @@ This fragment shows how the application might request injection of a `CarReposit

This integration between CDI and Jakarta Data allows for seamless management of repository instances within Jakarta EE applications.

==== CDI Events
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should differentiate these lifecycle events from other events, such as Domain Event, see: #374

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly I don't really see what the difference is. How is a "Domain Event" different from a lifecycle event for an entity that you're choosing call an "aggregate"?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, there is no difference.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, that's what I thought.

@njr-11
Copy link
Contributor

njr-11 commented Oct 25, 2024

For the PostInsertEvent and PostUpdateEvent, I wonder if some implementations based on a JPA provider might inadvertently end up with the entity value updated to include an automatically generated id and autoincremented version, which would no longer match the passed in entity value.

Yeah good point. Hrrm. Not sure.

I tried this out on the EclipseLink JPA provider and observed it to be doing the following:

After invoking persist, the AutoGenerated Id value was updated on the passed-in entity, but the Version value was not incremented. After also invoking flush, then it updated the Version value on that entity instance.

After invoking merge, the Version value was not immediately incremented on the passed-in entity. As in the former case, after also invoking flush, it then updated the Version value on the passed-in entity instance.

Note that Jakarta Data's CrudRepository.insert has language like, "After invoking this method, do not continue to use the instance that is supplied as a parameter. This method makes no guarantees about the state of the instance that is supplied as a parameter." All of this makes me uncomfortable about supplying that same instance via the PostInsert/PostUpdate events. But I can also see why we don't want to require it to supply an updated entity instance. That might require some implementations to do extra fetching just for the sake of publishing events that the application might not even care about. Leaving out the entity from the event makes the Post event not useful because you cannot correlate it with the Pre event. I suppose one option would be to supply only the Id value and entity class (not instance) on the Post event which would at least identify it as matching the Pre event, but that is awkward. Not really sure what would be best.

@gavinking
Copy link
Contributor Author

Not really sure what would be best.

I mean with things like this, there's always the option of saying "portable applications should not rely on .... ".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Lifecycle CDI Event
4 participants