I am deleting rows in a batch as follows (in an EJB).
int i=0;
List<Category> list = // Sent by a client which is JSF in this case.
for(Category category:list) {
if(++i%49==0) {
i=0;
entityManager.flush();
}
entityManager.remove(entityManager.contains(category) ? category : entityManager.merge(category));
}
Where Category is a JPA entity.
There is a callback that listens to this delete event.
#ApplicationScoped
public class CategoryListener {
#PostPersist
#PostUpdate
#PostRemove
public void onChange(Category category) {
//...
}
}
This callback method is invoked as many times as the number of rows which are deleted. For example, this method will be called 10 times, if 10 rows are deleted.
Is there a way to invoke the callback method only once at the end of a transaction i.e. as soon as the EJB method in which this code is executed returns or at least per batch i.e. when entityManager.flush(); occurs? The former is preferred in this case.
Additional Information :
I am doing some real time updates using WebSockets where clients are to be notified when such CRUD operations are performed on a few database tables. It is hereby meaningless to send a message to all the associated clients on deletion of every row which is performed in a batch - every time a single row is deleted. They should rather be notified only once/at once (as soon as) a transaction (or at least a batch) ends.
The following JPA 2.1 criteria batch delete approach does not work because it does not directly operate upon entities. No JPA callbacks will be triggered by this approach neither by using its equivalent JPQL.
CriteriaBuilder criteriaBuilder=entityManager.getCriteriaBuilder();
CriteriaDelete<Category> criteriaDelete = criteriaBuilder.createCriteriaDelete(Category.class);
Root<Category> root = criteriaDelete.from(entityManager.getMetamodel().entity(Category.class));
criteriaDelete.where(root.in(list));
entityManager.createQuery(criteriaDelete).executeUpdate();
I am using EclipseLink 2.5.2 having JPA 2.1
Unfortunately JPA provides entity callbacks, which are required to be called for each entity instances they listen on, so you will need to add in your own functionality to see that the listener is triggered only once per batch/transaction etc. The other alternative is to use provider specific behavior, in this case EclipseLink's session event listeners: https://wiki.eclipse.org/Introduction_to_EclipseLink_Sessions_(ELUG)#Session_Event_Manager_Events to listen for the PostCalculateUnitOfWorkChangeSet event or some other event that gets triggered when you need.
Related
Hi have created a EventSubscriber in TypeORM to listen to a specific entity and it's events on database level (quite straightforward)
But this Subscriber is being triggered at any CRUD operation in any table, or maybe fired due to indirect relations with the targeted entity (hopefully not) without the targeted entity/table not being CRUD-ed
This is how my subscriber looks:
#EventSubscriber()
export class ImpactViewSubscriber
implements EntitySubscriberInterface<TargetedEntity>
{
logger: Logger = new Logger(ImpactViewSubscriber.name);
listenTo(): any {
return TargetedEntity;
}
afterTransactionCommit(event: TransactionCommitEvent): Promise<any> | void {
this.logger.log(`Event subscriber fired...`);
return event.queryRunner.query(
`some query...`,
);
}
}
And it's (apparently) properly imported in typeorm.config.ts
....
subscribers: [join(__dirname, '..', '**/*.subscriber.{ts,js}')],
So for some reason the logic inside afterTransactionCommit() is being triggered at any interaction of any table, also when I first run the app (which is annoying).
What am I doing wrong? I just want to fire the logic when any CRUD operation is donde to my target entity, ideally after a transaction, as my target entity will only receive bulk INSERTS or DELETES
Any idea of where is the error?
Thanks in advance
UPDATE
I also tested using afterInsert() and afterRemove()
which does not make the logic get triggered at any event of any other table, but it is being triggered for each row inserted in the target table. And since I only have bulk operations, this is not useful.
My use cases are: Bulk inserts in the table, and bulk deletes by cascade. I am making sure those happens on a single transaction. Any ideas as to what can I do using typeorm avoiding to manually create specific DB triggers or similar?
Thanks!
I know this is a quite old post.
But have you tried to remove :any from the listener call?
listenTo() {
return TargetedEntity;
}
working with Spring data JPA and having this method in a #Service class
#Transactional(propagation = Propagation.NEVER)
public void getClientBydIdThreeTimes() {
clientRepository.findById(1L);
clientRepository.findById(1L);
clientRepository.findById(1L);
}
will it hit the database three times? It shouldnt because there is not
transactional environment(propagation = Propagation.NEVER) and each
query its a transaction itself and therefore each time the query is
executed, and entityManager is created having its own persistent
context, right?
I am having a wierd behaviour because when I make an http request and this method is executed, only the first query is sent to the database caching the next two calls but if I call this method from inside my application(like a Spring batch task), there are three sql sent to the database. I dont understand it, it should have the same behaviour, right?
Thanks
I'm trying to figure out how to use Entity Framework Cores 2.1 new ChangeTracker.Tracked Event to hook into reading queries. Unfortunately, I'm not being able to understand how to implement this.
Since it's a new feature it's not possible to find any articles on it and the official Microsoft docs site does not provide any help or sample code.
My scenario is pretty simple. I have a database with following columns:
id, customerId, metadata.
When a user queries this table I want to intercept the query result set and for every row, I want to compare the customerId with currently logged in user.
I'm hoping that ChangeTracker.Tracked Event can help me in intercepting the return result set. I'm looking for some sample code on how to achieve above.
Here is a sample usage of the ChangeTracker.Tracked event.
Add the following method to your context (requires using Microsoft.EntityFrameworkCore.ChangeTracking;):
void OnEntityTracked(object sender, EntityTrackedEventArgs e)
{
if (e.FromQuery && e.Entry.Entity is YourEntityClass)
{
var entity = (YourEntityClass)e.Entry.Entity;
bool isCurrentUser = entity.customerId == CurrentUserId;
// do something (not sure what)
}
}
and attach it to the ChangeTracker.Tracked even in your context constructor:
ChangeTracker.Tracked += OnEntityTracked;
As described in the Tracked event documentation:
An event fired when an entity is tracked by the context, either because it was returned from a tracking query, or because it was attached or added to the context.
Some things to mention.
The event is not fired for no-tracking queries
The event is fired for each entity instance created by the tracking query result set and not already tracked by the context
The bool FromQuery property of the event args is used to distinguish if the event is fired from the tracking query materialization process or via user code (Attach, Add etc. calls).
The EntityEntry Entry property of the event args gives you access to the entity instance and other related information (basically the same information that you get when calling the non-generic DbContext.Entry method)
I have a method persistData() which persists an entity object. I have another method findData() which performs find() operation on the same entity class for the primary key value which was persisted. When I call the findData() in the #PostPersist of the entity class, I get a null pointer exception. This has raised a few questions in my mind:
Why is it giving a null pointer error?
What is the use of #PostPersist in reality?
When is a #Postpersist actually called? After commit, during commit or before commit?
Any further insights would also be appreciated. Please find the relevant code and stacktrace below:
public void persistData(){
EntityManagerFactory fac= Persistence.createEntityManagerFactory("test");
EntityManager man = fac.createEntityManager();
Employee e = new Employee();
e.setEmpId(500);
e.setEmpName("Emp5");
e.setSalary(5000);
man.getTransaction().begin();
man.persist(e);
man.getTransaction().commit();
man.close();
}
public void findData(){
EntityManagerFactory fac= Persistence.createEntityManagerFactory("test");
EntityManager man = fac.createEntityManager();
Employee e=man.find(Employee.class, 500);
System.out.println(e.getEmpName());
man.close();
}
#PostPersist
public void getData(){
new Service().findData();
}
Stack Trace ( Partial ):
Exception in thread "main" javax.persistence.RollbackException: java.lang.NullPointerException
at oracle.toplink.essentials.internal.ejb.cmp3.transaction.base.EntityTransactionImpl.commit(EntityTransactionImpl.java:120)
at oracle.toplink.essentials.internal.ejb.cmp3.transaction.EntityTransactionImpl.commit(EntityTransactionImpl.java:60)
at Service.persistData(Service.java:18)
at Service.main(Service.java:34)
Caused by: java.lang.NullPointerException
at Service.findData(Service.java:28)
at Employee.getData(Employee.java:33)
Note: I am using JPA 1.0
To answer your question 1:
(need the code and the stacktrace)
To answer your question 2:
The #PostPersist indicate a JPA callback method. It allows you to trigger some code through the entity life-cycle events.
A real life example ?
Assume you have a User table and you want to generate a confirmation email every time a new User is persisted: you can do it in a PostPersist method.
To answer your question 3:
The relevant part of specs are in blod.
From JPA-2.0 specs:
The PostPersist and PostRemove callback methods are invoked for an entity after the entity has been made persistent or removed. These callbacks will also be invoked on all entities to which these operations are cascaded. The PostPersist and PostRemove methods will be invoked after the database insert and delete operations respectively. These database operations may occur directly after the persist, merge, or remove operations have been invoked or they may occur directly after a flush operation has occurred (which may be at the end of the transaction). Generated primary key values are available in the PostPersist method.
One real life example of #PostPersist which i am using is --
I am creating a task management system. In this task management system, task is assigned to an agent. However, there can be scenarios when task does not get an agent is assigned automatically by source system. In this scenario, i am triggering an event whenever an task is persisted and a task allocation engine listens to that event and does its processing.
I am sure there are other ways of doing the same thing, but i thought this Async way makes system better from performance perspective.
I have a named query that returns a Collection of entities.
These entities have a #PreUpdate-annotated method on them. This method is invoked during query.getResultList(). Because of this, the entity is changed within the persistence context, which means that upon transaction commit, the entity is written back to the database.
Why is this? The JPA 2.0 specification does not mention explicitly that #PreUpdate should be called by query execution.
The specification says:
The PreUpdate and PostUpdate callbacks occur before and after the
database update operations to entity data respectively. These database
operations may occur at the time the entity state is updated or they
may occur at the time state is flushed to the database (which may be
at the end of the transaction).
In this case calling query.getResultList() triggers a em.flush() so that the query can include changed from current EntityManager session. em.flush() pushes all the changes to the database (makes all UPDATE,INSERT calls). Before UPDATE is sent via JDBC #PreUpdate corresponding hooks are called.
This is just my comment from rzymek's answer with some follow up code:
I tried to reproduce the problem OP had, because it sounded like the EntityManager would get flushed everytime the query is called. But that's not the case. #PostUpdate methods are only called when there is actual changed being done to the Database as far as I can tell. If you made a change with the EntityManager that is not yet flushed to the DB query.getResultList will trigger the flush to the DB which is the behaviour one should expect.
Place valinorDb = em.find(Place.class, valinorId);
// this should not trigger an PostUpdate and doesn't
// TODO: unit-testify this
em.merge(valinorDb);
valinorDb.setName("Valinor123");
valinorDb.setName("Valinor");
// this shouldn't trigger an PostUpdate because the Data is the same as in the beginning and doesn't
em.merge(valinorDb);
{
// this is done to test the behaviour of PostUpdate because of
// this:
// http://stackoverflow.com/questions/12097485/why-does-a-jpa-preupdate-annotated-method-get-called-during-a-query
//
// this was tested by hand, but should maybe changed into a unit
// test? PostUpdate will only get called when there is an actual
// change present (at least for Hibernate & EclipseLink) so we
// should be fine
// to use PostUpdate for automatically updating our index
// this doesn't trigger a flush as well as the merge didn't even trigger one
Place place = (Place) em.createQuery("SELECT a FROM Place a")
.getResultList().get(0);
Sorcerer newSorcerer = new Sorcerer();
newSorcerer.setName("Odalbort the Unknown");
place.getSorcerers().add(newSorcerer);
//this WILL trigger an PostUpdate as the underlying data actually has changed.
place = (Place) em.createQuery("SELECT a FROM Place a")
.getResultList().get(0);
}
In my case JPA Event Listener (#EntityListeners) calls query.getResultList() in its logic (to do some validation) and in effect goes into
neverending loop that calls the same listener once again and again and in the end got StackOverflowError. I used flush-mode = COMMIT to avoid flush on query like below. Maybe for someone it will be helpful.
List l = entityManager.createQuery(query)
/**
* to NOT do em.flush() on query that trigger
* #PreUpdate JPA listener
*/
.setFlushMode(FlushModeType.COMMIT)
.getResultList();