Spring Data JPA Update @Query没有更新?

Ste*_*ane 47 java jpa spring-data

我有一个更新查询:

@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);
Run Code Online (Sandbox Code Playgroud)

我正在尝试在集成测试中使用它:

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());
Run Code Online (Sandbox Code Playgroud)

但是这些字段没有更新并保留其初始值,因此测试失败了.

我尝试在findOne查询之前添加一个flush:

adminRepository.flush();
Run Code Online (Sandbox Code Playgroud)

但失败的断言仍然相同.

我可以在日志中看到更新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='stephane@thalasoft.com', profile=NULL,
post_login_url=NULL where id=2839
Run Code Online (Sandbox Code Playgroud)

但是日志显示没有可能与finder相关的sql:

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

是否因某些缓存原因而被忽略?

如果我然后添加对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());
Run Code Online (Sandbox Code Playgroud)

然后我可以在日志中看到正在生成的sql语句:

但断言:

assertEquals("Toto", myadmin.getFirstname());
Run Code Online (Sandbox Code Playgroud)

即使跟踪显示检索到相同的域对象,仍然会失败:

TRACE [BasicExtractor] found [1037] as column [id14_]
Run Code Online (Sandbox Code Playgroud)

另一个令我困惑的另一个问题是它显示了一个限制2子句,即使它应该只返回一个Admin对象.

我认为返回一个域对象时总会有一个限制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, 'zemail@thalasoft.com039', '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 | zemail@thalasoft.com039 | 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='stephane@thalasoft.com', 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 | stephane@thalasoft.com | 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='stephane@thalasoft.com' 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 | stephane@thalasoft.com | 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 | stephane@thalasoft.com | Toto | LeHeros | qwerty | zpassword039 | | NULL | 0 | NULL | 0 | NULL | 
+-------+------------+------------+---------+------------+------------------------+--------------+-------------+----------+--------------+----------------+------------+------------------+------------+-------------+-----------+
1 row in set (0.00 sec)
Run Code Online (Sandbox Code Playgroud)

那么为什么这不会反映在Java级别呢?

小智 90

默认情况下,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);
Run Code Online (Sandbox Code Playgroud)

  • 如果您不希望在clearAutomatically更新后松开未刷新的更改,请阅读以下方法:[修改更新查询 - 刷新持久性上下文](http://stackoverflow.com/questions/32258857/spring-boot-data-jpa -modifying更新查询刷新,持久化上下文) (5认同)
  • 解决方案是完美的,只是一个小的更新,也将需要@Transactional。 (3认同)
  • 你是我的英雄! (2认同)
  • 缺少冲洗不是问题。问题是“EntityManager”保留未更改的实体。按照此答案中的说明清除它可以解决该问题。 (2认同)
  • 为什么不能使用`@Modifying(flushAutomatically = true,clearAutomatically = true)`?这两个问题不就都解决了吗? (2认同)

Ste*_*ane 10

我终于明白了发生了什么.

在保存对象的语句上创建集成测试时,建议刷新实体管理器以避免任何误报,即避免测试运行正常,但在生产中运行时操作会失败.实际上,测试可能运行正常,因为没有刷新第一级缓存并且没有写入数据库.为了避免这种假阴性积分测试,在测试体中使用显式刷新.请注意,生产代码永远不需要使用任何显式刷新,因为ORM的角色决定何时刷新.

在update语句上创建集成测试时,可能需要清除实体管理器以重新加载第一级缓存.实际上,更新语句完全绕过第一级缓存并直接写入数据库.然后,第一级缓存不同步并反映更新对象的旧值.要避免对象的这种陈旧状态,请在测试主体中使用显式清除.请注意,生产代码永远不需要使用任何明确的清除,因为ORM的角色决定何时清除.

我的测试现在运行得很好.


pka*_*nen 8

我能够让这个工作.我将在这里描述我的应用程序和集成测试.

示例应用程序

示例应用程序有两个类和一个与此问题相关的接口:

  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;
    }
}
Run Code Online (Sandbox Code Playgroud)

让我们假设我们有一个简单的实体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;
}
Run Code Online (Sandbox Code Playgroud)

我们的存储库接口有一个调用的方法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);
}
Run Code Online (Sandbox Code Playgroud)

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>
Run Code Online (Sandbox Code Playgroud)

用于验证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>
Run Code Online (Sandbox Code Playgroud)

实际集成测试的源代码如下所示(请记住使用注释注释测试方法@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");
    }
}
Run Code Online (Sandbox Code Playgroud)

运行集成测试后,测试通过并更新todo条目的标题.我遇到的唯一问题是版本字段未更新.有什么想法吗?

我不认为这种描述有点模糊.如果您想获得有关为Spring Data JPA存储库编写集成测试的更多信息,可以阅读我的博客文章.

  • 这不是JPA的工作方式.如果触发操纵查询,则持久性上下文保持不变.您可以通过将`@Modifying`上的`clearAutomatically`标志设置为true来解决此问题.这将调用`EntityManager.clear()`及其后果(例如,其他实体的剩余,待定更改等).如果您再次查找实体,则应该看到新值. (4认同)