Solución:
EntityManager no elimina los cambios automáticamente de forma predeterminada. Debe utilizar la siguiente opción con su declaración de consulta:
@Modifying(clearAutomatically = true)
@Query("update RssFeedEntry feedEntry set feedEntry.read =:isRead where feedEntry.id =:entryId")
void markEntryAsRead(@Param("entryId") Long rssFeedEntryId, @Param("isRead") boolean isRead);
Finalmente entendí lo que estaba pasando.
Al crear una prueba de integración en una declaración que guarda un objeto, se recomienda vaciar el administrador de la entidad para evitar cualquier false negativo, es decir, para evitar una prueba que funcione bien pero cuya operación fallaría cuando se ejecuta en producción. De hecho, la prueba puede funcionar bien simplemente porque la caché de primer nivel no se vacía y no hay escritura en la base de datos. Para evitar esto false prueba de integración negativa utilice un lavado explícito en el cuerpo de prueba. Tenga en cuenta que el código de producción nunca debería necesitar utilizar ningún vaciado explícito, ya que es función del ORM decidir cuándo vaciar.
Al crear una prueba de integración en una declaración de actualización, puede ser necesario borrar el administrador de la entidad para volver a cargar la caché de primer nivel. De hecho, una declaración de actualización omite por completo el caché de primer nivel y escribe directamente en la base de datos. La caché de primer nivel no está sincronizada y refleja el valor anterior del objeto actualizado. Para evitar este estado obsoleto del objeto, utilice un claro explícito en el cuerpo de prueba. Tenga en cuenta que el código de producción nunca debería necesitar utilizar ningún claro explícito, ya que es el papel del ORM decidir cuándo borrar.
Mi prueba ahora funciona bien.
Pude hacer que esto funcionara. Describiré mi aplicación y la prueba de integración aquí.
La aplicación de ejemplo
La aplicación de ejemplo tiene dos clases y una interfaz que son relevantes para este problema:
- La clase de configuración del contexto de la aplicación
- La clase de entidad
- La interfaz del repositorio
Estas clases y la interfaz del repositorio se describen a continuación.
El código fuente del PersistenceContext
la clase se ve de la siguiente manera:
import com.jolbox.bonecp.BoneCPDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
import java.util.Properties;
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = "net.petrikainulainen.spring.datajpa.todo.repository")
@PropertySource("classpath:application.properties")
public class PersistenceContext
protected static final String PROPERTY_NAME_DATABASE_DRIVER = "db.driver";
protected static final String PROPERTY_NAME_DATABASE_PASSWORD = "db.password";
protected static final String PROPERTY_NAME_DATABASE_URL = "db.url";
protected static final String PROPERTY_NAME_DATABASE_USERNAME = "db.username";
private static final String PROPERTY_NAME_HIBERNATE_DIALECT = "hibernate.dialect";
private static final String PROPERTY_NAME_HIBERNATE_FORMAT_SQL = "hibernate.format_sql";
private static final String PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO = "hibernate.hbm2ddl.auto";
private static final String PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY = "hibernate.ejb.naming_strategy";
private static final String PROPERTY_NAME_HIBERNATE_SHOW_SQL = "hibernate.show_sql";
private static final String PROPERTY_PACKAGES_TO_SCAN = "net.petrikainulainen.spring.datajpa.todo.model";
@Autowired
private Environment environment;
@Bean
public DataSource dataSource()
BoneCPDataSource dataSource = new BoneCPDataSource();
dataSource.setDriverClass(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_DRIVER));
dataSource.setJdbcUrl(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_URL));
dataSource.setUsername(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_USERNAME));
dataSource.setPassword(environment.getRequiredProperty(PROPERTY_NAME_DATABASE_PASSWORD));
return dataSource;
@Bean
public JpaTransactionManager transactionManager()
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactory().getObject());
return transactionManager;
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory()
LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
entityManagerFactoryBean.setDataSource(dataSource());
entityManagerFactoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
entityManagerFactoryBean.setPackagesToScan(PROPERTY_PACKAGES_TO_SCAN);
Properties jpaProperties = new Properties();
jpaProperties.put(PROPERTY_NAME_HIBERNATE_DIALECT, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_DIALECT));
jpaProperties.put(PROPERTY_NAME_HIBERNATE_FORMAT_SQL, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_FORMAT_SQL));
jpaProperties.put(PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO));
jpaProperties.put(PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY));
jpaProperties.put(PROPERTY_NAME_HIBERNATE_SHOW_SQL, environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_SHOW_SQL));
entityManagerFactoryBean.setJpaProperties(jpaProperties);
return entityManagerFactoryBean;
Supongamos que tenemos una entidad simple llamada Todo
cuyo código fuente se ve de la siguiente manera:
@Entity
@Table(name="todos")
public class Todo
public static final int MAX_LENGTH_DESCRIPTION = 500;
public static final int MAX_LENGTH_TITLE = 100;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(name = "description", nullable = true, length = MAX_LENGTH_DESCRIPTION)
private String description;
@Column(name = "title", nullable = false, length = MAX_LENGTH_TITLE)
private String title;
@Version
private long version;
Nuestra interfaz de repositorio tiene un solo método llamado updateTitle()
que actualiza el título de una entrada de tareas pendientes. El código fuente del TodoRepository
La interfaz tiene el siguiente aspecto:
import net.petrikainulainen.spring.datajpa.todo.model.Todo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface TodoRepository extends JpaRepository
@Modifying
@Query("Update Todo t SET t.title=:title WHERE t.id=:id")
public void updateTitle(@Param("id") Long id, @Param("title") String title);
los updateTitle()
El método no está anotado con el @Transactional
anotación porque creo que es mejor usar una capa de servicio como límite de transacción.
La prueba de integración
La prueba de integración utiliza DbUnit, Spring Test y Spring-Test-DBUnit. Tiene tres componentes que son relevantes para este problema:
- El conjunto de datos DbUnit que se utiliza para inicializar la base de datos en un estado conocido antes de que se ejecute la prueba.
- El conjunto de datos DbUnit que se utiliza para verificar que el título de la entidad está actualizado.
- La prueba de integración.
Estos componentes se describen con más detalles a continuación.
El nombre del archivo del conjunto de datos DbUnit que se utiliza para inicializar la base de datos a un estado conocido es toDoData.xml y su contenido tiene el siguiente aspecto:
El nombre del conjunto de datos DbUnit que se utiliza para verificar que el título de la entrada de tareas pendientes se actualiza se llama toDoData-update.xml y su contenido tiene el siguiente aspecto (por alguna razón, la versión de la entrada de tareas pendientes no se actualizó, pero el título sí. ¿Alguna idea de por qué?):
El código fuente de la prueba de integración real tiene el siguiente aspecto (recuerde anotar el método de prueba con la @Transactional
anotación):
import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.github.springtestdbunit.TransactionDbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DatabaseSetup;
import com.github.springtestdbunit.annotation.ExpectedDatabase;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
import org.springframework.transaction.annotation.Transactional;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = PersistenceContext.class)
@TestExecutionListeners( DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class,
TransactionalTestExecutionListener.class,
DbUnitTestExecutionListener.class )
@DatabaseSetup("todoData.xml")
public class ITTodoRepositoryTest
@Autowired
private TodoRepository repository;
@Test
@Transactional
@ExpectedDatabase("toDoData-update.xml")
public void updateTitle_ShouldUpdateTitle()
repository.updateTitle(1L, "FooBar");
Después de ejecutar la prueba de integración, la prueba pasa y se actualiza el título de la entrada de tareas pendientes. El único problema que tengo es que el campo de versión no está actualizado. ¿Alguna idea de por qué?
Entiendo que esta descripción es un poco vaga. Si desea obtener más información sobre cómo escribir pruebas de integración para los repositorios Spring Data JPA, puede leer la publicación de mi blog al respecto.
Si estás de acuerdo, eres capaz de dejar una reseña acerca de qué te ha parecido este post.