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

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

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

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

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