前幾天,有個同事在使用JPA的自定義SQL方法時,程式一直報異常,搗鼓了半天也沒能解決,咨詢我的時候,我看了一眼他的程式,差不多是這個樣子的: 我告訴他,你的deleteUserById方法缺少了@Modifying註解和@Transactional註解,他半信半疑地試了一下,然後果然就解決了。其實 ...
前幾天,有個同事在使用JPA的自定義SQL方法時,程式一直報異常,搗鼓了半天也沒能解決,咨詢我的時候,我看了一眼他的程式,差不多是這個樣子的:
1 @Repository 2 public interface UserRepository extends JpaRepository<User,Long> { 3 4 @Query(value = "delete from pro_user where id = ?1",nativeQuery = true) 5 void deleteUserById(Long id); 6 }
我告訴他,你的deleteUserById方法缺少了@Modifying註解和@Transactional註解,他半信半疑地試了一下,然後果然就解決了。其實,如果他查一下官方資料或許很快也就能找到答案。基於這個背景,本文詳細講解一下為何我們自定義的插入、更新、刪除操作需要加@Modifying註解和@Transactional註解。
一、@Modifying註解
在官方資料中,給出了這樣幾句說明:
As the queries themselves are tied to the Java method that executes them, you can actually bind them directly by using the Spring Data JPA @Query annotation rather than annotating them to the domain class.
You can modify queries that only need parameter binding by annotating the query method with @Modifying
The @Modifying annotation is only relevant in combination with the @Query annotation. Derived query methods or custom methods do not require this Annotation.
Doing so triggers the query annotated to the method as an updating query instead of a selecting one.
如下:
@Modifying @Query("update User u set u.firstname = ?1 where u.lastname = ?2") int setFixedFirstnameFor(String firstname, String lastname);
第一句話的意思是可以用@Query註解來將自定義sql語句綁定到自定義方法上。
第二句話的意思時,可以用@Modifying註解來標註只需要綁定參數的自定義的更新類語句(更新、插入、刪除)。
第三名話的意思是說@Modifying只與@Query聯合使用,派生類的查詢方法和自定義的方法不需要此註解,如:
1 @Repository 2 public interface UserRepository extends JpaRepository<User,Long> { 3 4 // 父類的保存方法 5 @Override 6 User save(User entity); 7 8 // 按照JPA語法規則自定義的查詢方法 9 List<User> findFirst10ByLastname(String lastName, Pageable pageable); 10 }
第四句話的意思是,當加上@Modifying註解時,JPA會以更新類語句來執行,而不再是以查詢語句執行。
也就是說,當我們要通過自已寫的更新、插入、刪除SQL語句來實現更新、插入、刪除操作時,至少需要用兩個步驟:
1)@Query來註入我們自定義的sql;
2)使用@Modifying來標註是一個更新類的自定義語句。
按照這個規則,修改同事的那個方法:
1 @Repository 2 public interface UserRepository extends JpaRepository<User,Long> { 3 4 @Modifying 5 @Query(value = "delete from pro_user where id = ?1",nativeQuery = true) 6 void deleteUserById(Long id); 7 }
但是,此時,該方法還不完整,執行時程式會報以下錯誤:
org.springframework.dao.InvalidDataAccessApiUsageException: Executing an update/delete query; nested exception is javax.persistence.TransactionRequiredException:
Executing an update/delete query at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:402) at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:255) ...... at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70) Caused by: javax.persistence.TransactionRequiredException: Executing an update/delete query at org.hibernate.internal.AbstractSharedSessionContract.checkTransactionNeededForUpdateOperation(AbstractSharedSessionContract.java:398) at org.hibernate.query.internal.AbstractProducedQuery.executeUpdate(AbstractProducedQuery.java:1585) .......
二、@Transactional註解
官方的說明:
readOnly
flag is set to true
. All others are configured with a plain @Transactional
so that default transaction configuration applies. For details, see JavaDoc of SimpleJpaRepository
. If you need to tweak transaction configuration for one of the methods declared in a repository, redeclare the method in your repository interface, as follows:
Example. Custom transaction configuration for CRUD
1 public interface UserRepository extends CrudRepository<User, Long> { 2 3 @Override 4 @Transactional(timeout = 10) 5 public List<User> findAll(); 6 7 // Further query method declarations 8 }
這句話的意思是,預設情況下,repository 介面中的CRUD方法都是被@Transactional註解修飾了的,對於讀的操作方法,@Transactional註解的readOnly屬性是被設置為true的,即只讀;CRUD中的其他方法被@Transactional修飾,即非只讀。如果你需要修改repository 介面中的某些方法的事務屬性,可以在該方法上重新加上@Transactional註解,並設置需要的屬性。
我們先來看一下,@Transactional註解的源碼:
1 @Target({ElementType.METHOD, ElementType.TYPE}) 2 @Retention(RetentionPolicy.RUNTIME) 3 @Inherited 4 @Documented 5 public @interface Transactional { 6 7 Propagation propagation() default Propagation.REQUIRED; 8 9 Isolation isolation() default Isolation.DEFAULT; 10 11 int timeout() default -1; 12 13 boolean readOnly() default false; 14 15 // 其他省略 16 }
由上可見@Transactional註解的readOnly預設的屬性的false,即非只讀,當一個事務是非只讀事務的時候,我們可以進行任何操作。
再看一下repository 介面的實現類SimpleJpaRepository的源碼(只摘了部分源碼):
1 @Repository 2 @Transactional( 3 readOnly = true 4 ) 5 public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> { 6 7 @Transactional 8 public void deleteById(ID id) { 9 Assert.notNull(id, "The given id must not be null!"); 10 this.delete(this.findById(id).orElseThrow(() -> { 11 return new EmptyResultDataAccessException(String.format("No %s entity with id %s exists!", this.entityInformation.getJavaType(), id), 1); 12 })); 13 } 14 15 @Transactional 16 public void delete(T entity) { 17 Assert.notNull(entity, "The entity must not be null!"); 18 this.em.remove(this.em.contains(entity) ? entity : this.em.merge(entity)); 19 } 20 21 @Transactional 22 public void deleteAll(Iterable<? extends T> entities) { 23 Assert.notNull(entities, "The given Iterable of entities not be null!"); 24 Iterator var2 = entities.iterator(); 25 26 while(var2.hasNext()) { 27 T entity = var2.next(); 28 this.delete(entity); 29 } 30 } 31 32 public T getOne(ID id) { 33 Assert.notNull(id, "The given id must not be null!"); 34 return this.em.getReference(this.getDomainClass(), id); 35 } 36 37 public List<T> findAll() { 38 return this.getQuery((Specification)null, (Sort)Sort.unsorted()).getResultList(); 39 } 40 41 public List<T> findAll(@Nullable Specification<T> spec) { 42 return this.getQuery(spec, Sort.unsorted()).getResultList(); 43 } 44 45 public List<T> findAll(@Nullable Specification<T> spec, Sort sort) { 46 return this.getQuery(spec, sort).getResultList(); 47 } 48 49 public <S extends T> long count(Example<S> example) { 50 return executeCountQuery(this.getCountQuery(new SimpleJpaRepository.ExampleSpecification(example), example.getProbeType())); 51 } 52 53 public <S extends T> boolean exists(Example<S> example) { 54 return !this.getQuery(new SimpleJpaRepository.ExampleSpecification(example), example.getProbeType(), (Sort)Sort.unsorted()).getResultList().isEmpty(); 55 } 56 57 @Transactional 58 public <S extends T> S save(S entity) { 59 if (this.entityInformation.isNew(entity)) { 60 this.em.persist(entity); 61 return entity; 62 } else { 63 return this.em.merge(entity); 64 } 65 } 66 67 @Transactional 68 public <S extends T> S saveAndFlush(S entity) { 69 S result = this.save(entity); 70 this.flush(); 71 return result; 72 } 73 74 @Transactional 75 public void flush() { 76 this.em.flush(); 77 } 78 }
從SimpleJpaRepository源碼中可以看出:
1)該類上註解了只讀事務@Transactional(readOnly = true);
2)該類的所有查詢類操作方法都與類相同,都擁有隻讀事務;
3)該類的所有保存、更新、刪除操作方法都用@Transactional重新註解了(預設readOnly=false)。
說明JPA為我們提供的所有方法,包括JPA規則的自定義方法在其底層都為我們做好了事務處理,而我們自定義的方法需要自己來標註事務的類型是只讀還是非只讀。根據這個原理,再次修改開篇所列出的方法:
1 @Repository 2 public interface UserRepository extends JpaRepository<User,Long> { 3 4 @Transactional 5 @Modifying 6 @Query(value = "delete from pro_user where id = ?1",nativeQuery = true) 7 void deleteUserById(Long id); 8 }
至此,該方法按所期望的結果運行成功了。
三、@Modifying註解補充說明
1 @Retention(RetentionPolicy.RUNTIME) 2 @Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) 3 @Documented 4 public @interface Modifying { 5 6 boolean flushAutomatically() default false; 7 8 boolean clearAutomatically() default false; 9 }
該註解中有兩個屬性:flushAutomatically、clearAutomatically,從字面理解是自動刷新和自動清除。
自動刷新,即執行完語句後立即將變化內容刷新到磁碟,如果是insert語句操作,則與JPA的<S extends T> S saveAndFlush(S entity);方法效果相同;
自動清除,即執行完語句後自動清除掉已經過期的實體,比如,我們刪除了一個實體,但是在還沒有執行flush操作時,這個實體還存在於實體管理器EntityManager中,但這個實體已經過期沒有任何用處,直到flush操作時才會被刪除掉。如果希望在刪除該實體時立即將該實體從實體管理器中刪除,則可以將該屬性設置為true,如:
1 @Modifying(clearAutomatically = true) 2 @Transactional 3 @Query(value = "delete from pro_user where id = ?1",nativeQuery = true) 4 void deleteUserById(Long id);