Spring Data JPA Обновление @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='stephane@thalasoft.com', 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_]

Еще одна вещь, которая озадачивает меня с этим другим искателем, состоит в том, что он показывает предложение limit 2, даже если предполагается, что он возвращает только один объект Admin.

Я думал, что всегда будет предел 1 при возврате одного объекта домена. Это неверное предположение о Spring Data?

При вставке в клиент 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, 'zemail@thalasoft.com039', '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 | zemail@thalasoft.com039 | 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='stephane@thalasoft.com', 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 | stephane@thalasoft.com | 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='stephane@thalasoft.com' 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 | stephane@thalasoft.com | 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 | stephane@thalasoft.com | Toto | LeHeros | qwerty | zpassword039 | | NULL | 0 | NULL | 0 | NULL | 
+-------+------------+------------+---------+------------+------------------------+--------------+-------------+----------+--------------+----------------+------------+------------------+------------+-------------+-----------+
1 row in set (0.00 sec)

Так почему же это не отражено на уровне Java?

3 ответа

Решение

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

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

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

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

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

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

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

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

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

Интеграционный тест

Интеграционный тест использует 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, вы можете прочитать об этом в моем блоге.

Основная проблема здесь - кеш первого уровня JPA. Из раздела 3.1 спецификации JPA версии 2.2. подчеркнуть мое:

Экземпляр EntityManager связан с контекстом сохранения. Контекст персистентности - это набор экземпляров сущности, в котором для любого постоянного идентификатора сущности существует уникальный экземпляр сущности.

Это важно, потому что JPA отслеживает изменения этого объекта, чтобы сбрасывать их в базу данных. В качестве побочного эффекта это также означает, что в рамках одного контекста постоянства объект загружается только один раз. Поэтому перезагрузка измененного объекта не имеет никакого эффекта.

У вас есть несколько вариантов, как с этим справиться:

  1. Выселить сущность из EntityManager. Это можно сделать, позвонив EntityManager.detach, аннотируя метод обновления с помощью @Modifying(clearAutomatically = true)который изгоняет все сущности. Убедитесь, что изменения в этих объектах сначала сбрасываются, иначе вы можете потерять изменения.

  2. Использовать EntityManager.refresh().

  3. Используйте другой контекст персистентности для загрузки объекта. Самый простой способ сделать это - сделать это в отдельной транзакции. В Spring это можно сделать, используя отдельные методы, помеченные@Transactional на beans, вызванном из bean-объекта, не аннотированного @Transactional. Другой способ - использовать TransactionTemplate что особенно хорошо работает в тестах, где границы транзакций становятся очень заметными.

Я боролся с той же проблемой, когда пытался выполнить запрос на обновление, такой же, как вы

@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 классы и включить поддержку транзакций.

Другие вопросы по тегам