diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/JpaEntityListenerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/JpaEntityListenerTests.java new file mode 100644 index 0000000000..b94e517b19 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/JpaEntityListenerTests.java @@ -0,0 +1,198 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.junit.jupiter.orm; + +import java.util.List; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.PersistenceContext; +import javax.sql.DataSource; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.Database; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.test.context.junit.jupiter.orm.domain.JpaPersonRepository; +import org.springframework.test.context.junit.jupiter.orm.domain.Person; +import org.springframework.test.context.junit.jupiter.orm.domain.PersonListener; +import org.springframework.test.context.junit.jupiter.orm.domain.PersonRepository; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Transactional tests for JPA entity listener support (a.k.a. lifecycle callback + * methods). + * + * @author Sam Brannen + * @since 5.3.18 + * @see issue gh-28228 + * @see org.springframework.test.context.junit4.orm.HibernateSessionFlushingTests + */ +@SpringJUnitConfig +@Transactional +@Sql(statements = "insert into person(id, name) values(0, 'Jane')") +class JpaEntityListenerTests { + + @PersistenceContext + EntityManager entityManager; + + @Autowired + JdbcTemplate jdbcTemplate; + + @Autowired + PersonRepository repo; + + + @BeforeEach + void setUp() { + assertPeople("Jane"); + PersonListener.methodsInvoked.clear(); + } + + @Test + void find() { + Person jane = repo.findByName("Jane"); + assertCallbacks("@PostLoad: Jane"); + + // Does not cause an additional @PostLoad + repo.findById(jane.getId()); + assertCallbacks("@PostLoad: Jane"); + + // Clear to cause a new @PostLoad + entityManager.clear(); + repo.findById(jane.getId()); + assertCallbacks("@PostLoad: Jane", "@PostLoad: Jane"); + } + + @Test + void save() { + Person john = repo.save(new Person("John")); + assertCallbacks("@PrePersist: John"); + + // Flush to cause a @PostPersist + entityManager.flush(); + assertPeople("Jane", "John"); + assertCallbacks("@PrePersist: John", "@PostPersist: John"); + + // Does not cause a @PostLoad + repo.findById(john.getId()); + assertCallbacks("@PrePersist: John", "@PostPersist: John"); + + // Clear to cause a @PostLoad + entityManager.clear(); + repo.findById(john.getId()); + assertCallbacks("@PrePersist: John", "@PostPersist: John", "@PostLoad: John"); + } + + @Test + void update() { + Person jane = repo.findByName("Jane"); + assertCallbacks("@PostLoad: Jane"); + + jane.setName("Jane Doe"); + // Does not cause a @PreUpdate or @PostUpdate + repo.save(jane); + assertCallbacks("@PostLoad: Jane"); + + // Flush to cause a @PreUpdate and @PostUpdate + entityManager.flush(); + assertPeople("Jane Doe"); + assertCallbacks("@PostLoad: Jane", "@PreUpdate: Jane Doe", "@PostUpdate: Jane Doe"); + } + + @Test + void remove() { + Person jane = repo.findByName("Jane"); + assertCallbacks("@PostLoad: Jane"); + + // Does not cause a @PostRemove + repo.remove(jane); + assertCallbacks("@PostLoad: Jane", "@PreRemove: Jane"); + + // Flush to cause a @PostRemove + entityManager.flush(); + assertPeople(); + assertCallbacks("@PostLoad: Jane", "@PreRemove: Jane", "@PostRemove: Jane"); + } + + private void assertCallbacks(String... callbacks) { + assertThat(PersonListener.methodsInvoked).containsExactly(callbacks); + } + + private void assertPeople(String... expectedNames) { + List names = this.jdbcTemplate.queryForList("select name from person", String.class); + if (expectedNames.length == 0) { + assertThat(names).isEmpty(); + } + else { + assertThat(names).containsExactlyInAnyOrder(expectedNames); + } + } + + + @Configuration(proxyBeanMethods = false) + @EnableTransactionManagement + static class Config { + + @Bean + PersonRepository personRepository() { + return new JpaPersonRepository(); + } + + @Bean + DataSource dataSource() { + return new EmbeddedDatabaseBuilder().generateUniqueName(true).build(); + } + + @Bean + JdbcTemplate jdbcTemplate(DataSource dataSource) { + return new JdbcTemplate(dataSource); + } + + @Bean + LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) { + LocalContainerEntityManagerFactoryBean emfb = new LocalContainerEntityManagerFactoryBean(); + emfb.setDataSource(dataSource); + emfb.setPackagesToScan(Person.class.getPackage().getName()); + HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter(); + hibernateJpaVendorAdapter.setGenerateDdl(true); + hibernateJpaVendorAdapter.setDatabase(Database.HSQL); + emfb.setJpaVendorAdapter(hibernateJpaVendorAdapter); + return emfb; + } + + @Bean + JpaTransactionManager transactionManager(EntityManagerFactory emf) { + return new JpaTransactionManager(emf); + } + + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/JpaPersonRepository.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/JpaPersonRepository.java new file mode 100644 index 0000000000..a3984b5359 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/JpaPersonRepository.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.junit.jupiter.orm.domain; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +/** + * JPA based implementation of the {@link PersonRepository} API. + * + * @author Sam Brannen + * @since 5.3.18 + */ +@Transactional +@Repository +public class JpaPersonRepository implements PersonRepository { + + @PersistenceContext + private EntityManager entityManager; + + @Override + public Person findById(Long id) { + return this.entityManager.find(Person.class, id); + } + + @Override + public Person findByName(String name) { + return this.entityManager.createQuery("from Person where name = :name", Person.class) + .setParameter("name", name) + .getSingleResult(); + } + + @Override + public Person save(Person person) { + this.entityManager.persist(person); + return person; + } + + @Override + public void remove(Person person) { + this.entityManager.remove(person); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/Person.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/Person.java new file mode 100644 index 0000000000..8d8f5b22fb --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/Person.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.junit.jupiter.orm.domain; + +import javax.persistence.Entity; +import javax.persistence.EntityListeners; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +/** + * Person entity. + * + * @author Sam Brannen + * @since 5.3.18 + */ +@Entity +@EntityListeners(PersonListener.class) +public class Person { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + private String name; + + + public Person() { + } + + public Person(String name) { + this.name = name; + } + + public Long getId() { + return this.id; + } + + protected void setId(Long id) { + this.id = id; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/PersonListener.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/PersonListener.java new file mode 100644 index 0000000000..1b5c56ddf6 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/PersonListener.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.junit.jupiter.orm.domain; + +import java.util.ArrayList; +import java.util.List; + +import javax.persistence.PostLoad; +import javax.persistence.PostPersist; +import javax.persistence.PostRemove; +import javax.persistence.PostUpdate; +import javax.persistence.PrePersist; +import javax.persistence.PreRemove; +import javax.persistence.PreUpdate; + +/** + * Person entity listener. + * + * @author Sam Brannen + * @since 5.3.18 + */ +public class PersonListener { + + public static final List methodsInvoked = new ArrayList<>(); + + + @PostLoad + public void postLoad(Person person) { + methodsInvoked.add("@PostLoad: " + person.getName()); + } + + @PrePersist + public void prePersist(Person person) { + methodsInvoked.add("@PrePersist: " + person.getName()); + } + + @PostPersist + public void postPersist(Person person) { + methodsInvoked.add("@PostPersist: " + person.getName()); + } + + @PreUpdate + public void preUpdate(Person person) { + methodsInvoked.add("@PreUpdate: " + person.getName()); + } + + @PostUpdate + public void postUpdate(Person person) { + methodsInvoked.add("@PostUpdate: " + person.getName()); + } + + @PreRemove + public void preRemove(Person person) { + methodsInvoked.add("@PreRemove: " + person.getName()); + } + + @PostRemove + public void postRemove(Person person) { + methodsInvoked.add("@PostRemove: " + person.getName()); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/PersonRepository.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/PersonRepository.java new file mode 100644 index 0000000000..9576a649d0 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/PersonRepository.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.junit.jupiter.orm.domain; + +/** + * Person repository API. + * + * @author Sam Brannen + * @since 5.3.18 + */ +public interface PersonRepository { + + Person findById(Long id); + + Person findByName(String name); + + Person save(Person person); + + void remove(Person person); + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/HibernateSessionFlushingTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/orm/HibernateSessionFlushingTests.java index 6c0dc92e90..e959681cc3 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/HibernateSessionFlushingTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/orm/HibernateSessionFlushingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,6 +44,7 @@ import static org.springframework.test.transaction.TransactionAssert.assertThatT * @author Juergen Hoeller * @author Vlad Mihalcea * @since 3.0 + * @see org.springframework.test.context.junit.jupiter.orm.JpaEntityListenerTests */ @ContextConfiguration public class HibernateSessionFlushingTests extends AbstractTransactionalJUnit4SpringContextTests {