1. 程式人生 > >Spring Data JPA的Repository

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方法。

如何檢查實體類的狀態

  1. 框架預設會檢查實體類的主鍵屬性的值,如果為null就表示尚未持久化。
  2. 如果實體類實現了Persistable介面,框架會呼叫isNew方法。
  3. 還可以實現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);
}

@QueryHintsvalue項是一組@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);
}