Spring-Data-Jpa使用總結

  • 2019 年 10 月 17 日
  • 筆記

參考資源列表

  1. 官方文檔:https://docs.spring.io/spring-data/jpa/docs/2.1.5.RELEASE/reference/html/
  2. 《Spring Data JPA入門到精通》

前言

JPAJava Persistence API的簡稱,是SpringHibernate的基礎上進行的二次封裝框架,為了更好更方便的融入Spring大家庭,同時也提供了一些Hibernate沒有的特性,與其他ORM框架一起構成SpringData,統一封裝了ORM層,使開發人員使用起來更加方便快捷。

備註:本文所有代碼都是基於SpringBoot 2.1.5版本

JPA的使用

基本單表操作的使用

對於操作單表來說,jpa提供了非常方便使用的封裝,我們只需要按規範編寫Repository接口同時繼承JpaRepository就可以享用jpa基本功能了。代碼如下:

User實體:

package com.yizhu.entity;    import lombok.Builder;  import lombok.Data;  import lombok.ToString;    import javax.persistence.*;  import java.io.Serializable;  import java.util.List;  import java.util.Set;    @Entity  @Table(name = "t_user")  @Data  @Builder  public class User implements Serializable {        private static final long serialVersionUID = 1L;        @Id      @GeneratedValue(strategy = GenerationType.IDENTITY)      private Long id;        private String name;        private String password;        private Integer age;        private Integer sex;  }  

repository接口:

package com.yizhu.repository;    import com.yizhu.entity.User;    public interface UserRepository extends JpaRepository<User, Long>{  }

下面看看jpa為我們提供了哪些默認操作單表數據的方法

package org.springframework.data.jpa.repository;  @NoRepositoryBean  public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {      List<T> findAll(); // 查詢全表數據        List<T> findAll(Sort var1); // 查詢全表數據,支持排序        List<T> findAllById(Iterable<ID> var1); // 根據id字段查詢所有匹配數據        <S extends T> List<S> saveAll(Iterable<S> var1); // 批量保存或更新數據        void flush(); // 刷新本地緩存到數據庫        <S extends T> S saveAndFlush(S var1); // 保存或更新單挑數據及刷新本地緩存到數據庫        void deleteInBatch(Iterable<T> var1); // 批量刪除數據        void deleteAllInBatch(); // 批量刪除全表數據        T getOne(ID var1); // 根據id查詢一條匹配數據        <S extends T> List<S> findAll(Example<S> ar1); // Example方式的查詢指定實體        <S extends T> List<S> findAll(Example<S> var1, Sort var2);  // Example方式的查詢指定實體並排序  }
package org.springframework.data.repository;  @NoRepositoryBean  public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {      Iterable<T> findAll(Sort var1);  // 根據排序查詢全表,返回類型是任意集合        Page<T> findAll(Pageable var1); // 根據分頁參數分頁查詢  }
package org.springframework.data.repository;  @NoRepositoryBean  public interface CrudRepository<T, ID> extends Repository<T, ID> {      <S extends T> S save(S var1);   // 保存或更新單條數據        <S extends T> Iterable<S> saveAll(Iterable<S> var1);  // 批量保存或更新        Optional<T> findById(ID var1);  // 根據id查詢數據,返回類型是Optional        boolean existsById(ID var1);    // 根據id判斷數據是否存在        Iterable<T> findAll(); // 查詢全表數據,返回類型為集合        Iterable<T> findAllById(Iterable<ID> var1); // 根據id集合查詢數據        long count();   // 統計全表數據量        void deleteById(ID var1); // 根據id刪除數據        void delete(T var1);    // 刪除單條數據        void deleteAll(Iterable<? extends T> var1);     // 刪除指定集合數據        void deleteAll();   // 刪除全表數據  }
package org.springframework.data.repository.query;  public interface QueryByExampleExecutor<T> {      <S extends T> Optional<S> findOne(Example<S> var1); // 根據Example查詢一條        <S extends T> Iterable<S> findAll(Example<S> var1); // 根據Example查詢所有數據        <S extends T> Iterable<S> findAll(Example<S> var1, Sort var2);  // 根據Example查詢所有數據,並排序        <S extends T> Page<S> findAll(Example<S> var1, Pageable var2);  // 根據Example分頁查詢        <S extends T> long count(Example<S> var1);  // 根據Example統計        <S extends T> boolean exists(Example<S> var1);  // 根據Example判斷數據是否存在  }

除此之外,jpa提供了一套新的生成sql的機制,非常方便好用,jpa根據Repository接口的方法中的關鍵字、實體字段及出入參,自動生成sql,這種方式啟動容器的時候就可以檢查語法是否正確,簡單使用例子如下:

package com.yizhu.repository;    import com.yizhu .entity.User;  import org.springframework.data.jpa.repository.EntityGraph;  import org.springframework.data.jpa.repository.JpaRepository;  import org.springframework.data.jpa.repository.JpaSpecificationExecutor;  import org.springframework.data.jpa.repository.Query;  import org.springframework.data.repository.query.Param;    import java.util.List;    public interface UserRepository extends JpaRepository<User, Long>{        /**       * 根據年齡查詢用戶信息       * @param age       * @return       */      List<User> findAllByAge(Integer age);        /**       * 根據用戶性別和所屬組織名稱查詢用戶信息       * @param userSex       * @param orgName       * @return       */      List<User> findBySexAndOrg(@Param("sex") Integer sex, @Param("name") String name);        /**       * 根據用戶名模糊查詢       * @return       */      List<User> findAllByNameLike(@Param("name") String name);  }

除了findByAnd之外,還有一些關鍵詞,全部定義在PartTreePart類,組裝起來可以生成各種各樣的sql,下面截取部分代碼,感興趣的同學可以打開源碼去閱讀

package org.springframework.data.repository.query.parser;  public class PartTree implements Streamable<PartTree.OrPart> {      private static final String KEYWORD_TEMPLATE = "(%s)(?=(\p{Lu}|\P{InBASIC_LATIN}))";      private static final String QUERY_PATTERN = "find|read|get|query|stream";      private static final String COUNT_PATTERN = "count";      private static final String EXISTS_PATTERN = "exists";      private static final String DELETE_PATTERN = "delete|remove";      private static final Pattern PREFIX_TEMPLATE = Pattern.compile("^(find|read|get|query|stream|count|exists|delete|remove)((\p{Lu}.*?))??By");      private final PartTree.Subject subject;      private final PartTree.Predicate predicate;      ...      private static String[] split(String text, String keyword) {          Pattern pattern = Pattern.compile(String.format("(%s)(?=(\p{Lu}|\P{InBASIC_LATIN}))", keyword));          return pattern.split(text);      }        private static class Predicate implements Streamable<PartTree.OrPart> {          private static final Pattern ALL_IGNORE_CASE = Pattern.compile("AllIgnor(ing|e)Case");          private static final String ORDER_BY = "OrderBy";          private final List<PartTree.OrPart> nodes;          private final OrderBySource orderBySource;          private boolean alwaysIgnoreCase;            public Predicate(String predicate, Class<?> domainClass) {              String[] parts = PartTree.split(this.detectAndSetAllIgnoreCase(predicate), "OrderBy");              if (parts.length > 2) {                  throw new IllegalArgumentException("OrderBy must not be used more than once in a method name!");              } else {                  this.nodes = (List)Arrays.stream(PartTree.split(parts[0], "Or")).filter(StringUtils::hasText).map((part) -> {                      return new PartTree.OrPart(part, domainClass, this.alwaysIgnoreCase);                  }).collect(Collectors.toList());                  this.orderBySource = parts.length == 2 ? new OrderBySource(parts[1], Optional.of(domainClass)) : OrderBySource.EMPTY;              }          }          ...      }        private static class Subject {          private static final String DISTINCT = "Distinct";          private static final Pattern COUNT_BY_TEMPLATE = Pattern.compile("^count(\p{Lu}.*?)??By");          private static final Pattern EXISTS_BY_TEMPLATE = Pattern.compile("^(exists)(\p{Lu}.*?)??By");          private static final Pattern DELETE_BY_TEMPLATE = Pattern.compile("^(delete|remove)(\p{Lu}.*?)??By");          private static final String LIMITING_QUERY_PATTERN = "(First|Top)(\d*)?";          private static final Pattern LIMITED_QUERY_TEMPLATE = Pattern.compile("^(find|read|get|query|stream)(Distinct)?(First|Top)(\d*)?(\p{Lu}.*?)??By");          private final boolean distinct;          private final boolean count;          private final boolean exists;          private final boolean delete;          private final Optional<Integer> maxResults;            public Subject(Optional<String> subject) {              this.distinct = (Boolean)subject.map((it) -> {                  return it.contains("Distinct");              }).orElse(false);              this.count = this.matches(subject, COUNT_BY_TEMPLATE);              this.exists = this.matches(subject, EXISTS_BY_TEMPLATE);              this.delete = this.matches(subject, DELETE_BY_TEMPLATE);              this.maxResults = this.returnMaxResultsIfFirstKSubjectOrNull(subject);          }            private Optional<Integer> returnMaxResultsIfFirstKSubjectOrNull(Optional<String> subject) {              return subject.map((it) -> {                  Matcher grp = LIMITED_QUERY_TEMPLATE.matcher(it);                  return !grp.find() ? null : StringUtils.hasText(grp.group(4)) ? Integer.valueOf(grp.group(4)) : 1;              });          }          ...            private boolean matches(Optional<String> subject, Pattern pattern) {              return (Boolean)subject.map((it) -> {                  return pattern.matcher(it).find();              }).orElse(false);          }      }  }
package org.springframework.data.repository.query.parser;  public class Part {      private static final Pattern IGNORE_CASE = Pattern.compile("Ignor(ing|e)Case");      private final PropertyPath propertyPath;      private final Part.Type type;      private Part.IgnoreCaseType ignoreCase;      ...      public static enum Type {          BETWEEN(2, new String[]{"IsBetween", "Between"}),          IS_NOT_NULL(0, new String[]{"IsNotNull", "NotNull"}),          IS_NULL(0, new String[]{"IsNull", "Null"}),          LESS_THAN(new String[]{"IsLessThan", "LessThan"}),          LESS_THAN_EQUAL(new String[]{"IsLessThanEqual", "LessThanEqual"}),          GREATER_THAN(new String[]{"IsGreaterThan", "GreaterThan"}),          GREATER_THAN_EQUAL(new String[]{"IsGreaterThanEqual", "GreaterThanEqual"}),          BEFORE(new String[]{"IsBefore", "Before"}),          AFTER(new String[]{"IsAfter", "After"}),          NOT_LIKE(new String[]{"IsNotLike", "NotLike"}),          LIKE(new String[]{"IsLike", "Like"}),          STARTING_WITH(new String[]{"IsStartingWith", "StartingWith", "StartsWith"}),          ENDING_WITH(new String[]{"IsEndingWith", "EndingWith", "EndsWith"}),          IS_NOT_EMPTY(0, new String[]{"IsNotEmpty", "NotEmpty"}),          IS_EMPTY(0, new String[]{"IsEmpty", "Empty"}),          NOT_CONTAINING(new String[]{"IsNotContaining", "NotContaining", "NotContains"}),          CONTAINING(new String[]{"IsContaining", "Containing", "Contains"}),          NOT_IN(new String[]{"IsNotIn", "NotIn"}),          IN(new String[]{"IsIn", "In"}),          NEAR(new String[]{"IsNear", "Near"}),          WITHIN(new String[]{"IsWithin", "Within"}),          REGEX(new String[]{"MatchesRegex", "Matches", "Regex"}),          EXISTS(0, new String[]{"Exists"}),          TRUE(0, new String[]{"IsTrue", "True"}),          FALSE(0, new String[]{"IsFalse", "False"}),          NEGATING_SIMPLE_PROPERTY(new String[]{"IsNot", "Not"}),          SIMPLE_PROPERTY(new String[]{"Is", "Equals"});            private static final List<Part.Type> ALL = Arrays.asList(IS_NOT_NULL, IS_NULL, BETWEEN, LESS_THAN, LESS_THAN_EQUAL, GREATER_THAN, GREATER_THAN_EQUAL, BEFORE, AFTER, NOT_LIKE, LIKE, STARTING_WITH, ENDING_WITH, IS_NOT_EMPTY, IS_EMPTY, NOT_CONTAINING, CONTAINING, NOT_IN, IN, NEAR, WITHIN, REGEX, EXISTS, TRUE, FALSE, NEGATING_SIMPLE_PROPERTY, SIMPLE_PROPERTY);          public static final Collection<String> ALL_KEYWORDS;          private final List<String> keywords;          private final int numberOfArguments;          ...          static {              List<String> allKeywords = new ArrayList();              Iterator var1 = ALL.iterator();                while(var1.hasNext()) {                  Part.Type type = (Part.Type)var1.next();                  allKeywords.addAll(type.keywords);              }                ALL_KEYWORDS = Collections.unmodifiableList(allKeywords);          }      }  }

可以看到單表操作的大部分需求jpa都為我們提供了現成的實現,但也支持我們使用@Query註解自定義查詢sql,方便有sql基礎的同學使用,sql可控性強。

package com.yizhu.repository;    import com.yizhu .entity.User;  import org.springframework.data.jpa.repository.EntityGraph;  import org.springframework.data.jpa.repository.JpaRepository;  import org.springframework.data.jpa.repository.JpaSpecificationExecutor;  import org.springframework.data.jpa.repository.Query;  import org.springframework.data.repository.query.Param;    import java.util.List;    public interface UserRepository extends JpaRepository<User, Long>{        /**       * 查詢所有用戶信息       * @return       */      @Query(value = "from User u")      List<User> findAll();        /**       * 根據年齡查詢用戶信息       * @param age       * @return       */      @Query(value = "select * from t_user u where u.user_age = ?1", nativeQuery = true)      List<User> findAllByAge(Integer age);        /**       * 根據用戶性別和所屬組織名稱查詢用戶信息       * @param userSex       * @param orgName       * @return       */      @Query(value = "select u from User u left join u.org o where u.userSex = :userSex and o.orgName = :orgName")      List<User> findUsersBySexAndOrg(@Param("userSex") Integer userSex, @Param("orgName") String orgName);    }

多表關聯

@OneToOne@OneToMany@ManyToOne@ManyToMany

@Entity  @Table(name = "t_user")  @NamedEntityGraph(name = "User.findUsers", attributeNodes = {@NamedAttributeNode("jobs"), @NamedAttributeNode("roles")})  @Data  @Builder  @NoArgsConstructor  @AllArgsConstructor  public class User implements Serializable {        private static final long serialVersionUID = 1L;        @ApiModelProperty(hidden = true)      @Id      @GeneratedValue(strategy = GenerationType.IDENTITY)      private Long id;        @ApiModelProperty(value = "用戶名")      @Column(name = "user_name")      private String name;        @ApiModelProperty(value = "用戶密碼")      @Column(name = "user_password")      private String password;        @ApiModelProperty(value = "用戶年齡")      @Column(name = "user_age")      private Integer age;        @ApiModelProperty(value = "用戶性別")      @Column(name = "user_sex")      private Integer sex;        @ApiModelProperty(value = "所屬組織id")      @Column(name = "org_id")      private Long orgId;        @ApiModelProperty(value = "用戶信息")      @OneToOne      @JoinColumn(name = "id", updatable = false, insertable = false)      private UserInfo userInfo;        @ApiModelProperty(value = "用戶所屬組織")      @ManyToOne      @JoinColumn(name = "org_id", updatable = false, insertable = false)      private Organization org;        @ApiModelProperty(value = "用戶角色")      @OneToMany      @JoinColumn(name = "user_id", referencedColumnName = "id", insertable = false, updatable = false)      @NotFound(action = NotFoundAction.IGNORE)      private Set<Role> roles;        @ApiModelProperty(value = "用戶工作")      @ManyToMany      @JoinTable(              name = "t_user_job",              joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),              inverseJoinColumns = @JoinColumn(name = "job_id", referencedColumnName = "id")      )      @NotFound(action = NotFoundAction.IGNORE)      private Set<Job> jobs;

private Set<Role> roles;private Set<Job> jobs;不能用同時使用List集合代替,會報錯org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: [com.yizhu.entity.User.jobs, com.yizhu.entity.User.roles]

動態查詢

package com.yizhu.repository;    import com.yizhu.dto.UserQueryDto;  import com.yizhu.entity.Organization;  import com.yizhu.entity.User;  import org.springframework.data.jpa.domain.Specification;    import javax.persistence.criteria.Join;  import javax.persistence.criteria.JoinType;  import javax.persistence.criteria.Predicate;  import java.util.ArrayList;  import java.util.List;  import java.util.Optional;    public class UserSpecs {        public static Specification<User> listQuerySpec(UserQueryDto userQueryDto){          return (root, query, builder) -> {              List<Predicate> predicates = new ArrayList<>();                Optional.ofNullable(userQueryDto.getId()).ifPresent(i -> predicates.add(builder.equal(root.get("id"), i)));              Optional.ofNullable(userQueryDto.getName()).ifPresent(n -> predicates.add(builder.equal(root.get("name"), n)));              Optional.ofNullable(userQueryDto.getAge()).ifPresent(a -> predicates.add(builder.equal(root.get("age"), a)));              Optional.ofNullable(userQueryDto.getOrgId()).ifPresent(oi -> predicates.add(builder.equal(root.get("orgId"), oi)));              Optional.ofNullable(userQueryDto.getOrgName()).ifPresent(on -> {                  Join<User, Organization> userJoin = root.join(root.getModel().getSingularAttribute("org", Organization.class), JoinType.LEFT);                  predicates.add(builder.equal(userJoin.get("orgName"), on));              });                return builder.and(predicates.toArray(new Predicate[predicates.size()]));          };      }  }
package com.yizhu.service;    import com.yizhu.dto.UserQueryDto;  import com.yizhu.entity.User;  import com.yizhu.repository.UserRepository;  import com.yizhu.repository.UserSpecs;  import org.springframework.beans.factory.annotation.Autowired;  import org.springframework.stereotype.Service;    import java.util.List;    @Service  public class UserService {        @Autowired      private UserRepository userRepository;        public List<User> findUsersDynamic(UserQueryDto userQueryDto){          return userRepository.findAll(UserSpecs.listQuerySpec(userQueryDto));      }  }

審計功能使用

在啟動類添加@EnableJpaAuditing註解表示開啟jpa審計功能

package com.yizhu;    import org.springframework.boot.SpringApplication;  import org.springframework.boot.autoconfigure.SpringBootApplication;  import org.springframework.data.jpa.repository.config.EnableJpaAuditing;    @EnableJpaAuditing  @SpringBootApplication  public class DemoApplication {        public static void main(String[] args) {          SpringApplication.run(DemoApplication.class, args);      }  }

在需要使用審計功能的實體類添加@EntityListeners(AuditingEntityListener.class)註解

package com.yizhu.entity;    import lombok.Builder;  import lombok.Data;  import org.springframework.data.jpa.domain.support.AuditingEntityListener;    import javax.persistence.*;  import java.io.Serializable;  import java.util.Date;    @Entity  @Table(name = "t_role")  @Data  @Builder  @EntityListeners(AuditingEntityListener.class)  public class Role implements Serializable {      private static final long serialVersionUID=1L;        @ApiModelProperty(hidden = true)      @Id      @GeneratedValue(strategy = GenerationType.AUTO)      private Long id;        private String roleName;        @CreatedDate      private Date createTime;        @CreatedBy      private Long createId;        @LastModifiedDate      private Date updateTime;        @LastModifiedBy      private Long updateId;  }

實現AuditorAware接口,告訴容器當前登錄人id

package com.yizhu.configuration;    import org.springframework.data.domain.AuditorAware;  import org.springframework.web.context.request.RequestContextHolder;  import org.springframework.web.context.request.ServletRequestAttributes;    import java.util.Optional;    public class UserAuditorAwareImpl implements AuditorAware<Long> {      @Override      public Optional<Long> getCurrentAuditor() {          // 從session中獲取登錄人id          ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();          Long userId = (Long)servletRequestAttributes.getRequest().getSession().getAttribute("userId");          return Optional.of(userId);      }  }

ok,然後jpa就會根據IDVersion判斷當前操作是更新還是新增數據,新增時會注入當前登錄人id到標有@CreateBy註解的字段上,當前時間注入到標有@CreateTime註解字段上;更新時則注入到@LastModifiedBy@LastModifiedDate對應的字段上。想詳細了解的可查看org.springframework.data.jpa.domain.support.AuditingEntityListener源碼。

常見的坑

  • N+1問題,當使用@ManyToMany@ManyToOne@OneToMany@OneToOne關聯
    關係的時候,FetchType怎麼配置LAZY或者EAGER。SQL真正執行的時
    候是由一條主表查詢和N條子表查詢組成的。這種查詢效率一般比較
    低下,比如子對象有N個就會執行N+1條SQL。使用JPA 2.1推出來的@EntityGraph、@NamedEntityGraph可以解決該問題。如下。
@ApiModel  @Entity  @Table(name = "t_user")  @NamedEntityGraph(name = "User.findUsers", attributeNodes = {@NamedAttributeNode("jobs"), @NamedAttributeNode("roles")})  @Data  @Builder  @NoArgsConstructor  @AllArgsConstructor  public class User implements Serializable {        private static final long serialVersionUID = 1L;        @ApiModelProperty(hidden = true)      @Id      @GeneratedValue(strategy = GenerationType.IDENTITY)      private Long id;        // 省略其他屬性  }
package com.yizhu.repository;    import com.yizhu.entity.User;  import org.springframework.data.jpa.repository.EntityGraph;  import org.springframework.data.jpa.repository.JpaRepository;  import org.springframework.data.jpa.repository.JpaSpecificationExecutor;  import org.springframework.data.jpa.repository.Query;  import org.springframework.data.repository.query.Param;  import java.util.List;    public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {        /**       * 根據id查詢用戶信息       * @param id       * @return       */      @EntityGraph(value = "User.findUsers", type = EntityGraph.EntityGraphType.FETCH)      User findAllById(Long id);        /**       * 根據name查詢用戶信息       * @param name       * @return       */      @EntityGraph(value = "User.findUsers", type = EntityGraph.EntityGraphType.FETCH)      @Query(value = "select * from t_user where user_name = :name", nativeQuery = true)      List<User> findAllByUserName(@Param("name") String name);  }
  • 所有的註解要麼全配置在字段上,要麼全配置在get方法上,不能混用,混用就會啟動不起來,但是語法配置沒有問題。
  • 所有的關聯都是支持單向關聯和雙向關聯的,視具體業務場景而定。JSON序列化的時候使用雙向註解會產生死循環,需要人為手動轉化一次,或者使用@JsonIgnore。
  • 在所有的關聯查詢中,表一般是不需要建立外鍵索引的。@mappedBy的使用需要注意。
  • 級聯刪除比較危險,建議考慮清楚,或者完全掌握。
  • 不同的關聯關係的配置,@JoinClumn裏面的name、referencedColumnName代表的意思是不一樣的,很容易弄混,可以根據打印出來的SQL做調整。
  • 當配置這些關聯關係的時候建議大家直接在表上面,把外鍵建好,然後通過後面我們介紹的開發工具直接生成,這樣可以減少自己調試的時間。

JPA常用註解

摘自《Spring Data JPA從入門到精通》

1554619508054

1554619555135

1554619583326

1554619600777

1554619615592

1554619631450

1554620031094

1554620213902

1554620262086

更多信息可以關注我的個人博客:逸竹小站逸竹小站