2013-06-15 20 views
24

devo una query di aggiornamento:Spring Data JPA Update @Query non si aggiorna?

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

Sto cercando di usarlo in un test di integrazione:

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

Ma i campi non vengono aggiornati e mantengono i loro valori iniziali, il test non riuscendo in tal modo .

Ho provato ad aggiungere un diritto a filo prima che la query findOne:

adminRepository.flush(); 

Ma l'affermazione non è riuscito rimasto identico.

posso vedere la dichiarazione di sql aggiornamento nel registro:

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 

Ma il registro non mostra alcun sql che potrebbero riguardare il cercatore:

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

E 'ignorato per qualche motivo la cache?

Se poi aggiungo un appello ai cercatori findByEmail e findByLogin come in:

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

allora posso vedere nel registro SQL generato:

Ma l'affermazione:

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

non riesce ancora anche se la traccia mostra che lo stesso oggetto dominio è stato recuperato:

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

Un'altra cosa che mi imbarazza con questo altro finder è che mostra una clausola limite 2 anche se si suppone che restituisca un solo oggetto Admin.

Ho pensato che ci sarebbe sempre stato un limite 1 quando si restituiva un oggetto dominio. È un'ipotesi errata su Spring Data?

Quando si incolla in un client di MySQL, le istruzioni SQL visualizzate nel registro della console, la logica funziona bene:

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) 

Allora, perché è questo non riflette a livello di Java?

risposta

45

EntityManager non esegue il flush delle modifiche automaticamente per impostazione predefinita. Si dovrebbe usare la seguente opzione con la sua dichiarazione di query:

@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

se non si desidera perdere le modifiche non sciacquate dopo un chiaro Aggiornamento automatico, leggere questo approccio: [Modifica query di aggiornamento - Aggiorna persistenza contesto] (http://stackoverflow.com/questions/32258857/spring-boot-data- jpa-modifying-update-query-refresh-persistence-context) –

+0

Sei il mio eroe! –

+0

Una soluzione a cinque stelle, grazie. –

5

Sono riuscito a farlo funzionare. Descriverò qui la mia domanda e il test di integrazione.

dell'esempio applicativo

L'applicazione esempio ha due classi e un'interfaccia che sono rilevanti per questo problema:

  1. La configurazione contesto applicativo classe classe
  2. L'entità
  3. L' interfaccia repository

Queste classi e l'interfaccia del repository sono descritte di seguito.

Il codice sorgente della classe PersistenceContext appare come segue:

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

Supponiamo che abbiamo un soggetto semplice chiamata Todo che il codice sorgente appare come segue:

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

La nostra interfaccia repository ha un singolo metodo chiamato updateTitle() che aggiorna il titolo di una voce di comando. Il codice sorgente dell'interfaccia TodoRepository appare come segue:

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

Il metodo updateTitle() non viene annotato con l'annotazione @Transactional perché penso che sia meglio usare un livello di servizio come un confine transazione.

L'integrazione di prova

il test di integrazione utilizza DBUnit, Test primavera e la primavera-Test-DBUnit. Dispone di tre componenti rilevanti per questo problema:

  1. Il set di dati DbUnit che viene utilizzato per inizializzare il database in uno stato noto prima dell'esecuzione del test.
  2. Il set di dati DbUnit utilizzato per verificare che il titolo dell'entità sia aggiornato.
  3. Test di integrazione.

Questi componenti sono descritti con ulteriori dettagli nel seguito.

Il nome del file di dati DBUnit che viene utilizzato per inizializzare il database di stato conosciuto è toDoData.xml e il suo contenuto appare come segue:

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

Il nome del set di dati che viene utilizzato DBUnit per verificare che il titolo della voce di todo sia aggiornato è chiamato toDoData-update.xml e il suo contenuto appare come segue (per qualche motivo la versione della voce di todo non è stata aggiornata ma il titolo era.):

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

Il codice sorgente del test di integrazione reale appare come segue (Ricordatevi di annotare il metodo di prova con la @Transactional annotazione):

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

Dopo aver eseguito il test di integrazione, il test viene superato e il titolo della voce di comando è aggiornato. L'unico problema che sto avendo è che il campo della versione non è aggiornato. Qualche idea, perché?

Capisco che questa descrizione è un po 'vaga. Se si desidera ottenere ulteriori informazioni sulla scrittura di test di integrazione per i repository JPA di Spring Data, è possibile leggere my blog post about it.

+0

Ciao Petri, grazie per la dimostrazione dell'integrazione. Posso vedere una cosa che potrebbe completarla, è avere qualche asserzione prima e dopo l'aggiornamento, per accertare che l'aggiornamento abbia effettivamente luogo. Ad esempio, un assertEquals sul valore del campo aggiornato. – Stephane

+2

Questo non è il modo in cui funziona JPA. Se si attiva una query di manipolazione, il contesto di persistenza rimane inalterato. Puoi aggirare questo impostando il flag 'clearAutomatically' su' @ Modifying' su true. Questo chiamerà 'EntityManager.clear()' con tutte le sue conseguenze (ad esempio rimanendo, in attesa di modifiche ad altre entità che vanno perse ecc.). Se si cerca di nuovo l'entità, si dovrebbero vedere i nuovi valori. –

+0

Ciao Oliver, grazie per questo commento di apertura. – Stephane

6

Ho finalmente capito cosa stava succedendo.

Quando si crea un test di integrazione su un'istruzione che salva un oggetto, si consiglia di svuotare il gestore entità in modo da evitare eventuali falsi negativi, vale a dire evitare un test eseguito correttamente ma la cui operazione non riuscirebbe quando viene eseguita nella produzione . In effetti, il test può funzionare bene semplicemente perché la cache di primo livello non viene scaricata e nessuna scrittura raggiunge il database. Per evitare questo test di integrazione falsi negativi, utilizzare un flush esplicito nel corpo del test. Si noti che il codice di produzione non dovrebbe mai essere necessario utilizzare alcun flush esplicito in quanto è il ruolo dell'ORM per decidere quando eseguire il flush.

Quando si crea un test di integrazione su un'istruzione di aggiornamento, potrebbe essere necessario cancellare il gestore di entità in modo da ricaricare la cache di primo livello. In effetti, una dichiarazione di aggiornamento ignora completamente la cache di primo livello e scrive direttamente nel database. La cache di primo livello non è sincronizzata e riflette il vecchio valore dell'oggetto aggiornato. Per evitare questo stato obsoleto dell'oggetto, utilizzare una chiara esplicita nel corpo del test. Si noti che il codice di produzione non dovrebbe mai aver bisogno di usare alcun esplicito chiaro in quanto è il ruolo dell'ORM per decidere quando cancellare.

Il mio test ora funziona correttamente.

-1

Si può provare ad aggiungere di versione parola chiave dopo la parola chiave UPDATE come indicato here. Quindi il seguente dovrebbe funzionare:

@Modifying 
@Query("UPDATE VERSIONED Admin...) 
Problemi correlati