2013-06-15 16 views
24

Mam kwerendę aktualizującą:Dane wiosny Aktualizacja JPA @Query nie aktualizuje?

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

staram się go używać w teście integracji:

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

Ale pola nie są aktualizowane i zachowują swoje wartości początkowe, test braku zatem .

Próbowałem dodanie koloru tuż przed zapytania findOne:

adminRepository.flush(); 

Ale udało twierdzenie pozostały identyczne.

widzę aktualizacji instrukcji SQL w dzienniku:

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 

Ale dziennika nie wykazuje sql, które mogą odnosić się do wyszukiwarki:

Admin loadedAdmin = adminRepository.findOne(admin0.getId()); 
The finder sql statement is not making its way to the database. 

reguły jest on ignorowany przez jakiś buforowania powodu?

Gdybym wtedy dodać wywołanie dystansu findByEmail i findByLogin jak w:

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

potem widzę w dzienniku instrukcji SQL generowany:

Ale twierdzenie:

assertEquals("Toto", myadmin.getFirstname()); 

nadal kończy się niepowodzeniem, mimo że śledzenie pokazuje, że ten sam obiekt domeny został odzyskany:

TRACE [BasicExtractor] found [1037] as column [id14_] 

Jeszcze jedną rzeczą, która mnie zastanawia z tym innym wykrywaczem, jest to, że pokazuje klauzulę limitu 2, mimo że ma zwracać tylko jeden obiekt Admin.

Pomyślałem, że zawsze będzie limit 1 podczas zwracania jednego obiektu domeny. Czy to błędne założenie w Spring Data?

Podczas wklejania w kliencie MySQL, SQL wyświetlane w dzienniku konsoli logika działa dobrze:

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) 

Więc dlaczego nie jest to odzwierciedlenie na poziomie Java?

Odpowiedz

45

EntityManager nie spłukuje się automatycznie zmienia się domyślnie. należy użyć następującej opcji z wyciągu z zapytaniem:

@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

, jeśli nie chcesz stracić nieprzygotowanych zmian po wyraźnej automatycznej aktualizacji, przeczytaj następujące podejście: [Modyfikuj zapytanie o aktualizację - Odśwież kontekst utrwalania] (http://stackoverflow.com/questions/32258857/spring-boot-data- jpa-modyfikowanie-aktualizacja-zapytanie-odświeżanie-utrwalanie-kontekst) –

+0

Jesteś moim bohaterem! –

+0

Pięciogwiazdkowe rozwiązanie, dziękuję. –

5

Udało mi się to uruchomić. Opiszę tutaj moją aplikację i test integracji.

przykładowej aplikacji

Przykład aplikacja posiada dwie klasy i jednego interfejsu, które są odpowiednie do tego problemu:

  1. Konfiguracja kontekstu aplikacji class
  2. Jednostka
  3. Interfejs repozytorium

Te klasy i interfejs repozytorium są opisane poniżej.

Kod źródłowy klasy PersistenceContext wygląda następująco:

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

Załóżmy, że mamy prosty byt zwany Todo którego kod źródłowy wygląda następująco:

@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; 
} 

repozytorium interfejs ma pojedyncza metoda o nazwie updateTitle(), która aktualizuje tytuł wpisu todo. Kod źródłowy interfejsu TodoRepository wygląda następująco:

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

Sposób updateTitle() nie jest odnotowany z @Transactional adnotacji, ponieważ myślę, że najlepiej jest użyć warstwy usług jako granica transakcji.

Test Integracja

Test integracja wykorzystuje dbunit, Spring test i sprężyna prób DBUnit. Ma trzy składniki, które są istotne dla tego problemu:

  1. Zestaw danych DbUnit używany do zainicjowania bazy danych w znanym stanie przed wykonaniem testu.
  2. Zestaw danych DbUnit używany do sprawdzania, czy tytuł jednostki jest aktualizowany.
  3. Test integracji.

Elementy te zostały opisane bardziej szczegółowo poniżej.

Nazwa pliku zestawu danych DBUnit który jest używany do inicjacji bazy danych do znanego stanu jest toDoData.xml i jego zawartość wygląda następująco:

<dataset> 
    <todos id="1" description="Lorem ipsum" title="Foo" version="0"/> 
    <todos id="2" description="Lorem ipsum" title="Bar" version="0"/> 
</dataset> 

Nazwa zestawu danych, który jest używany DBUnit Aby sprawdzić, czy tytuł wpisu do todo jest aktualizowany, nazywa się toDoData-update.xml, a jego zawartość wygląda następująco (z jakiegoś powodu wersja wpisu do todo nie została zaktualizowana, ale tytuł był.):

<dataset> 
    <todos id="1" description="Lorem ipsum" title="FooBar" version="0"/> 
    <todos id="2" description="Lorem ipsum" title="Bar" version="0"/> 
</dataset> 

Kod źródłowy rzeczywistego testu integracyjnego wygląda następująco (Pamiętaj, aby opisywać metody badawczej z @Transactional adnotacji):

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

Po uruchomieniu testu integracji, test przechodzi i aktualizowany jest tytuł pozycji todo. Jedynym problemem, który mam, jest to, że pole wersji nie jest aktualizowane. Jakieś pomysły, dlaczego?

Nie podoba mi się, że ten opis jest nieco niejasny. Jeśli chcesz uzyskać więcej informacji na temat pisania testów integracyjnych dla repozytoriów Spring Data JPA, możesz przeczytać my blog post about it.

+0

Witam Petri, dziękuję za ten test pokazowy. Widzę jedną rzecz, która mogłaby go uzupełnić, to mieć pewne twierdzenie przed i po aktualizacji, aby upewnić się, że aktualizacja rzeczywiście ma miejsce. Na przykład assertEquals na wartość zaktualizowanego pola. – Stephane

+2

Po prostu nie działa JPA. Jeśli wywołasz zapytanie manipulacyjne, kontekst utrwalania pozostaje nietknięty. Możesz obejść to, ustawiając flagę 'clearAutomatically' na' @ Modifying' na wartość true. Spowoduje to wywołanie 'EntityManager.clear()' z wszystkimi jego konsekwencjami (np. Pozostałymi, oczekującymi zmianami na utratę innych elementów itp.). Jeśli ponownie poszukujesz elementu, powinieneś zobaczyć nowe wartości. –

+0

Witaj Oliver, dzięki za komentarz otwierający oko. – Stephane

6

W końcu zrozumiałem, co się dzieje.

Podczas tworzenia testu integracji dla instrukcji zapisywania obiektu, zaleca się opróżnienie menedżera encji, aby uniknąć fałszywego negatywu, czyli uniknięcia testu działającego poprawnie, ale którego działanie zakończy się niepowodzeniem po uruchomieniu w procesie produkcyjnym . Rzeczywiście, test może działać dobrze tylko dlatego, że pamięć podręczna pierwszego poziomu nie jest opróżniana i żadne zapisy nie trafiają do bazy danych. Aby uniknąć tego fałszywie ujemnego testu integracji, użyj wyraźnego koloru w teście. Zauważ, że kod produkcyjny nigdy nie powinien używać żadnego jawnego koloru, ponieważ to ORM decyduje o tym, kiedy spłukać.

Podczas tworzenia testu integracji na oświadczeniu aktualizacji może być konieczne wyczyszczenie menedżera encji, aby ponownie załadować pamięć podręczną pierwszego poziomu. W rzeczywistości instrukcja aktualizacji całkowicie pomija pamięć podręczną pierwszego poziomu i zapisuje ją bezpośrednio w bazie danych. Pamięć podręczna pierwszego poziomu nie jest zsynchronizowana i odzwierciedla starą wartość zaktualizowanego obiektu. Aby uniknąć tego nieaktualnego stanu obiektu, użyj wyraźnego wyczyszczenia w treści testu. Zauważ, że kod produkcyjny nie powinien nigdy wymagać użycia wyraźnego wyraźnego, ponieważ to rola ORM decyduje, kiedy usunąć.

Mój test działa dobrze.

-1

Można spróbować dodać wersjonowanym słowa kluczowego po słowie kluczowym UPDATE jak stwierdzono here. Tak więc poniżej powinno zadziałać:

@Modifying 
@Query("UPDATE VERSIONED Admin...) 
Powiązane problemy