Skip to content

@DomainEvents method not being called on saveAll with Window<> #2938

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

Closed
jiaheng opened this issue Sep 12, 2023 · 8 comments
Closed

@DomainEvents method not being called on saveAll with Window<> #2938

jiaheng opened this issue Sep 12, 2023 · 8 comments
Assignees
Labels
type: bug A general bug

Comments

@jiaheng
Copy link

jiaheng commented Sep 12, 2023

I have an entity which will registerEvent when I update the field, and MyClassModifyEventHandler.handle() is expected to be executed upon repository.save() or repository.saveAll()

@DynamicInsert
@DynamicUpdate
@Entity
public class MyClass extends AbstractAggregateRoot<MyClass> {

    @Id private UUID id;

    @Column(name = "field")
    private String field;

    @CreationTimestamp Instant createdAt;
    @UpdateTimestamp Instant updatedAt;
    @Version Integer version;

    public void setField(String field) {
        this.field = field;
        registerEvent(new MyClassModifyEvent(this));
    }
}
@Component
public class MyClassModifyEventHandler {
    @EventListener
    public void handle(final MyClassModifyEvent persistenceEvent) {
        // do something
    }
}

I have a method which will query a list of MyClass and update their respective fields.

    @Transactional
    public void eventHandlerNotBeingCalled() {
        // ..
        Window<MyClass> myClasses =
                myClassRepo.findFirst20(ScrollPosition.keyset());

        for (MyClass myClass : myClasses) {
            myClass.setField("newField");
        }
        myClassRepo.findFirst20.saveAll(myClasses);
        // working solution
        // myClassRepo.findFirst20.saveAll(myClasses.toList());
    }

So with this, the @EventListener is not working even though the event is registered before saveAll(). If I calling saveAll on List<MyClass> instead (ie myClassRepo.saveAll(myClasses.toList())), the MyClassModifyEventHandler.handle will be called. Is this expected behaviour?

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Sep 12, 2023
@quaff
Copy link
Contributor

quaff commented Sep 12, 2023

Could you provide a minimal reproducer project?

@jiaheng
Copy link
Author

jiaheng commented Sep 13, 2023

Could you provide a minimal reproducer project?

@quaff here is the demo project https://github.com/jiaheng/spring-data-jpa-domain-event, and these are the steps to reproduce:

  1. Insert record(s) via POST /my-classes endpoint
curl -X 'POST' \
  'http://localhost:8080/my-classes' \
  -H 'accept: */*' \
  -d ''
  1. Trigger batch update via POST /my-classes/created/update endpoint
curl -X 'POST' \
  'http://localhost:8080/my-classes/created/update' \
  -H 'accept: */*' \
  -d ''

Based on the demo code EventHandler.handleMyClassUpdateEvent is not being called upon myClassRepository.saveAll(myClasses).
By changing line here to myClassRepository.saveAll(myClasses.toList());, EventHandler.handleMyClassUpdateEvent will be called using same steps above.

@quaff
Copy link
Contributor

quaff commented Sep 13, 2023

It's not minimal, and doesn't contains any test case.

@jiaheng
Copy link
Author

jiaheng commented Sep 13, 2023

It's not minimal, and doesn't contains any test case.

@quaff I updated spring-data-jpa-domain-event repo to strip away some unnecessary code and component. Also I added 2 test cases to compare the behaviour of using List<> and Window<>. Let me know if this works for you.

@quaff
Copy link
Contributor

quaff commented Sep 13, 2023

It can be minified to this:

package com.example.demo.service;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.event.EventListener;
import org.springframework.data.domain.AbstractAggregateRoot;
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Window;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.stereotype.Repository;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.then;

@EnableJpaRepositories(considerNestedRepositories = true)
@SpringBootTest
class DomainEventHandlerTest {

	@Autowired
	MyClassRepository myClassRepository;

	@Autowired
	MyClassService myClassService;

	@SpyBean
	EventHandler eventHandler;

	@Test
	void test() {
        myClassRepository.save(new MyClass());
		myClassService.update();
        then(eventHandler).should().handleMyClassUpdateEvent(any());
	}

	@TestConfiguration
	static class Config {
		@Bean
		MyClassService myClassService(MyClassRepository myClassRepository) {
			return new MyClassService(myClassRepository);
		}

		@Bean
		EventHandler eventHandler() {
			return new EventHandler();
		}
	}

	@Entity
	static class MyClass extends AbstractAggregateRoot<MyClass> {
		@Id
		@GeneratedValue
		private Integer id;

		private String field;

		public void setField(String field) {
			this.field = field;
			registerEvent(new MyClassUpdateEvent());
		}
	}

	@Repository
	interface MyClassRepository extends JpaRepository<MyClass, Integer> {
		Window<MyClass> findFirst1By(KeysetScrollPosition position);
	}

	static class MyClassUpdateEvent {
	}

	static class MyClassService {
		private final MyClassRepository myClassRepository;

		public MyClassService(MyClassRepository myClassRepository) {
			this.myClassRepository = myClassRepository;
		}

		public void update() {
			Window<MyClass> myClasses = myClassRepository.findFirst1By(ScrollPosition.keyset());
			for (MyClass myClass : myClasses) {
				myClass.setField("updated");
			}
			myClassRepository.saveAll(myClasses); // change myClasses to myClasses.toList() will pass
		}
	}

	static class EventHandler {
		@EventListener
		public void handleMyClassUpdateEvent(MyClassUpdateEvent event) {
		}
	}

}

@quaff
Copy link
Contributor

quaff commented Sep 13, 2023

I believe it's a bug of spring-data-commons, I'm going to fix it.

quaff referenced this issue in quaff/spring-data-commons Sep 13, 2023
repository.saveAll(Window<?>) will be handled properly after this commit.

Fixes https://github.com/spring-projects/spring-data-jpa/issues/3153
@jiaheng
Copy link
Author

jiaheng commented Sep 14, 2023

It can be minified to this:

package com.example.demo.service;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.event.EventListener;
import org.springframework.data.domain.AbstractAggregateRoot;
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Window;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.stereotype.Repository;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.then;

@EnableJpaRepositories(considerNestedRepositories = true)
@SpringBootTest
class DomainEventHandlerTest {

	@Autowired
	MyClassRepository myClassRepository;

	@Autowired
	MyClassService myClassService;

	@SpyBean
	EventHandler eventHandler;

	@Test
	void test() {
        myClassRepository.save(new MyClass());
		myClassService.update();
        then(eventHandler).should().handleMyClassUpdateEvent(any());
	}

	@TestConfiguration
	static class Config {
		@Bean
		MyClassService myClassService(MyClassRepository myClassRepository) {
			return new MyClassService(myClassRepository);
		}

		@Bean
		EventHandler eventHandler() {
			return new EventHandler();
		}
	}

	@Entity
	static class MyClass extends AbstractAggregateRoot<MyClass> {
		@Id
		@GeneratedValue
		private Integer id;

		private String field;

		public void setField(String field) {
			this.field = field;
			registerEvent(new MyClassUpdateEvent());
		}
	}

	@Repository
	interface MyClassRepository extends JpaRepository<MyClass, Integer> {
		Window<MyClass> findFirst1By(KeysetScrollPosition position);
	}

	static class MyClassUpdateEvent {
	}

	static class MyClassService {
		private final MyClassRepository myClassRepository;

		public MyClassService(MyClassRepository myClassRepository) {
			this.myClassRepository = myClassRepository;
		}

		public void update() {
			Window<MyClass> myClasses = myClassRepository.findFirst1By(ScrollPosition.keyset());
			for (MyClass myClass : myClasses) {
				myClass.setField("updated");
			}
			myClassRepository.saveAll(myClasses); // change myClasses to myClasses.toList() will pass
		}
	}

	static class EventHandler {
		@EventListener
		public void handleMyClassUpdateEvent(MyClassUpdateEvent event) {
		}
	}

}

Got it. Thanks for the suggestion.

@mp911de mp911de added the for: team-attention An issue we need to discuss as a team to make progress label Sep 14, 2023
quaff referenced this issue in quaff/spring-data-commons Sep 15, 2023
repository.saveAll(Window<?>) will be handled properly after this commit.

Fixes https://github.com/spring-projects/spring-data-jpa/issues/3153
quaff referenced this issue in quaff/spring-data-commons Sep 15, 2023
@mp911de mp911de self-assigned this Sep 18, 2023
@mp911de mp911de added type: enhancement A general enhancement and removed for: team-attention An issue we need to discuss as a team to make progress labels Sep 18, 2023
@mp911de mp911de assigned odrotbohm and unassigned mp911de Sep 18, 2023
@mp911de mp911de removed the status: waiting-for-triage An issue we've not yet triaged label Sep 18, 2023
@odrotbohm odrotbohm transferred this issue from spring-projects/spring-data-jpa Sep 20, 2023
@odrotbohm odrotbohm added type: bug A general bug and removed type: enhancement A general enhancement labels Sep 20, 2023
@odrotbohm odrotbohm added this to the 2.7.17 (2021.2.17) milestone Sep 20, 2023
@odrotbohm
Copy link
Member

This has been fixed for the 2.7, 3.0, 3.1 bugfix releases and the 3.2 RC1 coming. I've turned this into a bug as we already had test cases in place that described the feature as being expected to be available but ultimately didn't actually test it was.

This is now in place by handling calls to saveAll(…) explicitly and considering the Iterable handed to it as is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment