Spring Data JPA Update @Query не обновляется?
У меня есть запрос на обновление:
@Modifying
@Transactional
@Query("UPDATE Admin SET firstname = :firstname, lastname = :lastname, login = :login, superAdmin = :superAdmin, preferenceAdmin = :preferenceAdmin, address = :address, zipCode = :zipCode, city = :city, country = :country, email = :email, profile = :profile, postLoginUrl = :postLoginUrl WHERE id = :id")
public void update(@Param("firstname") String firstname, @Param("lastname") String lastname, @Param("login") String login, @Param("superAdmin") boolean superAdmin, @Param("preferenceAdmin") boolean preferenceAdmin, @Param("address") String address, @Param("zipCode") String zipCode, @Param("city") String city, @Param("country") String country, @Param("email") String email, @Param("profile") String profile, @Param("postLoginUrl") String postLoginUrl, @Param("id") Long id);
Я пытаюсь использовать его в тесте интеграции:
adminRepository.update("Toto", "LeHeros", admin0.getLogin(), admin0.getSuperAdmin(), admin0.getPreferenceAdmin(), admin0.getAddress(), admin0.getZipCode(), admin0.getCity(), admin0.getCountry(), admin0.getEmail(), admin0.getProfile(), admin0.getPostLoginUrl(), admin0.getId());
Admin loadedAdmin = adminRepository.findOne(admin0.getId());
assertEquals("Toto", loadedAdmin.getFirstname());
assertEquals("LeHeros", loadedAdmin.getLastname());
Но поля не обновляются и сохраняют свои начальные значения, поэтому тест не работает.
Я попытался добавить флеш прямо перед запросом findOne:
adminRepository.flush();
Но неудавшееся утверждение оставалось идентичным.
Я могу увидеть инструкцию sql update в журнале:
update admin set firstname='Toto', lastname='LeHeros', login='stephane', super_admin=0, preference_admin=0,
address=NULL, zip_code=NULL, city=NULL, country=NULL, email='[email protected]', profile=NULL,
post_login_url=NULL where id=2839
Но в журнале нет никакого sql, который мог бы относиться к поиску:
Admin loadedAdmin = adminRepository.findOne(admin0.getId());
The finder sql statement is not making its way to the database.
Не игнорируется ли она по причине кэширования?
Если я затем добавлю вызов findByEmail и findByLogin finders, как в:
adminRepository.update("Toto", "LeHeros", "qwerty", admin0.getSuperAdmin(), admin0.getPreferenceAdmin(), admin0.getAddress(), admin0.getZipCode(), admin0.getCity(), admin0.getCountry(), admin0.getEmail(), admin0.getProfile(), admin0.getPostLoginUrl(), admin0.getId());
Admin loadedAdmin = adminRepository.findOne(admin0.getId());
Admin myadmin = adminRepository.findByEmail(admin0.getEmail());
Admin anadmin = adminRepository.findByLogin("qwerty");
assertEquals("Toto", anadmin.getFirstname());
assertEquals("Toto", myadmin.getFirstname());
assertEquals("Toto", loadedAdmin.getFirstname());
assertEquals("LeHeros", loadedAdmin.getLastname());
то я могу видеть в журнале создаваемый оператор sql:
Но утверждение:
assertEquals("Toto", myadmin.getFirstname());
все еще не удается, хотя на трассе отображается тот же объект домена, который был получен:
TRACE [BasicExtractor] found [1037] as column [id14_]
Еще одна вещь, которая озадачивает меня этим другим искателем, заключается в том, что он показывает предложение limit 2, хотя предполагается, что он должен вернуть только один объект Admin.
Я думал, что при возврате одного объекта домена всегда будет предел 1. Это неверное предположение о данных Spring?
При вставке в MySQL-клиент, операторы sql, отображаемые в журнале консоли, логика отлично работает:
mysql> insert into admin (version, address, city, country, email, firstname, lastname, login, password,
-> password_salt, post_login_url, preference_admin, profile, super_admin, zip_code) values (0,
-> NULL, NULL, NULL, '[email protected]', 'zfirstname039', 'zlastname039', 'zlogin039',
-> 'zpassword039', '', NULL, 0, NULL, 1, NULL);
Query OK, 1 row affected (0.07 sec)
mysql> select * from admin;
+------+---------+---------------+--------------+-----------+--------------+---------------+-------------+------------------+---------+----------+------+---------+-------------------------+---------+----------------+
| id | version | firstname | lastname | login | password | password_salt | super_admin | preference_admin | address | zip_code | city | country | email | profile | post_login_url |
+------+---------+---------------+--------------+-----------+--------------+---------------+-------------+------------------+---------+----------+------+---------+-------------------------+---------+----------------+
| 1807 | 0 | zfirstname039 | zlastname039 | zlogin039 | zpassword039 | | 1 | 0 | NULL | NULL | NULL | NULL | [email protected] | NULL | NULL |
+------+---------+---------------+--------------+-----------+--------------+---------------+-------------+------------------+---------+----------+------+---------+-------------------------+---------+----------------+
1 row in set (0.00 sec)
mysql> update admin set firstname='Toto', lastname='LeHeros', login='qwerty', super_admin=0, preference_admin=0, address=NULL, zip_code=NULL, city=NULL, country=NULL, email='[email protected]', profile=NULL, post_login_url=NULL where id=1807;
Query OK, 1 row affected (0.07 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from admin; +------+---------+-----------+----------+--------+--------------+---------------+-------------+------------------+---------+----------+------+---------+------------------------+---------+----------------+
| id | version | firstname | lastname | login | password | password_salt | super_admin | preference_admin | address | zip_code | city | country | email | profile | post_login_url |
+------+---------+-----------+----------+--------+--------------+---------------+-------------+------------------+---------+----------+------+---------+------------------------+---------+----------------+
| 1807 | 0 | Toto | LeHeros | qwerty | zpassword039 | | 0 | 0 | NULL | NULL | NULL | NULL | [email protected] | NULL | NULL |
+------+---------+-----------+----------+--------+--------------+---------------+-------------+------------------+---------+----------+------+---------+------------------------+---------+----------------+
1 row in set (0.00 sec)
mysql> select admin0_.id as id14_, admin0_.version as version14_, admin0_.address as address14_, admin0_.city as city14_, admin0_.country as country14_, admin0_.email as email14_, admin0_.firstname as firstname14_, admin0_.lastname as lastname14_, admin0_.login as login14_, admin0_.password as password14_, admin0_.password_salt as password11_14_, admin0_.post_login_url as post12_14_, admin0_.preference_admin as preference13_14_, admin0_.profile as profile14_, admin0_.super_admin as super15_14_, admin0_.zip_code as zip16_14_ from admin admin0_ where admin0_.email='[email protected]' limit 2;
+-------+------------+------------+---------+------------+------------------------+--------------+-------------+----------+--------------+----------------+------------+------------------+------------+-------------+-----------+
| id14_ | version14_ | address14_ | city14_ | country14_ | email14_ | firstname14_ | lastname14_ | login14_ | password14_ | password11_14_ | post12_14_ | preference13_14_ | profile14_ | super15_14_ | zip16_14_ |
+-------+------------+------------+---------+------------+------------------------+--------------+-------------+----------+--------------+----------------+------------+------------------+------------+-------------+-----------+
| 1807 | 0 | NULL | NULL | NULL | [email protected] | Toto | LeHeros | qwerty | zpassword039 | | NULL | 0 | NULL | 0 | NULL |
+-------+------------+------------+---------+------------+------------------------+--------------+-------------+----------+--------------+----------------+------------+------------------+------------+-------------+-----------+
1 row in set (0.00 sec)
mysql> select admin0_.id as id14_, admin0_.version as version14_, admin0_.address as address14_, admin0_.city as city14_, admin0_.country as country14_, admin0_.email as email14_, admin0_.firstname as firstname14_, admin0_.lastname as lastname14_, admin0_.login as login14_, admin0_.password as password14_, admin0_.password_salt as password11_14_, admin0_.post_login_url as post12_14_, admin0_.preference_admin as preference13_14_, admin0_.profile as profile14_, admin0_.super_admin as super15_14_, admin0_.zip_code as zip16_14_ from admin admin0_ where admin0_.login='qwerty' limit 2;
+-------+------------+------------+---------+------------+------------------------+--------------+-------------+----------+--------------+----------------+------------+------------------+------------+-------------+-----------+
| id14_ | version14_ | address14_ | city14_ | country14_ | email14_ | firstname14_ | lastname14_ | login14_ | password14_ | password11_14_ | post12_14_ | preference13_14_ | profile14_ | super15_14_ | zip16_14_ |
+-------+------------+------------+---------+------------+------------------------+--------------+-------------+----------+--------------+----------------+------------+------------------+------------+-------------+-----------+
| 1807 | 0 | NULL | NULL | NULL | [email protected] | Toto | LeHeros | qwerty | zpassword039 | | NULL | 0 | NULL | 0 | NULL |
+-------+------------+------------+---------+------------+------------------------+--------------+-------------+----------+--------------+----------------+------------+------------------+------------+-------------+-----------+
1 row in set (0.00 sec)
Так почему же это не отражается на уровне Java?
Ответы
Ответ 1
По умолчанию EntityManager не выполняет сброс автоматически. Вы должны использовать следующий параметр в своем заявлении запроса:
@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);
Ответ 2
Наконец я понял, что происходит.
При создании теста интеграции для оператора, сохраняющего объект, рекомендуется скрывать диспетчер объектов, чтобы избежать ложного отрицательного результата, т.е. Избежать проверки работоспособности, но работа которого не удалась при запуске в процессе производства. В самом деле, тест может работать нормально, потому что кеш первого уровня не сбрасывается, и никакая запись не попадает в базу данных. Чтобы избежать этого теста с ложной отрицательной интеграцией, используйте явный поток в тестовом теле. Обратите внимание, что производственный код никогда не должен использовать какой-либо явный флеш, поскольку это роль ORM, чтобы решить, когда нужно очистить.
При создании теста интеграции в операторе обновления может потребоваться очистить диспетчер объектов, чтобы перезагрузить кеш первого уровня. Действительно, оператор обновления полностью обходит кеш первого уровня и записывает непосредственно в базу данных. Кэш первого уровня затем не синхронизирован и отражает старое значение обновленного объекта. Чтобы избежать этого устаревшего состояния объекта, используйте явное ясное в тестовом теле. Обратите внимание, что производственный код никогда не должен использовать явное ясность, так как роль ORM определяет, когда нужно очистить.
Мой тест теперь работает отлично.
Ответ 3
Я смог заставить это работать. Здесь я опишу свое приложение и интеграционный тест.
Пример приложения
Пример приложения имеет два класса и один интерфейс, которые имеют отношение к этой проблеме:
- Класс конфигурации контекста приложения
- Класс сущности
- Интерфейс репозитория
Эти классы и интерфейс репозитория описаны ниже.
Исходный код класса PersistenceContext
выглядит следующим образом:
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;
}
}
Предположим, что у нас есть простой объект под названием Todo
исходный код которого выглядит следующим образом:
@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;
}
В нашем интерфейсе репозитория есть один метод под названием updateTitle()
который обновляет заголовок записи todo. Исходный код интерфейса TodoRepository
выглядит следующим образом:
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<Todo, Long> {
@Modifying
@Query("Update Todo t SET t.title=:title WHERE t.id=:id")
public void updateTitle(@Param("id") Long id, @Param("title") String title);
}
Метод updateTitle()
не аннотируется аннотацией @Transactional
потому что я считаю, что лучше использовать уровень сервиса как границу транзакции.
Интеграционный тест
Тест интеграции использует DbUnit, Spring Test и Spring-Test-DBUnit. Он имеет три компонента, которые имеют отношение к этой проблеме:
- Набор данных DbUnit, который используется для инициализации базы данных в известном состоянии перед выполнением теста.
- Набор данных DbUnit, который используется для проверки того, что заголовок объекта обновляется.
- Интеграционный тест.
Эти компоненты описаны более подробно ниже.
Имя файла набора данных DbUnit, который используется для инициализации базы данных в известном состоянии, является toDoData.xml, и его содержимое выглядит следующим образом:
<dataset>
<todos id="1" description="Lorem ipsum" title="Foo" version="0"/>
<todos id="2" description="Lorem ipsum" title="Bar" version="0"/>
</dataset>
Имя набора данных DbUnit, которое используется для проверки того, что заголовок записи todo обновляется, называется toDoData-update.xml, и его содержимое выглядит следующим образом (по какой-то причине версия записи todo не была обновлена, но заголовок был Любые идеи, почему?):
<dataset>
<todos id="1" description="Lorem ipsum" title="FooBar" version="0"/>
<todos id="2" description="Lorem ipsum" title="Bar" version="0"/>
</dataset>
Исходный код фактического теста интеграции выглядит следующим образом (помните, чтобы аннотировать метод проверки с @Transactional
аннотации @Transactional
):
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");
}
}
После запуска теста интеграции тест проходит, и заголовок записи todo обновляется. Единственная проблема, с которой я сталкиваюсь, заключается в том, что поле версии не обновляется. Любые идеи почему?
Я понял, что это описание немного расплывчато. Если вы хотите получить дополнительную информацию о написании интеграционных тестов для репозиториев Spring Data JPA, вы можете прочитать об этом в блоге.
Ответ 4
Я боролся с той же проблемой, когда пытался выполнить запрос на обновление, такой же, как вы did-
@Modifying
@Transactional
@Query(value = "UPDATE SAMPLE_TABLE st SET st.status=:flag WHERE se.referenceNo in :ids")
public int updateStatus(@Param("flag")String flag, @Param("ids")List<String> references);
Это будет работать, если вы поместили аннотацию @EnableTransactionManagement
в основной класс. Spring 3.1 представляет аннотацию @EnableTransactionManagement
которая будет использоваться в классах @Configuration
и обеспечивает поддержку транзакций.