使用spring data jpa實現統一的多條件查詢

因爲篇幅限制,本文中,咱們只給出了部分示例代碼。
若是你須要完整的代碼,請點擊: https://github.com/mengyunzhi/springBootSampleCode/tree/master/multiQuery

本文開發環境:java:1.8 + maven:3.3html

WHY TO DO

在系統開發中,咱們避免不了要使用多條件查詢,好比咱們按學生的性氏來查找學生,按學生所在的班級查找學生,按學生入學的時間來查找學生等。在以往的開發中,進行綜合查詢時,咱們須要爲每一個實體單獨寫綜合查詢的代碼。具體思路爲:先將查詢條件加入map,而後在綜合查詢時,取出map中的值,並加入查詢條件中,最後實現綜合查詢。java

時間一長,咱們發如今查詢的過程當中,大多有如下規律:git

  • 傳入的值類型爲number(short int long ...)時,大多進行的爲精確查詢。
  • 傳入的值的類型爲String時,大多進行的爲模糊查詢。
  • 傳入的值爲Collection,大多進行爲in查詢。
  • 傳入的值爲Date時,大多進行的爲範圍查詢。

鑑於大多數符合以上規律,本文將闡述了一種簡單的方法,可以使得咱們像使用findById(Long id)同樣,來使用pageByEntity(Object entity)進行綜合條件查詢。github

本文需求

有3個實體類:教師,班級,地址。一個實體基類,YunZhiAbstractEntity
三者的關係以下:spring

clipboard.png

功能需求以下:
klass對象作爲查詢參數,按klass對象中的屬性值來進行綜合查詢:
好比:sql

  1. 設置klass對象的name值爲zhangsan,則模糊查出全部namezhangsan班級信息。
  2. 設置klass對象的totalStudentCount值爲3,則查出全部totalStudentCount3班級信息。
  3. 設置klass對象中的teacher。則查詢時加入klass->teacher的信息。若是teacher設置了ID,則加入teacher對應ID的精確查詢,若是未設置ID,則查詢teacher的其它屬性,其屬性不爲null,加入查詢條件。
  4. 設置klass對象中的teacher中的address,則按第3點的標準進行klass->teacher->address的關聯查詢。
  5. 能夠在查詢中實現排除某些字段(不爲null,也不進行關聯查詢)。
  6. 能夠實現對某些字段的範圍查詢。

知識準備

本文大致將使用到如下知識點:api

  • spring-data-jpa5.5章節對綜合查詢進行了說明,而且給出了示例代碼。
  • java的反射機制。
  • java的註解。

基本思路:springboot

  1. 根據傳入的查詢實體,反射出其字段、字段中的註解、字段的值。而後在依次判斷其類型,按類型的不一樣,加入查詢條件。
  2. 若是字段類型爲實體,則說明進行關聯查詢(join),此時,進行遞歸調用,來獲取關聯查詢實體的字段,並按字段類型,字段值進行綜合查詢
  3. 特殊的查詢(排除字段,範圍查詢的開始,結束字段,in查詢),使用註解來協助完成。

代碼實現

本文github中給出的示例代碼,除in查詢外,已經實現了其它幾點在本文中所須要的功能。本節中,只直接給出關鍵代碼,其它的關聯代碼,還須要到github中進行查看。app

文件列表

├── java
│   └── com
│       └── mengyunzhi
│           └── springbootsamplecode
│               └── multiquery
│                   ├── MultiQueryApplication.java
│                   ├── annotation
│                   │   ├── BeginQueryParam.java
│                   │   ├── EndQueryParam.java
│                   │   └── IgnoreQueryParam.java
│                   ├── entity
│                   │   ├── Address.java
│                   │   ├── Klass.java
│                   │   ├── Teacher.java
│                   │   └── YunZhiAbstractEntity.java
│                   ├── repository
│                   │   ├── AddressRepository.java
│                   │   ├── KlassRepository.java
│                   │   └── TeacherRepository.java
│                   └── service
│                       ├── AddressService.java
│                       ├── AddressServiceImpl.java
│                       ├── CommonService.java
│                       ├── KlassService.java
│                       ├── KlassServiceImpl.java
│                       ├── TeacherService.java
│                       ├── TeacherServiceImpl.java
│                       ├── YunzhiService.java
│                       └── YunzhiServiceImpl.java
└── resources
    └── application.yml

實體類的繼承咱們在需求中已經給出。annotation註解,repository倉庫(dao)層,service服務層中都很普通,它們是本文完成綜合查詢的基礎。最下方的 YunzhiServiceImpl.java,即是咱們今天的主角。less

YunzhiService

package com.mengyunzhi.springbootsamplecode.multiquery.service;


import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

import java.util.List;

/**
 * @author panjie
 */
public interface YunzhiService {
    /**
     * 經過傳入的實體進行多條件查詢
     * @param entity
     * @param pageable
     * @return 分頁數據
     * panjie
     */
    Page<?> page(JpaSpecificationExecutor jpaSpecificationExecutor, Object entity, Pageable pageable);

    /**
     * 經過傳入的實體查詢全部數據
     * @param entity
     * @return
     * panjie
     */
    List<?> findAll(JpaSpecificationExecutor jpaSpecificationExecutor, Object entity);
}

YunzhiServiceImpl

代碼中,已經給出詳細的註釋。請參閱。

package com.mengyunzhi.springbootsamplecode.multiquery.service;

import com.mengyunzhi.springbootsamplecode.multiquery.annotation.BeginQueryParam;
import com.mengyunzhi.springbootsamplecode.multiquery.annotation.EndQueryParam;
import com.mengyunzhi.springbootsamplecode.multiquery.annotation.IgnoreQueryParam;
import com.mengyunzhi.springbootsamplecode.multiquery.entity.YunZhiAbstractEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Service;

import javax.persistence.criteria.*;
import java.lang.reflect.Field;
import java.sql.Date;
import java.util.Calendar;
import java.util.Collection;
import java.util.List;

/**
 * 多條件綜合查詢
 *
 * @author panjie
 */
@Service
public class YunzhiServiceImpl implements YunzhiService {
    private static final Logger logger = LoggerFactory.getLogger(YunzhiServiceImpl.class);

    @Override
    public Page<Object> page(JpaSpecificationExecutor jpaSpecificationExecutor, Object entity, Pageable pageable) {
        Specification specification = this.getSpecificationByEntity(entity);
        Page<Object> objectPage = jpaSpecificationExecutor.findAll(specification, pageable);
        return objectPage;
    }

    @Override
    public List<Object> findAll(JpaSpecificationExecutor jpaSpecificationExecutor, Object entity) {
        Specification specification = this.getSpecificationByEntity(entity);
        List<Object> objectList = jpaSpecificationExecutor.findAll(specification);
        return objectList;
    }

    private Specification getSpecificationByEntity(Object entity) {
        Specification<Object> specification = new Specification<Object>() {
            private Predicate predicate = null;
            private CriteriaBuilder criteriaBuilder;

            // 設置and謂語.注意,這裏只能設置and關係的謂語,若是謂語爲OR,則須要手動設置
            private void andPredicate(Predicate predicate) {
                if (null != predicate) {
                    if (null == this.predicate) {
                        this.predicate = predicate;
                    } else {
                        this.predicate = this.criteriaBuilder.and(this.predicate, predicate);
                    }
                }
            }

            private void generatePredicate(Object entity, From<Object, ?> root) {
                logger.debug("反射字段,按字段類型,進行綜合查詢");
                Field[] fields = entity.getClass().getDeclaredFields();
                try {
                    for (Field field : fields) {
                        logger.info("反射字段名,及字段值。並設置爲字段可見");
                        String name = field.getName();
                        field.setAccessible(true);
                        Object value = field.get(entity);

                        if (value != null) {
                            if (field.getAnnotation(IgnoreQueryParam.class) != null) {
                                logger.debug("存在@IgnoreQueryParam註解, 跳出");
                                continue;
                            }

                            // 初始化兩個界限的變量
                            Boolean isBegin = false;
                            Boolean isEnd = false;

                            // 查找開始與結束的註解
                            BeginQueryParam beginQueryParam = field.getAnnotation(BeginQueryParam.class);
                            if (beginQueryParam != null) {
                                logger.debug("存在@BeginQueryParam註解");
                                isBegin = true;
                                name = beginQueryParam.name();
                            } else if (field.getAnnotation(EndQueryParam.class) != null) {
                                logger.debug("存在@EndQueryParam註解");
                                isEnd = true;
                                name = field.getAnnotation(EndQueryParam.class).name();
                            }

                            // 按字段類型進行查詢
                            if (value instanceof String) {
                                logger.debug("字符串則進行模糊查詢");
                                String stringValue = ((String) value);
                                if (!stringValue.isEmpty()) {
                                    this.andPredicate(criteriaBuilder.like(root.get(name).as(String.class), "%" + stringValue + "%"));
                                }
                            } else if (value instanceof Number) {
                                logger.debug("若是爲number,則進行精確或範圍查詢");
                                if (value instanceof Short) {
                                    Short shortValue = (Short) value;
                                    if (isBegin) {
                                        this.andPredicate(criteriaBuilder.greaterThanOrEqualTo(root.get(name).as(Short.class), shortValue));
                                    } else if (isEnd) {
                                        this.andPredicate(criteriaBuilder.lessThanOrEqualTo(root.get(name).as(Short.class), shortValue));
                                    } else {
                                        this.andPredicate(criteriaBuilder.equal(root.get(name).as(Short.class), shortValue));
                                    }
                                } else if (value instanceof Integer) {
                                    Integer integerValue = (Integer) value;
                                    if (isBegin) {
                                        this.andPredicate(criteriaBuilder.greaterThanOrEqualTo(root.get(name).as(Integer.class), integerValue));
                                    } else if (isEnd) {
                                        this.andPredicate(criteriaBuilder.lessThanOrEqualTo(root.get(name).as(Integer.class), integerValue));
                                    } else {
                                        this.andPredicate(criteriaBuilder.equal(root.get(name).as(Integer.class), integerValue));
                                    }
                                } else if (value instanceof Long) {
                                    Long longValue = (Long) value;
                                    if (isBegin) {
                                        this.andPredicate(criteriaBuilder.greaterThanOrEqualTo(root.get(name).as(Long.class), longValue));
                                    } else if (isEnd) {
                                        this.andPredicate(criteriaBuilder.lessThanOrEqualTo(root.get(name).as(Long.class), longValue));
                                    } else {
                                        this.andPredicate(criteriaBuilder.equal(root.get(name).as(Long.class), longValue));
                                    }
                                } else {
                                    logger.error("綜合查詢Number類型,暫時只支持到Short,Integer,Long");
                                }
                            } else if (value instanceof Calendar) {
                                logger.debug("Calendar類型");
                                Calendar calendarValue = (Calendar) value;
                                if (isBegin) {
                                    this.andPredicate(criteriaBuilder.greaterThanOrEqualTo(root.get(name).as(Calendar.class), calendarValue));
                                } else if (isEnd) {
                                    this.andPredicate(criteriaBuilder.lessThanOrEqualTo(root.get(name).as(Calendar.class), calendarValue));
                                } else {
                                    this.andPredicate(criteriaBuilder.equal(root.get(name).as(Calendar.class), calendarValue));
                                }
                            } else if (value instanceof Date) {
                                logger.debug("Sql.Date類型");
                                Date dateValue = (Date) value;
                                if (isBegin) {
                                    this.andPredicate(criteriaBuilder.greaterThanOrEqualTo(root.get(name).as(Date.class), dateValue));
                                } else if (isEnd) {
                                    this.andPredicate(criteriaBuilder.lessThanOrEqualTo(root.get(name).as(Date.class), dateValue));
                                } else {
                                    this.andPredicate(criteriaBuilder.equal(root.get(name).as(Date.class), dateValue));
                                }
                            } else if (value instanceof YunZhiAbstractEntity) {
                                logger.debug("是實體類");
                                YunZhiAbstractEntity yunZhiAbstractEntity = (YunZhiAbstractEntity) value;
                                if (yunZhiAbstractEntity.getId() != null) {
                                    logger.debug("對應的ManyToOne,加入了id, 則按ID查詢");
                                    this.andPredicate(criteriaBuilder.equal(root.join(name).get("id").as(Long.class), yunZhiAbstractEntity.getId()));
                                } else {
                                    logger.debug("未加入id, 則進行Join查詢");
                                    this.generatePredicate(value, root.join(name));
                                }

                            } else if (value instanceof Collection) {
                                Collection<?> collectionValue = (Collection<?>)value;
                                if (collectionValue.size() > 0) {
                                    logger.warn("暫不支持一對多,多對多查詢");
                                    // todo: 一對多,多對多查詢
                                }
                            } else {
                                logger.error("綜合查詢暫不支持傳入的數據類型", name, field);
                            }
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

            @Override
            public Predicate toPredicate(Root<Object> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
                // 設置CriteriaBuilder,用於合併謂語
                this.criteriaBuilder = criteriaBuilder;
                this.generatePredicate(entity, root);
                return this.predicate;
            }
        };

        return specification;
    }
}

測試

代碼中,已經給出詳細的註釋。請參閱。

package com.mengyunzhi.springbootsamplecode.multiquery.service;

import com.mengyunzhi.springbootsamplecode.multiquery.entity.Address;
import com.mengyunzhi.springbootsamplecode.multiquery.entity.Klass;
import com.mengyunzhi.springbootsamplecode.multiquery.entity.Teacher;
import com.mengyunzhi.springbootsamplecode.multiquery.repository.KlassRepository;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.Calendar;
import java.util.List;

/**
 * @author panjie
 */
@SpringBootTest
@RunWith(SpringRunner.class)
public class YunzhiServiceImplTest {
    private final static Logger logger = LoggerFactory.getLogger(YunzhiServiceImplTest.class);
    @Autowired KlassService klassService;
    @Autowired YunzhiService yunzhiService;
    @Autowired TeacherService teacherService;
    @Autowired AddressService addressService;
    @Autowired
    KlassRepository klassRepository;

    @Test
    public void pageByEntity() {
        Klass originKlass = klassService.getOneSavedKlass();
        PageRequest pageRequest = PageRequest.of(0, 2);

        Klass klass = this.initQueryKlass();
        Page<Klass> klassPage = (Page<Klass>) yunzhiService.page(klassRepository, klass, pageRequest);
        Assertions.assertThat(klassPage.getTotalElements()).isEqualTo(1);

        logger.info("更改short值 ,斷言返回爲0");
        klass.setTotalStudentCount((short) 11);
        klassPage = (Page<Klass>) yunzhiService.page(klassRepository, klass, pageRequest);
        Assertions.assertThat(klassPage.getTotalElements()).isEqualTo(0);

        logger.info("設置short值爲null ,斷言返回爲1");
        klass.setTotalStudentCount(null);
        klassPage = (Page<Klass>) yunzhiService.page(klassRepository, klass, pageRequest);
        Assertions.assertThat(klassPage.getTotalElements()).isEqualTo(1);

        logger.info("更改int值 ,斷言返回爲0");
        klass.setIntegerTest(101);
        klassPage = (Page<Klass>) yunzhiService.page(klassRepository, klass, pageRequest);
        Assertions.assertThat(klassPage.getTotalElements()).isEqualTo(0);

        logger.info("設置int值爲null ,斷言返回爲1");
        klass.setIntegerTest(null);
        klassPage = (Page<Klass>) yunzhiService.page(klassRepository, klass, pageRequest);
        Assertions.assertThat(klassPage.getTotalElements()).isEqualTo(1);

        logger.info("更改long值 ,斷言返回爲0");
        klass.setLongTest((long) 1001);
        klassPage = (Page<Klass>) yunzhiService.page(klassRepository, klass, pageRequest);
        Assertions.assertThat(klassPage.getTotalElements()).isEqualTo(0);

        logger.info("設置long值爲null ,斷言返回爲1");
        klass.setLongTest(null);
        klassPage = (Page<Klass>) yunzhiService.page(klassRepository, klass, pageRequest);
        Assertions.assertThat(klassPage.getTotalElements()).isEqualTo(1);

        logger.info("測試關聯實體");
        klass = this.initQueryKlass();
        klass.setTeacher(originKlass.getTeacher());
        klassPage = (Page<Klass>) yunzhiService.page(klassRepository, klass, pageRequest);
        Assertions.assertThat(klassPage.getTotalElements()).isEqualTo(1);

        logger.info("更改關聯實體");
        Teacher teacher = teacherService.getOneSavedTeacher();
        klass.setTeacher(teacher);
        klassPage = (Page<Klass>) yunzhiService.page(klassRepository, klass, pageRequest);
        Assertions.assertThat(klassPage.getTotalElements()).isEqualTo(0);

        logger.info("測試二級關聯實體Address");
        teacher = new Teacher();
        teacher.setAddress(originKlass.getTeacher().getAddress());
        klass.setTeacher(teacher);
        klassPage = (Page<Klass>) yunzhiService.page(klassRepository, klass, pageRequest);
        Assertions.assertThat(klassPage.getTotalElements()).isEqualTo(1);

        logger.info("更改測試二級關聯實體Address");
        teacher.setAddress(addressService.getOneSavedAddress());
        klassPage = (Page<Klass>) yunzhiService.page(klassRepository, klass, pageRequest);
        Assertions.assertThat(klassPage.getTotalElements()).isEqualTo(0);

        logger.info("測試二級關聯實體 屬性");
        Address address = new Address();
        address.setCity("測試城市");
        teacher.setAddress(address);
        klassPage = (Page<Klass>) yunzhiService.page(klassRepository, klass, pageRequest);
        Assertions.assertThat(klassPage.getTotalElements()).isEqualTo(1);

        logger.info("測試二級關聯實體 屬性");
        address.setCity("測試城市不存在");
        teacher.setAddress(address);
        klassPage = (Page<Klass>) yunzhiService.page(klassRepository, klass, pageRequest);
        Assertions.assertThat(klassPage.getTotalElements()).isEqualTo(0);

        logger.info("測試@IgnoreQueryParam註解");
        klass.setTeacher(null);
        klass.setIgnoreTeacher(teacher);
        klassPage = (Page<Klass>) yunzhiService.page(klassRepository, klass, pageRequest);
        Assertions.assertThat(klassPage.getTotalElements()).isEqualTo(1);

        logger.info("測試@BeginQueryParam, @EndQueryParam註解在Calendar上在做用");
        klass = this.initQueryKlass();
        teacher = new Teacher();
        klass.setTeacher(teacher);
        Calendar beginCalendar = Calendar.getInstance();
        beginCalendar.setTimeInMillis(originKlass.getTeacher().getCreateTime().getTimeInMillis());
        Calendar endCalendar = Calendar.getInstance();
        endCalendar.setTimeInMillis(originKlass.getTeacher().getCreateTime().getTimeInMillis());
        teacher.setBeginCreateTime(beginCalendar);
        teacher.setEndCreateTime(endCalendar);
        klassPage = (Page<Klass>) yunzhiService.page(klassRepository, klass, pageRequest);
        Assertions.assertThat(klassPage.getTotalElements()).isEqualTo(1);

        logger.info("將範圍擴大");
        beginCalendar.add(Calendar.MINUTE, -1);
        endCalendar.add(Calendar.MINUTE, 1);
        klassPage = (Page<Klass>) yunzhiService.page(klassRepository, klass, pageRequest);
        Assertions.assertThat(klassPage.getTotalElements()).isEqualTo(1);

        logger.info("區間後移");
        beginCalendar.add(Calendar.MINUTE, 2);
        endCalendar.add(Calendar.MINUTE, 2);
        klassPage = (Page<Klass>) yunzhiService.page(klassRepository, klass, pageRequest);
        Assertions.assertThat(klassPage.getTotalElements()).isEqualTo(0);

        logger.info("區間前移");
        beginCalendar.add(Calendar.MINUTE, -4);
        endCalendar.add(Calendar.MINUTE, -4);
        klassPage = (Page<Klass>) yunzhiService.page(klassRepository, klass, pageRequest);
        Assertions.assertThat(klassPage.getTotalElements()).isEqualTo(0);

        logger.info("區間調換");
        beginCalendar.add(Calendar.MINUTE, 4);
        klassPage = (Page<Klass>) yunzhiService.page(klassRepository, klass, pageRequest);
        Assertions.assertThat(klassPage.getTotalElements()).isEqualTo(0);

        logger.info("date的區間測試累了,不測了");
    }

    @Test
    public void findAllByEntity() {
        logger.info("上面的分頁,已經將該測試的測試完了,這裏只是作作樣子,防止語法錯語");
        Klass originKlass = klassService.getOneSavedKlass();

        Klass klass = this.initQueryKlass();
        List<Klass> klassPage = (List<Klass>) yunzhiService.findAll(klassRepository, klass);
        Assertions.assertThat(klassPage.size()).isEqualTo(1);

        logger.info("測試二級關聯實體Address");
        Teacher teacher = new Teacher();
        teacher.setAddress(originKlass.getTeacher().getAddress());
        klass.setTeacher(teacher);
        klassPage = (List<Klass>) yunzhiService.findAll(klassRepository, klass);
        Assertions.assertThat(klassPage.size()).isEqualTo(1);
    }

    /**
     * 獲取初始化用於查詢的班級
     * @return
     * panjie
     */
    private Klass initQueryKlass() {
        logger.info("加入全部的測試信息,斷言返回1條記錄");
        Klass klass = new Klass();
        klass.setName("測試班級名稱");
        klass.setTotalStudentCount((short) 10);
        klass.setIntegerTest(100);
        klass.setLongTest(1000L);
        return klass;
    }
}

總結

從第一次接觸spring-data-jpa的綜合查詢,再到本身熟練使用,再到學生熟練使用,再到進行其它的項目維護時,見到了單表綜合查詢,再到今天的多實體綜合查詢。一步步的證實在學習--實踐--學習的路上,咱們是對的。該方式也是當前,咱們能找到最有效的學習方法。

簡單的事情重複作,你就是專家;重複的事情認真作,你就是贏家。

學習的道路如此簡單:重複簡單的,思索重複的。你就是本身人生的贏家!

最後,感謝我聰明好學的學生們給了我在項目敲定完工日期後,仍然能夠靜心學習的自信;感謝spring-boot,讓我在整個的學習過程當中,一次次的提高對面向接口開發思想的認識;感謝來自廊坊的張應亮先生的單表查詢代碼給了我實現多表關聯查詢的靈感與動力;感受廊坊世通科技的李廣川總經理對團隊的信任及支持。謝謝大家!

相關文章
相關標籤/搜索