it-swarm.com.de

Spring Data JPA Update @Query nicht aktualisiert?

Ich habe eine Aktualisierungsanfrage:

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

Ich versuche es in einem Integrationstest zu verwenden:

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

Die Felder werden jedoch nicht aktualisiert und behalten ihre Anfangswerte, sodass der Test fehlschlägt.

Ich habe versucht, direkt vor der findOne-Abfrage einen Flush hinzuzufügen:

adminRepository.flush();

Die gescheiterte Behauptung blieb jedoch identisch.

Ich kann die Update-SQL-Anweisung im Protokoll sehen:

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

Das Protokoll zeigt jedoch keine SQL, die sich auf den Finder beziehen könnte:

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

Wird es aus irgendeinem Grund ignoriert?

Wenn ich dann findByEmail und findByLogin Finder einen Aufruf hinzufüge, wie 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());

dann kann ich im Protokoll sehen, dass die SQL-Anweisung generiert wird:

Aber die Behauptung:

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

schlägt immer noch fehl, obwohl in der Ablaufverfolgung dasselbe Domänenobjekt abgerufen wurde:

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

Eine andere Sache, die mich mit diesem anderen Finder verwirrt, ist, dass er eine Klausel von Limit 2 enthält, obwohl nur ein Admin-Objekt zurückgegeben werden soll.

Ich dachte, es würde immer ein Limit 1 geben, wenn ein Domain-Objekt zurückgegeben wird. Ist dies eine falsche Annahme in Spring Data?

Beim Einfügen der in dem Konsolenprotokoll angezeigten SQL-Anweisungen in einem MySQL-Client funktioniert die Logik gut:

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)

Warum spiegelt sich dies nicht auf Java-Ebene wider?

41
Stephane

Der EntityManager wird standardmäßig nicht automatisch gelöscht. Sie sollten die folgende Option für Ihre Abfrage verwenden:

@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);
73
Nick V

Ich verstand endlich, was los war.

Beim Erstellen eines Integrationstests für eine Anweisung, die ein Objekt speichert, wird empfohlen, den Entitätsmanager zu leeren, um falsch negative Ergebnisse zu vermeiden, d. Tatsächlich läuft der Test möglicherweise einwandfrei, nur weil der Cache der ersten Ebene nicht geleert wird und kein Schreibvorgang in der Datenbank auftritt. Um diesen falsch negativen Integrationstest zu vermeiden, verwenden Sie einen expliziten Flush im Testkörper. Beachten Sie, dass der Produktionscode niemals ein explizites Flush verwenden muss, da der ORM die Rolle hat, wann er gelöscht werden soll.

Beim Erstellen eines Integrationstests für eine Aktualisierungsanweisung kann es erforderlich sein, den Entitätsmanager zu löschen, um den Cache der ersten Ebene erneut zu laden. In der Tat umgeht eine Aktualisierungsanweisung den Cache der ersten Ebene vollständig und schreibt direkt in die Datenbank. Der Cache der ersten Ebene ist dann nicht synchron und gibt den alten Wert des aktualisierten Objekts wieder. Um diesen veralteten Zustand des Objekts zu vermeiden, verwenden Sie ein explizites Löschen im Testkörper. Beachten Sie, dass der Produktionscode niemals ein explizites Clear verwenden muss, da es die Rolle des ORM ist, zu entscheiden, wann gelöscht werden soll.

Mein Test funktioniert jetzt gut.

10
Stephane

Ich konnte das zum Laufen bringen. Ich werde hier meine Anwendung und den Integrationstest beschreiben.

Die Beispielanwendung

Die Beispielanwendung verfügt über zwei Klassen und eine Schnittstelle, die für dieses Problem relevant sind:

  1. Die Konfigurationsklasse des Anwendungskontexts
  2. Die Entitätsklasse
  3. Die Repository-Schnittstelle

Diese Klassen und die Repository-Schnittstelle werden im Folgenden beschrieben.

Der Quellcode der PersistenceContext-Klasse sieht folgendermaßen aus:

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

Nehmen wir an, wir haben eine einfache Entität namens Todo, deren Quellcode wie folgt aussieht:

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

Unsere Repository-Schnittstelle verfügt über eine einzige Methode namens updateTitle(), die den Titel eines ToDo-Eintrags aktualisiert. Der Quellcode der TodoRepository-Schnittstelle sieht folgendermaßen aus:

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

Die updateTitle()-Methode wird nicht mit der @Transactional-Annotation versehen, da ich denke, dass es am besten ist, eine Service-Schicht als Transaktionsgrenze zu verwenden.

Der Integrationstest

Der Integrationstest verwendet DbUnit, Spring Test und Spring-Test-DBUnit. Es hat drei Komponenten, die für dieses Problem relevant sind:

  1. Das DbUnit-Dataset, das verwendet wird, um die Datenbank vor der Ausführung des Tests in einen bekannten Zustand zu initialisieren.
  2. Das DbUnit-Dataset, mit dem überprüft wird, ob der Titel der Entität aktualisiert wurde.
  3. Der Integrationstest.

Diese Komponenten werden im Folgenden ausführlicher beschrieben.

Der Name der DbUnit-Dataset-Datei, die zum Initialisieren der Datenbank im bekannten Zustand verwendet wird, lautet toDoData.xml. Ihr Inhalt sieht folgendermaßen aus:

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

Der Name des DbUnit-Datasets, mit dem überprüft wird, ob der Titel des ToDo-Eintrags aktualisiert wurde, heißt toDoData-update.xml und sein Inhalt sieht wie folgt aus (aus irgendeinem Grund wurde die Version des ToDo-Eintrags nicht aktualisiert.) aber der Titel war. Irgendwelche Ideen warum?):

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

Der Quellcode des eigentlichen Integrationstests sieht folgendermaßen aus (Denken Sie daran, die Testmethode mit der Annotation @Transactional zu kommentieren):

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

Nachdem ich den Integrationstest ausgeführt habe, wird der Test bestanden und der Titel des ToDo-Eintrags wird aktualisiert. Das einzige Problem, das ich habe, ist, dass das Versionsfeld nicht aktualisiert wird. Irgendwelche Ideen warum?

Ich verstehe, dass diese Beschreibung etwas vage ist. Wenn Sie weitere Informationen zum Schreiben von Integrationstests für JPA-Repositorys von Spring Data erhalten möchten, lesen Sie mein Blogbeitrag dazu .

6
pkainulainen

Ich hatte Probleme mit dem gleichen Problem, bei dem ich versuchte, eine Aktualisierungsabfrage auszuführen, wie Sie es auch getan haben- 

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

Dies funktioniert, wenn Sie @EnableTransactionManagement-Annotation für die Hauptklasse festgelegt haben . Spring 3.1 führt die @EnableTransactionManagement-Annotation ein, die in @Configuration-Klassen verwendet werden soll, und aktiviert die Transaktionsunterstützung.

0
Innovationchef