Spring Data JPA的Repository

撰写于 2018-02-21 修改于 2018-12-15 标签 翻译

本文摘译自官方文档第四章《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);
}
Site by Zhang,Xin using Hexo & Random

Hide