Spring Data JPA Update @Query 没有更新?
Posted
技术标签:
【中文标题】Spring Data JPA Update @Query 没有更新?【英文标题】:Spring Data JPA Update @Query not updating? 【发布时间】:2013-06-11 21:04:30 【问题描述】:我有一个更新查询:
@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();
但失败的断言保持不变。
在日志中可以看到更新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
但日志显示没有可能与查找器相关的 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 让我感到困惑的另一件事是它显示了一个限制 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)
那么为什么这没有反映在 Java 级别呢?
【问题讨论】:
【参考方案1】:默认情况下,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);
【讨论】:
如果您不想在 clearAutomatically 更新后丢失未刷新的更改,请阅读此方法:Modifying update query - Refresh persistence context 解决方案是完美的,只是一个小更新,@Transactional 也将是必需的。 缺少刷新不是问题。问题是EntityManager
保留未更改的实体。按照这个答案中的描述清除它可以解决这个问题。
为什么不能使用@Modifying(flushAutomatically = true, clearAutomatically = true)
?这不是解决这两个问题吗?
此解决方案的描述与解决方案本身不对应。 “默认情况下,EntityManager 不会在执行此查询后清除持久性上下文”。这就是clearAutomatically = true
所做的。它与“冲洗”无关。【参考方案2】:
我终于明白是怎么回事了。
在保存对象的语句上创建集成测试时,建议刷新实体管理器以避免任何误报,即避免测试运行良好但在生产中运行时操作失败。事实上,测试可能运行良好,因为第一级缓存没有刷新并且没有写入数据库。为了避免这种假阴性集成测试,请在测试主体中使用显式刷新。请注意,生产代码永远不需要使用任何显式刷新,因为它是 ORM 的角色来决定何时刷新。
在更新语句上创建集成测试时,可能需要清除实体管理器以重新加载一级缓存。实际上,更新语句完全绕过一级缓存,直接写入数据库。第一级缓存然后不同步并反映更新对象的旧值。要避免对象的这种陈旧状态,请在测试主体中使用显式清除。请注意,生产代码永远不需要使用任何显式清除,因为它是 ORM 的角色来决定何时清除。
我的测试现在运行良好。
【讨论】:
【参考方案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()
的方法,用于更新待办事项条目的标题。 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。它包含三个与此问题相关的组件:
-
DbUnit 数据集,用于在执行测试之前将数据库初始化为已知状态。
用于验证实体标题是否已更新的 DbUnit 数据集。
集成测试。
下面将对这些组件进行更详细的描述。
用于将数据库初始化为已知状态的 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,其内容如下所示(出于某种原因待办事项条目未更新,但标题已更新。任何想法为什么?):
<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");
在我运行集成测试后,测试通过并且待办事项条目的标题被更新。我遇到的唯一问题是版本字段没有更新。任何想法为什么?
我不明白这个描述有点模糊。如果你想获得更多关于为 Spring Data JPA 存储库编写集成测试的信息,你可以阅读my blog post about it。
【讨论】:
您好 Petri,感谢您提供集成测试展示案例。我可以看到可以补充它的一件事,就是在更新之前和之后有一些断言,以确定更新确实发生了。例如,更新字段的值上的 assertEquals。 这不是 JPA 的工作方式。如果您触发操纵查询,则持久性上下文保持不变。您可以通过将@Modifying
上的clearAutomatically
标志设置为true 来解决此问题。这将调用EntityManager.clear()
及其所有后果(例如,剩余、对其他实体的未决更改丢失等)。如果您随后再次查找实体,您应该会看到新值。
1- 我认为默认情况下 ORM 会使持久层不可见,并允许我仅考虑 Java 对象。
2- 我认为事实并非如此,我确信有一些好的和正当的理由。
3- 正如你所说,使用 clearAutomatically 属性会产生一些后果,从你使用的措辞来看,我觉得这些不是我们真正想要的。【参考方案4】:
我在尝试执行更新查询时遇到了同样的问题 -
@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
类并启用事务支持。
【讨论】:
【参考方案5】:这里的根本问题是 JPA 的一级缓存。 来自 JPA 规范版本 2.2 第 3.1 节。强调是我的:
EntityManager 实例与持久性上下文相关联。持久性上下文是一组实体实例,其中对于任何持久性实体身份都有一个唯一的实体实例。
这很重要,因为 JPA 会跟踪对该实体的更改,以便将它们刷新到数据库中。 作为副作用,它还意味着在单个持久性上下文中,实体只加载一次。 这就是为什么重新加载更改的实体没有任何效果的原因。
你有几个选择来处理这个问题:
从EntityManager
中驱逐实体。
这可以通过调用EntityManager.detach
来完成,用@Modifying(clearAutomatically = true)
注释更新方法,这会驱逐所有实体。
确保首先刷新对这些实体的更改,否则您最终可能会丢失更改。
使用EntityManager.refresh()
。
使用不同的持久性上下文来加载实体。
最简单的方法是在单独的事务中进行。
使用 Spring,这可以通过在从未使用 @Transactional
注释的 bean 调用的 bean 上使用带有 @Transactional
注释的单独方法来完成。
另一种方法是使用TransactionTemplate
,它在使事务边界非常明显的测试中效果特别好。
【讨论】:
以上是关于Spring Data JPA Update @Query 没有更新?的主要内容,如果未能解决你的问题,请参考以下文章
Spring Data JPA 中使用Update Query更新实体类问题
spring data jpa执行update和delete语句时报错处理
Spring Data JPA“ON DUPLICATE KEY UPDATE amount = account.amount + someValue”
[Spring Data JPA问题]Executing an update/delete query; nested exception is javax.persistence.Transacti
我现在用的是springmvc+spring data jpa ;在用EntityManager类的q.getResultList()类型转换失败