• 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);
    }
    
  • 相关阅读:
    玩耍redis遇到的问题之记录
    哈勃望远镜--星柱图
    用js将从后台得到的时间戳(毫秒数)转换为想要的日期格式
    div水平居中
    hibernate和spring下载网址
    intellj idea 如何设置类头注释和方法注释(转载)
    转载:IT人高效的休息方式
    什么是REST?以及RESTful的实现
    easyui datagrid 获取记录数 页数 当前页
    font字体文件跨域
  • 原文地址:https://www.cnblogs.com/rim99/p/8672912.html
Copyright © 2020-2023  润新知