2013-06-15 2 views
24

У меня есть запрос на обновление: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 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 как в:

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_] 

Еще одна вещь, которая озадачивает меня этим другом, заключается в том, что он показывает предложение предела 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?

ответ

45

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); 
+3

, если вы не хотите потерять не сбрасываемые изменения после обновления clearAutomatically, прочитайте этот подход: [Изменить запрос на обновление - Обновить контекст сохранения] (http://stackoverflow.com/questions/32258857/spring-boot-data- jpa-modifying-update-query-refresh-persistence-context) –

+0

Ты мой герой! –

+0

Пятизвездочное решение, спасибо. –

5

Я смог заставить это работать. Здесь я опишу свое приложение и интеграционный тест.

Пример приложения

В этом примере приложение имеет два класса и один интерфейс, которые имеют отношение к этой проблеме:

  1. Конфигурация контекста приложения Класс Класс
  2. Объект
  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 аннотацию, потому что я думаю, что лучше использовать слой службы в качестве границы транзакции.

Интеграция Test

Тест Интеграция использует DbUnit, Spring Test и Spring-Test-DBUnit. Он имеет три компонента, которые имеют отношение к этой проблеме:

  1. Набор данных DbUnit, который используется для инициализации базы данных в известном состоянии до выполнения теста.
  2. Набор данных DbUnit, который используется для проверки того, что заголовок объекта обновляется.
  3. Тестирование интеграции.

Эти компоненты описаны более подробно ниже.

Имя файла набора данных 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 аннотацию):

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, вы можете прочитать my blog post about it.

+0

Привет, Петри, благодарю вас за этот случай проверки интеграции. Я могу видеть одну вещь, которая может ее дополнить, - это иметь некоторое утверждение до и после обновления, чтобы убедиться, что обновление действительно имеет место. Например, assertEquals о значении обновленного поля. – Stephane

+2

Это не так, как работает JPA. Если вы вызываете манипулирующий запрос, контекст persistence остается нетронутым. Вы можете обойти это, установив флаг 'clearAutomatically' в' @ Modifying' в значение true. Это вызовет 'EntityManager.clear()' со всеми его последствиями (например, оставшиеся, ожидающие изменения для других утерянных объектов и т. Д.). Если вы снова просмотрите объект, вы увидите новые значения. –

+0

Привет, Оливер, спасибо за комментарий к открытию глаза. – Stephane

6

Я наконец понял, что происходит.

При создании теста интеграции для оператора, сохраняющего объект, рекомендуется отключить диспетчер объектов, чтобы избежать ложного отрицательного результата, т. Е. Избежать проверки работоспособности, но работа которого не удалась при запуске в производство , В самом деле, тест может работать нормально, потому что кеш первого уровня не сбрасывается, и никакая запись не попадает в базу данных. Чтобы избежать этого теста с ложной отрицательной интеграцией, используйте явный поток в тестовом теле. Обратите внимание, что производственный код никогда не должен использовать какой-либо явный флеш, поскольку это роль ORM, чтобы решить, когда нужно очистить.

При создании теста интеграции в операторе обновления может потребоваться очистить диспетчер сущности, чтобы перезагрузить кеш первого уровня. Действительно, оператор обновления полностью обходит кеш первого уровня и записывает непосредственно в базу данных. Кэш первого уровня затем не синхронизирован и отражает старое значение обновленного объекта. Чтобы избежать этого устаревшего состояния объекта, используйте явное ясное в тестовом теле. Обратите внимание, что производственный код никогда не должен использовать явное ясность, так как роль ORM определяет, когда нужно очистить.

Мой тест теперь работает отлично.

-1

Вы можете попробовать добавить версированное ключевое слово после UPDATE ключевого слова, как указано here. Таким образом, нижеследующее должно работать:

@Modifying 
@Query("UPDATE VERSIONED Admin...) 
Смежные вопросы