<input id="ohw05"></input>
  • <table id="ohw05"><menu id="ohw05"></menu></table>
  • <var id="ohw05"></var>
  • <code id="ohw05"><cite id="ohw05"></cite></code>
    <label id="ohw05"></label>
    <var id="ohw05"></var>
  • Spring Data JPA系列3:JPA項目中核心場景與進階用法介紹

    大家好,又見面了。

    到這里呢,已經是本SpringData JPA系列文檔的第三篇了,先來回顧下前面兩篇:

    本篇內容將在上一篇已有的內容基礎上,進一步的聊一下項目中使用JPA的一些高階復雜場景的實踐指導,覆蓋了主要核心的JPA使用場景,可以讓你在需求開發的時候對JPA的使用更加的游刃有余。

    Repository

    上一篇文檔中,我們知道業務代碼中直接調用Repository層中默認提供的方法或者是自己自定義的接口方法,便可以進行DB的相關操作。這里我們再對repository的整體實現情況進一步探索下。

    repository全貌梳理

    先看下Repository相關的類圖:

    整體類圖雖然咋看上去很龐雜,但其實主線脈絡還是比較清晰的。

    • 先看下藍色的部分其實就是Repository的一整個接口定義鏈條,而橙色的則是我們自己自定義的一些Repository接口類,繼承父層接口的所有已有能力。
    • 左側的類圖與接口,其實都是JPA提供的一些用于實現或者定制查詢操作的一些輔助實現類,后面章節中會看到他們的身影。

    對主體repository層級提供的主要方法進行簡單的梳理,如下:

    下面對各個repository接口進行簡單的獨立介紹。

    JpaRepository與它的父類們

    • Repository位于Spring Data Common的lib里面,是Spring Data 里面做數據庫操作的最底層的抽象接口、最頂級的父類,源碼里面其實什么方法都沒有,僅僅起到一個標識作用。
    • CrudRepository作為直接繼承Repository的次頂層接口類,看名字也可以大致猜測出其主要作用就是封裝提供基礎CRUD操作。
    • PagingAndSortingRepository繼承自CrudRepository,自然也就具備了CrudRepository提供的全部接口能力。此外,從其自身新提供的接口來看,增加了排序和分頁查詢列表的能力,非常符合其類名的含義。

    JpaRepository與其前面的幾個父類相比是個特殊的存在,其中補充添加了一組JPA規范的接口方法。前面的幾個接口類都是Spring Data為了兼容NoSQL而進行的一些抽象封裝(因為SpringData項目是一個龐大的家族,支持各種SQL與NoSQL的數據庫,SpringData JPA是SpringData家族中面向SQL數據庫的一個子分支項目),從JpaRepository開始是對關系型數據庫進行抽象封裝。

    從類圖可以看得出來它繼承了PagingAndSortingRepository類,也就繼承了其所有方法,并且實現類也是SimpleJpaRepository。從類圖上還可以看出JpaRepository繼承和擁有了QueryByExampleExecutor的相關方法。

    通過源碼和CrudRepository相比較,它支持Query By Example,批量刪除,提高刪除效率,手動刷新數據庫的更改方法,并將默認實現的查詢結果變成了List。

    額外補充一句:

    實際的項目編碼中,大部分的場景中,我們自定義Repository都是繼承JpaRepository來實現的。

    自定義Repository

    先看個自定義Repository的例子,如下:

    看下對應類圖結構,自定義Repository繼承了JpaRepository,具備了其父系所有的操作接口,此外,額外擴展了業務層面自定義的一些接口方法:

    自定義Repository的時候,繼承JpaRepository需要傳入兩個泛型:

    • 此Repository需要操作的具體Entity對象(Entity與具體DB中表映射,所以指定Entity也等同于指定了此Repository所對應的目標操作Table),
    • 此Entity實體的主鍵數據類型(也就是第一個參數指定的Entity類中以@Id注解標識的字段的類型)

    分頁、排序,一招搞定

    分頁,排序使用Pageable對象進行傳遞,其中包含PageSort參數對象。

    查詢的時候,直接傳遞Pageable參數即可(注意下,如果是用原生SQL查詢的方式,此法行不通,后文有詳細說明)。

    
    // 定義repository接口的時候,直接傳入Pageable參數即可
    List<UserEntity> findAllByDepartment(DepartmentEntity department, Pageable pageable);
    
    

    還有一種特殊的分頁場景。比如,DB表中有100w條記錄,然后現在需要將這些數據全量的加載到ES中。如果逐條查詢然后插入ES,顯然效率太慢;如果一次性全部查詢出來然后直接往ES寫,服務端內存可能會爆掉。

    這種場景,其實可以基于Slice結果對象進行實現。Slice的作用是,只知道是否有下一個Slice可用,不會執行count,所以當查詢較大的結果集時,只知道數據是足夠的就可以了,而且相關的業務場景也不用關心一共有多少頁。

    
    private <T extends EsDocument, F> void fullLoadToEs(IESLoadService<T, F> esLoadService) {
        try {
            final int batchHandleSize = 10000;
            Pageable pageable = PageRequest.of(0, batchHandleSize);
            do {
                // 批量加載數據,返回Slice類型結果
                Slice<F> entitySilce = esLoadService.slicePageQueryData(pageable);
    
                // 具體業務處理邏輯
                List<T> esDocumentData = esLoadService.buildEsDocumentData(entitySilce);
                esUtil.batchSaveOrUpdateAsync(esDocumentData);
    
                // 獲取本次實際上加載到的具體數據量
                int pageLoadedCount = entitySilce.getNumberOfElements();
                if (!entitySilce.hasNext()) {
                    break;
                }
    
                // 自動重置page分頁參數,繼續拉取下一批數據
                pageable = entitySilce.nextPageable();
            } while (true);
        } catch (Exception e) {
            log.error("error occurred when load data into es", e);
        }
    }
    
    

    復雜搜索,其實不復雜

    按照條件進行搜索查詢,是項目中遇到的非常典型且常用的場景。但是條件搜索也分幾種場景,下面分開說下。

    簡單固定場景

    所謂簡單固定,即查詢條件就是固定的1個字段或者若干個字段,且查詢字段數量不會變,比如根據部門查詢具體人員列表這種。
    這種情況,我們可以簡單的直接在repository中,根據命名規范定義一個接口即可。

    
    @Repository
    public interface UserRepository extends JpaRepository<UserEntity, Long> {
        // 根據一個固定字段查詢
        List<UserEntity> findAllByDepartment(DepartmentEntity department);
        // 根據多個固定字段組合查詢
        UserEntity findFirstByWorkIdAndUserNameAndDepartment(String workId, String userName, DepartmentEntity department);
    }
    
    

    簡單不固定場景

    考慮一種場景,界面上需要做一個用戶搜索的能力,要求支持根據用戶名、工號、部門、性別、年齡、職務等等若干個字段中的1個或者多個的組合來查詢符合條件的用戶信息。
    顯然,上述通過直接在repository中按照命名規則定義接口的方式行不通了。這個時候,Example對象便排上用場了。

    其實在前面整體介紹Repository的UML圖中,就已經有了Example的身影了,雖然這個名字起的很敷衍,但其功能確是挺實在的。

    看下具體用法:

    
    public Page<UserEntity> queryUsers(Request request, UserEntity queryParams) {
        // 查詢條件構造出對應Entity對象,轉為Example查詢條件
        Example<UserEntity> example = Example.of(queryParams);
        // 構造分頁參數
        Pageable pageable = PageHelper.buildPageable(request);
        
        // 按照條件查詢,并分頁返回結果
        return userRepository.findAll(example, pageable);
    }
    
    

    復雜場景

    如果是一些自定義的復雜查詢場景,可以通過定制SQL語句的方式來實現。

    
    @Repository
    public interface UserRepository extends JpaRepository<UserEntity, Long> {
        @Query(
            value = "select t.*,(select group_concat(a.assigner_name) from workflow_task a where a.state='R' and a.proc_inst_id=t.proc_inst_id) deal_person,"
                + " (select a.task_name from workflow_task a where a.state='R' and a.proc_inst_id=t.proc_inst_id limit 1) cur_step "
                + "   from workflow_info t where t.state='R'  and t.type in (?1) "
                + "and exists(select 1 from workflow_task b where b.assigner=?2 and b.state='R' and b.proc_inst_id=t.proc_inst_id) order by t.create_time desc",
            countQuery = "select count(1) from workflow_info t where t.state='R'  and t.type in (?1) "
                + "and exists(select 1 from workflow_task b where b.assigner=?2 and b.state='R' and b.proc_inst_id=t.proc_inst_id) ",
            nativeQuery = true)
        Page<FlowResource> queryResource(List<String> type, String workId, Pageable pageable);
    }
    
    

    此外,還可以基于JpaSpecificationExecutor提供的能力接口來實現。
    自定義接口需要增加JpaSpecificationExecutor的繼承,然后利用Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);接口來實現復雜查詢能力。

    
    // 增加對JpaSpecificationExecutor的繼承
    @Repository
    public interface UserRepository extends JpaRepository<UserEntity, Long>, JpaSpecificationExecutor<UserEntity> {
    
    }
    
    
    
    public List<UserEntity> queryUsers(QueryParams queryParams) {
        // 構造Specification查詢條件
        Specification<UserEntity> specification =
            (root, query, cb) -> {
                List<Predicate> predicates = new ArrayList<>();
                // 范圍查詢條件構造
                predicates.add(cb.greaterThanOrEqualTo(root.get("age"), queryParams.getMinAge()));
                predicates.add(cb.lessThanOrEqualTo(root.get("age"), queryParams.getMaxAge()));
                // 精確匹配查詢條件構造
                predicates.add(cb.equal(root.get("department"), queryParams.getDepartment()));
                // 關鍵字模糊匹配條件構造
                if (Objects.nonNull(queryParams.getNameKeyword())) {
                    predicates.add(cb.like(root.get("userName"), "%" + queryParams.getNameKeyword() + "%"));
                }
                return query.where(predicates.toArray(new Predicate[0])).getRestriction();
            };
        // 執行復雜查詢條件
        return userRepository.findAll(specification);
    }
    
    

    自定義Listener,玩出花樣

    實際項目中,經常會有一種場景,就是需要監聽某個數據的變更然后做一些額外的處理邏輯。一種邏輯,是寫操作的時候順便調用下相關業務的處理API,這樣會造成業務間耦合加深;優化點的策略是搞個MQ隊列,然后在這個寫DB操作的同時發個消息到MQ里面,然后一堆的consumer會監聽MQ并去做對應的處理邏輯,這樣引入個消息隊列代價也有點高。

    這個時候,我們可以借助JPA的自定義EntityListener功能來完美解決。通過監聽某個Entity表的變更情況,通知或者調用相關其他的業務代碼處理,完美實現了與主體業務邏輯的解耦,也無需引入其他組件。

    舉個例子:現有一個論壇發帖系統,發帖Post和評論Comment屬于兩個相對獨立又有點關系的數據,現在需要檢測當評論變化的時候,需要更新下Post對應記錄的評論數字段。下面演示下具體實現。

    • 首先,定制一個Listener類,并指定Callbacks注解
    
    public class CommentCountAuditListener {
        /**
         *  當Comment表有新增數據的操作時,觸發此方法的調用
         */
        @PostPersist
        public void postPersist(CommentEntity entity) {
            // 執行Post表中評論數字段的更新
            // do something here...
        }
    
        /**
         *  當Comment表有刪除數據的操作時,觸發此方法的調用
         */
        @PostRemove
        public void postRemove(CommentEntity entity) {
            // 執行Post表中評論數字段的更新
            // do something here...
        }
    
        /**
         *  當Comment表有更新數據的操作時,觸發此方法的調用
         */
        @PostUpdate
        public void postUpdate(CommentEntity entity) {
            // 執行Post表中評論數字段的更新
            // do something here...
        }
        
    }
    
    
    • 其次,在評論實體CommentEntity上,加上自定義Listener信息
    
    @Entity
    @Table("t_comment")
    // 指定前面定制的Listener
    @EntityListeners({CommentCountAuditListener.class})
    public class CommentEntity extends AbstractAuditable {
        // ...
    }
    
    

    這樣就搞定了。

    自定義Listener還有個典型的使用場景,就是可以統一的記錄DB數據的操作日志。

    定制化SQL,隨心所欲

    JPA提供@Query注解,可以實現自定義SQL語句的能力。比如:

    
    @Query(value = "select * from user " +
            "where work_id in (?1) " +
            "and department_id = 0 " +
            "order by CREATE_TIME desc ",
            nativeQuery = true)
    List<OssFileInfoEntity> queryUsersByWorkIdIn(List<String> workIds);
    
    

    如果需要執行寫操作SQL的時候,需要額外增加@Modifying注解標識,如下:

    
    @Modifying
    @Query(value = "insert into user (work_id, user_name) values (?1, ?2)",
            nativeQuery = true)
    int createUser(String workId, String userName);
    
    

    其中,nativeQuery = true表示@Query注解中提供的value值為原生SQL語句。如果nativeQuery未設置或者設置為false,則表示將使用JPQL語言來執行。所謂JPQL,即JAVA持久化查詢語句,是一種類似SQL的語法,不同點在于其使用類名來替代表名,使用類字段來替代表字段名。比如:

    
    @Query("SELECT u FROM com.vzn.demo.UserInfo u WHERE u.userName = ?1")
    public UserInfo getUserInfoByName(String name);
    
    

    幾個關注點要特別闡述下:

    • like查詢的時候,參數前后的%需要手動添加,系統是不會自動加上的
    
    // like 需要手動添加百分號
    @Query("SELECT u FROM com.vzn.demo.UserInfo u WHERE u.userName like %?1")
    public UserInfo getUserInfoByName(String name);
    
    
    • 使用nativeQuery=true查詢的時候(原生SQL方式),不支持API接口里面傳入Sort對象然后進行混合執行
    
    // 錯誤示范:  自定義sql與API中Sort參數不可同時混用
    @Query("SELECT * FROM t_user u WHERE u.user_name = ?1", nativeQuery=true)
    public UserInfo getUserInfoByName(String name, Sort sort);
    
    
    // 正確示范:  自定義SQL完成對應sort操作
    @Query("SELECT * FROM t_user u WHERE u.user_name = ?1 order by ?2", nativeQuery=true)
    public UserInfo getUserInfoByName(String name, String sortColumn);
    
    
    • 未指定nativeQuery=true查詢的時候(JPQL方式),支持API接口里面傳入SortPageRequest等對象然后進行混合執行,來完成排序、分頁等操作
    
    // 正確:自定義jpql與API中Sort參數不可同時混用
    @Query("SELECT u FROM com.vzn.demo.UserInfo u WHERE u.userName = ?1")
    public UserInfo getUserInfoByName(String name, Sort sort);
    
    
    • 支持使用參數名作為@Query查詢中的SQL或者JPQL語句的入參,取代參數順序占位符

    默認情況下,參數是通過順序綁定在自定義執行語句上的,這樣如果API接口傳參順序或者位置改變,極易引起自定義查詢傳參出問題,為了解決此問題,我們可以使用@Param注解來綁定一個具體的參數名稱,然后以參數名稱的形式替代位置順序占位符,這也是比較推薦的一種做法。

    
    // 默認的順序位置傳參
    @Query("SELECT * FROM t_user u WHERE u.user_name = ?1 order by ?2", nativeQuery=true)
    public UserInfo getUserInfoByName(String name, String sortColumn);
    
    // 使用參數名稱傳參
    @Query("SELECT * FROM t_user u WHERE u.user_name = :name order by :sortColumn", nativeQuery=true)
    public UserInfo getUserInfoByName(@Param("name") String name, @Param("sortColumn") String sortColumn);
    
    

    字段命名映射策略

    一般而言,JAVA的編碼規范都要求filed字段命名需要遵循小駝峰命名的規范,比如userName,而DB中column命名的時候,很多人習慣于使用下劃線分隔的方式命名,比如user_name這種。這樣就涉及到一個映射的策略問題,需要讓JPA知道代碼里面的userName就對應著DB中的user_name

    這里就會涉及到對命名映射策略的映射。主要有兩種映射配置,下面分別闡述下。

    • implicit-strategy

    配置項key值:

    spring.jpa.hibernate.naming.implicit-strategy=xxxxx
    

    取值說明:

    映射規則說明
    org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImp 默認的命名策略,兼容JPA2.0規范
    org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyHbmImpl 兼容老版本Hibernate的命名規范
    org.hibernate.boot.model.naming.ImplicitNamingStrategyComponentPathImpl 與ImplicitNamingStrategyJpaCompliantImp基本相同
    org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl 兼容JPA 1.0規范中的命名規范。
    org.hibernate.boot.model.naming.SpringImplicitNamingStrategy 繼承ImplicitNamingStrategyJpaCompliantImpl,對外鍵、鏈表查詢、索引如果未定義,都有下劃線的處理策略,而table和column名字都默認與字段一樣
    • physical-strategy

    配置項key值:

    spring.jpa.hibernate.naming.physical-strategy=xxxxx
    

    取值說明:

    映射規則說明
    org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl 默認字符串一致映射,不做任何轉換處理,比如java類中userName,映射到table中列名也叫userName
    org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy java類中filed名稱小寫字母進行映射到DB表column名稱,遇大寫字母時轉為分隔符"_"命名格式,比如java類中userName字段,映射到DB表column名稱叫user_name
    • physical-strategy與implicit-strategy

    SpringData JPA只是對JPA規范的二次封裝,其底層使用的是Hibernate,所以此處涉及到Hibernate提供的一些處理策略。Hibernate將對象模型映射到關系數據庫分為兩個步驟:

    1. 從對象模型中確定邏輯名稱。邏輯名可以由用戶顯式指定(使用@Column@Table),也可以隱式指定。
    2. 將邏輯名稱映射到物理名稱,也就是數據庫中使用的名稱。

    這里,implicit-strategy用于第一步隱式指定邏輯名稱,而physical-strategy則用于第二步中邏輯名稱到物理名稱的映射。

    注意:
    當沒有使用@Table@Column注解時,implicit-strategy配置項才會被使用,即implicit-strategy定義的是一種缺省場景的處理策略;而physical-strategy屬于一種高優先級的策略,只要設置就會被執行,而不管是否有@Table@Column注解。

    小結,承上啟下

    好啦,本篇內容就介紹到這里。

    通過本篇的內容,我們對于如何在項目中使用Spring Data JPA來進行一些較為復雜場景的處理方案與策略有了進一步的了解,再結合本系列此前的內容,到此掌握的JPA的相關技能已經足以應付大部分項目開發場景。

    在實際項目中,為了保障數據操作的可靠、避免臟數據的產生,需要在代碼中加入對數據庫操作的事務控制。在下一篇文檔中,我們將一起聊一聊Spring Data JPA業務代碼開發中關于數據庫事務的控制,以及編碼中存在哪些可能會導致事務失效的場景等等。

    如果對本文有自己的見解,或者有任何的疑問或建議,都可以留言,我們一起探討、共同進步。


    補充

    Spring Data JPA作為Spring Data中對于關系型數據庫支持的一種框架技術,屬于ORM的一種,通過得當的使用,可以大大簡化開發過程中對于數據操作的復雜度。

    本文檔隸屬于《Spring Data JPA用法與技能探究》系列的第3篇。本系列文檔規劃對Spring Data JPA進行全方位的使用介紹,一共分為5篇文檔,如果感興趣,歡迎關注交流。

    《Spring Data JPA用法與技能探究》系列涵蓋內容:


    我是悟道,聊技術、又不僅僅聊技術~

    如果覺得有用,請點個關注,也可以關注下我的公眾號【架構悟道】,獲取更及時的更新。

    期待與你一起探討,一起成長為更好的自己。

    posted @ 2022-06-24 16:13  架構悟道  閱讀(227)  評論(0編輯  收藏  舉報
    国产美女a做受大片观看