Spring-Data-Jpa使用總結
- 2019 年 10 月 17 日
- 筆記
參考資源列表
- 官方文檔:https://docs.spring.io/spring-data/jpa/docs/2.1.5.RELEASE/reference/html/
- 《Spring Data JPA入門到精通》
前言
JPA
是Java Persistence API
的簡稱,是Spring
在Hibernate
的基礎上進行的二次封裝框架,為了更好更方便的融入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); }
除了find
、By
、And
之外,還有一些關鍵詞,全部定義在PartTree
、Part
類,組裝起來可以生成各種各樣的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就會根據ID
和Version
判斷當前操作是更新還是新增數據,新增時會注入當前登錄人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從入門到精通》