1. 程式人生 > >SpringBoot第四講擴充套件和封裝Spring Data JPA(一)_自定義Repository和建立自己的BaseRepository

SpringBoot第四講擴充套件和封裝Spring Data JPA(一)_自定義Repository和建立自己的BaseRepository

這一講主要介紹Spring Data JPA的封裝。和設計相關的東西都是仁者見仁,智者見智的事情,如果你有更好的封裝方案可以和我交流,互相學習。這一講會講如下一些內容
- 擴充套件Spring Data JPA實現自己的一些特殊方法
- 建立一個自己的BaseRepository
- 封裝Specification來快速完成一些簡單的查詢操作
- 封裝分頁和排序操作。

在一些特殊時候,我們會設計到對Spring Data JPA中的方法進行重新實現,這將會面臨一個問題,如果我們新建立一個實現類。如果這個實現類實現了JpaRepository介面,這樣我們不得不實現該介面中的所有方法,如果不實現該介面,那意味著我們就無法使用Spring Data JPA中給我們提供的那些好用的方法。所以在擴充套件的時候我們需要按照如下方法進行。

Spring Data JPA 自定義方法

這些需要注意的是,介面和實現類的名稱必須遵循spring data jpa的命名規範,如果要為介面StudentBaseRepository寫自定義的介面,首先需要建立一個介面名稱為StudentBaseRepositoryCustom,這表示是自定義介面,實現類的名稱必須是StudentBaseRepositoryImpl,此時當StudentBaseRepository實現StudentBaseRepositoryCustom之後就可以使用我們自己實現的方法了,同理StudentBaseRepository也可以繼承JpaRepository來獲取Spring Data Jpa 給我們的方法。

StudentBaseRepositoryCustom程式碼如下

public interface StudentBaseRepositoryCustom {
    //基於原生態的sql進行查詢
    List<Object[]> groupByStudentAsSql();
    //基於Hibernate的HQL進行查詢
    List<Object[]> groupByStudentAsHql();
    //基於Specification的方式進行查詢,使用的是CriteriaQuery進行查詢
    List<Object[]> groupByStudentAsSpecification();
}

以上程式碼中定義了三個方法,第一個是基於原始的SQL來進行分組查詢,第二個是基於Hibernate的HQL進行查詢,最後一個是用Specification中的CriteriaQuery來進行處理,首先要解決的問題是StudentBaseRepositoryCustom沒有實現Repository,該如何來執行SQL語句呢,我們可以給實現類注入另一個EntityManger,通過EntityManager來執行SQL語句。以下是StudentBaseRepositoryImpl的實現程式碼

public class StudentBaseRepositoryImpl implements StudentBaseRepositoryCustom {
    @Autowired
    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public List<Object[]> groupByStudentAsSql() {
        List<Object[]> list = entityManager
                .createNativeQuery("select address,count(*) from t_student group by address")
                .getResultList();

        return list;
    }

    @Override
    public List<Object[]> groupByStudentAsHql() {
        List<Object[]> list = entityManager
                .createQuery("select address,count(*) from Student group by address")
                .getResultList();
        return list;
    }

    @Override
    public List<Object[]> groupByStudentAsSpecification() {
        //根據地址分組查詢,並且學生數量大於3的所有地址
        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
        CriteriaQuery<Object[]> query = builder.createQuery(Object[].class);
        Root<Student> root = query.from(Student.class);
        query.multiselect(root.get("address"),builder.count(root.get("id")))
                .groupBy(root.get("address")).having(builder.gt(builder.count(root.get("id")),3));

        return entityManager.createQuery(query).getResultList();
    }
}

前面兩個方法的實現都非常容易理解,就是建立一個查詢語句,執行完成之後會返回一組Object[]的投影,第三個方法稍微有些複雜,這是CriteriaQuery的標準寫法。

到這裡我們解決了擴充套件類的問題,但仍然有些疑問,如果每個類都有自己獨立的方法,那麼是不是每一個類都得按照上面的方面來寫介面和實現類,以上做法雖然可以很好的解決自定義類的擴充套件問題,但是仍然稍顯麻煩,我們可以定義一個基類來覆蓋一些比較通用的方法,如通用的SQL查詢等。下面我們就來建立這個BaseRepository,整個建立的過程有些複雜,可以參照專案的原始碼(原始碼在文章的最後有連結)。

建立的第一步定義一個BaseRepository的介面

package org.konghao.repo.base;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.NoRepositoryBean;

import java.io.Serializable;
import java.util.List;

/**
 * Created by konghao on 2016/12/7.
 */
@NoRepositoryBean
public interface BaseRepository<T,ID extends Serializable> extends JpaRepository<T,ID> {
    List<Object[]> listBySQL(String sql);
}

該介面實現了JpaRepository,這樣就保證擁有了Spring Data JPA中那些比較好用的方法,然後可以自定義自己需要的方法,程式碼中定義了一個listBySQL的方法。需要注意的是@NoRepositoryBean,這個表示該介面不會建立這個介面的例項(我們原來定義的StudentPageRepository這些,Spring Data JPA的基礎元件都會自動為我們建立一個例項物件,加上這個annotation,spring data jpa的基礎元件就不會再為我們建立它的例項)。之後我們編寫實現類BaseRepositoryImpl

package org.konghao.repo.base;

import org.springframework.data.jpa.repository.support.SimpleJpaRepository;

import javax.persistence.EntityManager;
import java.io.Serializable;
import java.util.List;

/**
 * Created by konghao on 2016/12/7.
 */
public class BaseRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T,ID>
        implements BaseRepository<T,ID> {

    private final EntityManager entityManager;

    //父類沒有不帶引數的構造方法,這裡手動構造父類
    public BaseRepositoryImpl(Class<T> domainClass, EntityManager entityManager) {
        super(domainClass, entityManager);
        this.entityManager = entityManager;
    }

    //通過EntityManager來完成查詢
    @Override
    public List<Object[]> listBySQL(String sql) {
        return entityManager.createNativeQuery(sql).getResultList();
    }
}

這個實現類比較的簡單,首先我們需要繼承SimpleJpaRepositorySimpleJpaRepository幫助我們實現了JpaRepository中的方法。然後實現BaseRepository介面。listBySQL方法非常的簡單,上面已經詳細介紹過了,具體的作用就是執行一條sql返回一組投影的列表。

下一步我們需要建立一個自定義的工廠,在這個工廠中註冊我們自己定義的BaseRepositoryImpl的實現。這個工廠的寫法具體參照Spring Data的JpaRepositoryFactoryBeanJpaRepositoryFactory。這個類上面一堆的泛型,我們不用考慮,只要按照相同的方式來寫即可。

建立JpaRepositoryFactoryBean需要呼叫如下方法,通過這個方法來返回一個工廠,這裡返回的是JpaRepositoryFactory。

protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {
    return new JpaRepositoryFactory(entityManager);
}

在JpaRepositoryFactory中,有兩個方法比較關鍵

protected Object getTargetRepository(RepositoryInformation information) {
    SimpleJpaRepository repository = this.getTargetRepository(information, this.entityManager);
    repository.setRepositoryMethodMetadata(this.crudMethodMetadataPostProcessor.getCrudMethodMetadata());
    return repository;
}

protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
   return this.isQueryDslExecutor(metadata.getRepositoryInterface())?QueryDslJpaRepository.class:SimpleJpaRepository.class;
}

通過這兩個方法來確定具體的實現類,也就是Spring Data Jpa具體例項化一個介面的時候會去建立的實現類。通過程式碼我們可以發現,Spring Data JPA都是呼叫SimpleJpaRepository來建立例項。以下是我們自己的工廠實現的程式碼

package org.konghao.repo.base;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactory;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;

import javax.persistence.EntityManager;
import java.io.Serializable;

/**
 * Created by konghao on 2016/12/7.
 */
public class BaseRepositoryFactoryBean<R extends JpaRepository<T, I>, T,
        I extends Serializable> extends JpaRepositoryFactoryBean<R, T, I> {
    @Override
    protected RepositoryFactorySupport createRepositoryFactory(EntityManager em) {
        return new BaseRepositoryFactory(em);
    }

    //建立一個內部類,該類不用在外部訪問
    private static class BaseRepositoryFactory<T, I extends Serializable>
            extends JpaRepositoryFactory {

        private final EntityManager em;

        public BaseRepositoryFactory(EntityManager em) {
            super(em);
            this.em = em;
        }

        //設定具體的實現類是BaseRepositoryImpl
        @Override
        protected Object getTargetRepository(RepositoryInformation information) {
            return new BaseRepositoryImpl<T, I>((Class<T>) information.getDomainType(), em);
        }

        //設定具體的實現類的class
        @Override
        protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
            return BaseRepositoryImpl.class;
        }
    }
}

接著我們需要讓spring在載入的時候找到我們自定義的BaseRepository的工廠,當我們使用了SpringBoot之後一切都變得簡單了,只要在入口類中加入@EnableJpaRepositories即可,程式碼如下

/**
 * Created by konghao on 2016/11/24.
 */
 @EnableJpaRepositories(basePackages = {"org.konghao"},
         repositoryFactoryBeanClass = BaseRepositoryFactoryBean.class//指定自己的工廠類
 )
 @SpringBootApplication
 public class DemoApplication {
     public static void main(String[] args) {
         SpringApplication.run(DemoApplication.class,args);
     }
 }

到這裡我們的整個自定義工廠的流程就結束了,我們寫一個介面實現BaseRepository即可

/**
 * Created by konghao on 2016/12/7.
 */
public interface StudentExtendsRepository extends BaseRepository<Student,Integer> {
    /**
     * 原來JPARepository的方法依然可以使用*/
    List<Student> findByNameAndAddress(String name, String address);
}

測試類中的程式碼如下

@Test
public void testBaseRepository() {
    //直接使用BaseRepository中的方法
    List<Object[]> list = studentExtendsRepository.listBySQL("select address,count(*) from t_student group by address");
    Assert.assertEquals(2,list.size());
    Assert.assertEquals("km",list.get(0)[0]);
    //原JpaRepository的方法依然可以使用
    List<Student> list2 = studentExtendsRepository.findByNameAndAddress("bar","zt");
    Assert.assertEquals(1,list2.size());
}

到這裡,我們這部分的內容就基本結束了,下一章節我們要解決的問題是基於Specification的封裝問題。

本文的原始碼在這裡:原始碼