web-dev-qa-db-ja.com

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

しかし、失敗したアサーションは同じままでした。

ログでupdate 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='[email protected]', profile=NULL,
post_login_url=NULL where id=2839

ただし、ログには、Finderに関連する可能性のある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_]

この他のFinderで私を困惑させるもう1つのことは、Adminオブジェクトを1つだけ返すことになっているにもかかわらず、limit 2句が表示されることです。

1つのドメインオブジェクトを返す場合、常に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, '[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レベルに反映されないのでしょうか?

44
Stephane

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

私は最終的に何が起こっているのか理解しました。

オブジェクトを保存するステートメントで統合テストを作成する場合、エンティティマネージャーをフラッシュして、偽陰性を回避すること、つまり、テストは正常に実行されるが実稼働で実行すると失敗するテストを回避することをお勧めします。実際、1次キャッシュがフラッシュされず、データベースに書き込みがヒットしないため、テストは正常に実行されます。この偽陰性統合テストを回避するには、テスト本体で明示的なフラッシュを使用します。プロダクションコードは、フラッシュするタイミングを決定するORMの役割であるため、明示的なフラッシュを使用する必要がないことに注意してください。

更新ステートメントで統合テストを作成する場合、1次キャッシュを再ロードするためにエンティティマネージャーをクリアする必要がある場合があります。実際、更新ステートメントは完全に一次キャッシュをバイパスし、データベースに直接書き込みます。その後、第1レベルのキャッシュは同期されなくなり、更新されたオブジェクトの古い値を反映します。オブジェクトのこの古い状態を回避するには、テスト本体で明示的なクリアを使用します。プロダクションコードは、クリアするタイミングを決定するのがORMの役割であるため、明示的なクリアを使用する必要はありません。

テストが正常に機能するようになりました。

10
Stephane

これを機能させることができました。ここでアプリケーションと統合テストについて説明します。

サンプルアプリケーション

サンプルアプリケーションには、この問題に関連する2つのクラスと1つのインターフェイスがあります。

  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()という1つのメソッドがあり、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を使用します。この問題に関連する3つのコンポーネントがあります。

  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>

Todoエントリのタイトルが更新されたことを確認するために使用されるDbUnitデータセットの名前は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リポジトリーの統合テストの作成に関する詳細情報が必要な場合は、 私のブログ投稿 をご覧ください。

7
pkainulainen

私はあなたと同じような更新クエリを実行しようとしていた同じ問題に苦労しました-

@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アノテーションを導入し、トランザクションサポートを有効にします。

0
Innovationchef