Spring Data JPA的Repository
Spring Data JPA的Repository
本文摘譯自官方文件第四章《JPA Repositories》。版本:2.0.3.RELEASE
基本配置
這裡是Spring Data JPA的註解風格的配置類示例。(為便於描述,後文直接稱Spring Data JPA為框架)。
@Configuration @EnableJpaRepositories @EnableTransactionManagement class ApplicationConfig { @Bean public DataSource dataSource() { EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder(); return builder.setType(EmbeddedDatabaseType.HSQL).build(); } @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); vendorAdapter.setGenerateDdl(true); LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean(); factory.setJpaVendorAdapter(vendorAdapter); factory.setPackagesToScan("com.acme.domain"); factory.setDataSource(dataSource()); return factory; } @Bean public PlatformTransactionManager transactionManager() { JpaTransactionManager txManager = new JpaTransactionManager(); txManager.setEntityManagerFactory(entityManagerFactory()); return txManager; } }
上面這個例子展示了使用Spring的JDBC API - EmbeddedDatabaseBuilder
設定嵌入式HSQL資料庫。然後用Hibernate實現持久化機制。這裡使用了LocalContainerEntityManagerFactoryBean
而不是EntityManagerFactory
,是因為前者可以更好的處理異常。還有一個基礎元件就是JpaTransactionManager
。最後使用@EnableJpaRepositories
註解保證每一個註解了@Repository
的倉儲類丟擲的異常可以轉入到Spring的DataAccessException
異常體系。如果沒有指定基礎package,就預設為配置類所在的package。
持久化物件
儲存持久化物件可以使用CrudRepository.save
方法。這個方法將持久化物件的持久化(persist)和合並(merge)抽象為一個方法。如果物件還沒有持久化,就會呼叫entityManager.persist
方法。如果已經持久化,就會呼叫entityManager.merge
方法。
如何檢查實體類的狀態
- 框架預設會檢查實體類的主鍵屬性的值,如果為null就表示尚未持久化。
- 如果實體類實現了
Persistable
介面,框架會呼叫isNew
方法。 - 還可以實現
EntityInformation
介面,但這個方法比較複雜,一般不怎麼用,詳細請研究文件。
查詢方法
框架支援函式命名的查詢方法定義,也支援註解方式。
函式命名的關鍵字,可以看文件。
NamedQuery
@NamedQuery
註解可以自定義查詢語句。這個註解使用在實體類上。
@Entity
@NamedQuery(name = "User.findByEmailAddress",
query = "select u from User u where u.emailAddress = ?1")
public class User {
...
}
倉儲介面的定義。
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByLastname(String lastname);
User findByEmailAddress(String emailAddress);
}
當呼叫介面方法時,框架首先根據實體類查詢是否註解了方法名對應的自定義查詢語句。例如,呼叫findByEmailAddress
的時候,找到了實體類註解的方法select u from User u where u.emailAddress = ?1
。
Query
上面那個方法多少有點不直觀。@Query
註解可以直接在介面方法上註明自定義的查詢語句。
public interface UserRepository extends JpaRepository<User, Long> {
@Query("select u from User u where u.emailAddress = ?1")
User findByEmailAddress(String emailAddress);
}
在實際應用中,相比@NamedQuery
註解,@Query
註解有更高的優先順序。
如果@Query
註解的native
值為true
,方法就可以直接執行SQL語句查詢了。
不過,對於這種SQL語句,文件聲稱目前不支援動態排序查詢。對於分頁,用於需要指定計數查詢語句.
public interface UserRepository extends JpaRepository<User, Long> {
@Query(value = "SELECT * FROM USERS WHERE LASTNAME = ?1",
countQuery = "SELECT count(*) FROM USERS WHERE LASTNAME = ?1",
nativeQuery = true)
Page<User> findByLastname(String lastname, Pageable pageable);
}
排序
Sort
和@Query
配合使用比較方便。Sort
構造器引數必須是查詢結果返回的欄位,不接受SQL函式。要使用SQL函式,應該用JpaSort.unsafe
。
public interface UserRepository extends JpaRepository<User, Long> {
@Query("select u from User u where u.lastname like ?1%")
List<User> findByAndSort(String lastname, Sort sort);
@Query("select u.id, LENGTH(u.firstname) as fn_len from User u where u.lastname like ?1%")
List<Object[]> findByAsArrayAndSort(String lastname, Sort sort);
}
repo.findByAndSort("lannister", new Sort("firstname")); // 1
repo.findByAndSort("stark", new Sort("LENGTH(firstname)")); // 2
repo.findByAndSort("targaryen", JpaSort.unsafe("LENGTH(firstname)")); // 3
repo.findByAsArrayAndSort("bolton", new Sort("fn_len")); // 4
上面第二個呼叫是會丟擲異常的,應該像第三個方法那樣呼叫。
如何使用命名引數
框架預設使用的佔位符是按照引數順序,這樣不太直觀。使用命名引數,程式碼能更直觀。
public interface UserRepository extends JpaRepository<User, Long> {
@Query("select u from User u where u.firstname = :firstname or u.lastname = :lastname")
User findByLastnameOrFirstname(@Param("lastname") String lastname,
@Param("firstname") String firstname);
}
SpEL表示式
框架還吃支援在@Query
註解中使用SpEL表示式。
SpEL表示式中可以使用#{#entityName}
特指實體類的名稱。這個與實體類的@Entity
註解的name
屬性引數一致。
@Entity
public class User {
@Id
@GeneratedValue
Long id;
String lastname;
}
public interface UserRepository extends JpaRepository<User,Long> {
@Query("select u from #{#entityName} u where u.lastname = ?1")
List<User> findByLastname(String lastname);
}
這種定義方式通常用於定義範型倉儲介面。
@MappedSuperclass
public abstract class AbstractMappedType {
…
String attribute
}
@Entity
public class ConcreteType extends AbstractMappedType { … }
@NoRepositoryBean
public interface MappedTypeRepository<T extends AbstractMappedType> extends Repository<T, Long> {
@Query("select t from #{#entityName} t where t.attribute = ?1")
List<T> findAllByAttribute(String attribute);
}
public interface ConcreteRepository extends MappedTypeRepository<ConcreteType> { … }
修改式查詢
對於update
或者delete
這樣的修改式查詢,需要在@Query
註解上增加@Modifying
註解。執行過查詢之後,EntityManager
有可能會存在過時的實體物件。但是,EntityManager
預設不會自動更新,因為呼叫EntityManager.clear
方法會抹去EntityManager
所有的未提交修改。如果確認要自動更新,需要將@Modifying
註解的clearAutomatically
屬性設定為true
。
框架支援命名式刪除語句,也支援註解式。
interface UserRepository extends Repository<User, Long> {
void deleteByRoleId(long roleId);
@Modifying
@Query("delete from User u where user.role.id = ?1")
void deleteInBulkByRoleId(long roleId);
}
兩者在執行時有一個很大的區別。後者僅僅執行JPQL查詢,不會觸發任何生命週期回撥。而前者會在執行完查詢之後,呼叫CrudRepository.delete(Iterable<User> users)
方法,從而觸發@PreRemove
回撥。
QueryHints
@QueryHints
註解支援對查詢語句進行微調。例如,設定快取、設定鎖超時等等。
可以看看這篇文章,講的不錯。
public interface UserRepository extends Repository<User, Long> {
@QueryHints(value = { @QueryHint(name = "name", value = "value")},
forCounting = false)
Page<User> findByLastname(String lastname, Pageable pageable);
}
@QueryHints
的value
項是一組@QueryHint
,另一個forCounting
表示是否為可能的聚合查詢應用這些微調。例子中,分頁查詢回去查詢總頁數,這個子查詢不會應用微調。
配置載入計劃
@EntityGraph
和@NamedEntityGraph
配合使用可以實現懶載入多級關聯物件。
@NamedEntityGraph
註解在實體類上,表示的是載入計劃。
@Entity
@NamedEntityGraph(name = "GroupInfo.detail",
attributeNodes = @NamedAttributeNode("members"))
public class GroupInfo {
// default fetch mode is lazy.
@ManyToMany
List<GroupMember> members = new ArrayList<GroupMember>();
...
}
@EntityGraph
表示要執行的載入計劃。
@Repository
public interface GroupRepository extends CrudRepository<GroupInfo, String> {
@EntityGraph(value = "GroupInfo.detail", type = EntityGraphType.LOAD)
GroupInfo getByGroupName(String name);
}
也可以不用@NamedEntityGraph
註解,而是直接使用屬性attributePaths
臨時設定查詢計劃。
@Repository
public interface GroupRepository extends CrudRepository<GroupInfo, String> {
@EntityGraph(attributePaths = { "members" })
GroupInfo getByGroupName(String name);
}
這個說起來很多內容,具體研究一下JPA 2.1規範的3.7.4章節。
儲存過程的呼叫
假設資料庫中有這樣的儲存過程。
/;
DROP procedure IF EXISTS plus1inout
/;
CREATE procedure plus1inout (IN arg int, OUT res int)
BEGIN ATOMIC
set res = arg + 1;
END
/;
這是一個原子加一的方法。
首先要在實體類上宣告過程。
@Entity
@NamedStoredProcedureQuery(name = "User.plus1", procedureName = "plus1inout", parameters = {
@StoredProcedureParameter(mode = ParameterMode.IN, name = "arg", type = Integer.class),
@StoredProcedureParameter(mode = ParameterMode.OUT, name = "res", type = Integer.class) })
public class User {}
然後再倉儲介面中宣告方法。以下四種方式是等效的。
@Procedure("plus1inout")
Integer explicitlyNamedPlus1inout(Integer arg);
@Procedure(procedureName = "plus1inout")
Integer plus1inout(Integer arg);
@Procedure(name = "User.plus1")
Integer entityAnnotatedCustomNamedProcedurePlus1(@Param("arg") Integer arg);
@Procedure
Integer plus1(@Param("arg") Integer arg);
Specification
JPA 2.0 引入了criteria
API能夠以程式碼的方式構建查詢。criteria
API其實就是為領域類的查詢操作構建where子句。退一步來看,其實criteria
也就是一種謂詞(predicate)。Spring Data JPA框架接受了Eric Evans的《Domain Driven Design》一書的Specification概念,擁有與criteria
相似的API。
首先,倉儲介面必須繼承JpaSpecificationExecutor
介面。
public interface CustomerRepository extends CrudRepository<Customer, Long>, JpaSpecificationExecutor {
…
}
該介面定義了一系列方法,可以實現謂詞的可變性。
List<T> findAll(Specification<T> spec);
實際上,Specification
也是一個介面。
public interface Specification<T> {
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder);
}
Specification
可以很方便的構建新謂詞。看個例子。
先定義基礎的Specification
。
public class CustomerSpecs {
public static Specification<Customer> isLongTermCustomer() {
return new Specification<Customer>() {
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query,
CriteriaBuilder builder) {
LocalDate date = new LocalDate().minusYears(2);
return builder.lessThan(root.get(_Customer.createdAt), date);
}
};
}
public static Specification<Customer> hasSalesOfMoreThan(MontaryAmount value) {
return new Specification<Customer>() {
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
CriteriaBuilder builder) {
// build query here
}
};
}
}
這時使用方法。
List<Customer> customers = customerRepository.findAll(isLongTermCustomer());
這樣可以構建新的複雜謂詞。
MonetaryAmount amount = new MonetaryAmount(200.0, Currencies.DOLLAR);
List<Customer> customers = customerRepository.findAll(
where(isLongTermCustomer()).or(hasSalesOfMoreThan(amount)));
事務
倉儲介面物件的CRUD方法均預設具備事務性。讀取查詢的readonly
屬性預設為true
。具體可看文件SimpleJpaRepository。要想修改事務配置,需要覆蓋原來的方法。
public interface UserRepository extends CrudRepository<User, Long> {
@Override
@Transactional(timeout = 10)
public List<User> findAll();
// Further query method declarations
}
上面這個例子設定了10s超時。
還有一種方法是在service層進行調整。
@Service
class UserManagementImpl implements UserManagement {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
@Autowired
public UserManagementImpl(UserRepository userRepository,
RoleRepository roleRepository) {
this.userRepository = userRepository;
this.roleRepository = roleRepository;
}
@Transactional
public void addRoleToAllUsers(String roleName) {
Role role = roleRepository.findByName(roleName);
for (User user : userRepository.findAll()) {
user.addRole(role);
userRepository.save(user);
}
}
上面這個例子實現了addRoleToAllUsers
方法的事務性,而方法內部呼叫的事務性會被忽視。如果想要在facade裡面配置事務性,需要增加註解@EnableTransactionManagement
。
介面定義處也可以註解@Transactional
,但是優先順序低於方法定義處的同類註解。
鎖
框架支援為查詢操作加鎖。
interface UserRepository extends Repository<User, Long> {
// Plain query method
@Lock(LockModeType.READ)
List<User> findByLastname(String lastname);
}