Skip to content

自定义 Spring Data JPA 实现复杂分页

Spring Data JPA 简单分页示例

我们都知道JPA对于简单的分页查询使用是非常便利的,一种是通过 Specification 构造查询分页,一种是通过 @Query 注解自定义 SQL 实现查询分页,对应示例如下:

  • Specification 的简单使用
kotlin
val pageRequest = PageRequest.of(inUsePromotionCouponsVO.index - 1, inUsePromotionCouponsVO.size)

val specification = Specification<PromotionCoupon> { root, criteriaQuery, criteriaBuilder ->
    val predicate = criteriaBuilder.conjunction()
    predicate.expressions.add(criteriaBuilder.equal(root.get<Int>("couponBaseID"), inUsePromotionCouponsVO.couponBaseID))
    predicate.expressions.add(criteriaBuilder.equal(root.get<Int>("isDel"), inUsePromotionCouponsVO.isDel))
    predicate.expressions.add(criteriaBuilder.equal(root.get<Int>("isUse"), 1))
    if(inUsePromotionCouponsVO.useTime != null && inUsePromotionCouponsVO.useTime != "") predicate.expressions.add(criteriaBuilder.like(root.get<String>("useTime"), inUsePromotionCouponsVO.useTime + "%"))
    criteriaQuery.orderBy(criteriaBuilder.desc(root.get<Int>("id")))
    predicate
}
val page = promotionCouponRepository.findAll(specification, pageRequest)

优点:面向对象,不用接触 SQL 语句,单表动态条件支持非常好

缺点:多表操作很复杂,不支持字段子查询(待确认?)

  • @Query 注解的简单使用
kotlin
@Query(nativeQuery = true, value = "select a.*, (select shopName from shop where memID = a.memID limit 0, 1) shopName, " +
        "(select count(1) from promotionCoupon where couponBaseID in (select couponBaseID from promotionActivityCoupon where actID = a.id) and isUse = 0) receivedTotal, " +
        "(select GROUP_CONCAT(preferentialText SEPARATOR '&') from promotionCouponBase where id in (select couponBaseID from promotionActivityCoupon where actID = a.id)) preferentialText " +
        " from promotionActivity a where isDel = 0 and (name like CONCAT('%', ?1, '%') or ?1 is null) " +
        " and (status = ?2 or ?2 is null) and (actType = ?3 or ?3 is null) " +
        " and (memID = ?4 or ?4 is null) " +
        " and (isRecommend = ?5 or ?5 is null) " +
        " and (actMode = ?6 or ?6 is null) " +
        " order by id desc",
        countQuery = "select count(1) from promotionActivity where isDel = 0 and (name like CONCAT('%', ?1, '%') or ?1 is null) " +
                " and (status = ?2 or ?2 is null) and (actType = ?3 or ?3 is null) and (memID = ?4 or ?4 is null) and (isRecommend = ?5 or ?5 is null) and (actMode = ?6 or ?6 is null)")
fun findByPage(name: String?, status: Int?, actType: Int?, memID: Int?, isRecommend:Int?=null,actMode:Int?,pageable: Pageable): Page<PromotionActivityDO>

优点:直接写 SQL 语句,随心所欲

缺点:不支持动态条件,分页需要多写一条 COUNT 语句

通过 Spring Data JPA 自定义复杂分页

  • 自定义动态 SQL 语句
kotlin
class SimpleSQL {

    private val query = StringBuilder(50)
    private val countQuery = StringBuilder(50)
    private val params = mutableMapOf<String, Any?>()

    fun select(columns: String): SimpleSQL {
        query.append("SELECT ").append(columns).append(" ")
        countQuery.append("SELECT COUNT(1) ")
        return this
    }

    fun from(table: String): SimpleSQL {
        query.append(" FROM ").append(table).append(" ")
        countQuery.append(" FROM ").append(table).append(" ")
        return this
    }

    fun where(criteria: String): SimpleSQL {
        query.append(" WHERE ").append(criteria).append(" ")
        countQuery.append(" WHERE ").append(criteria).append(" ")
        return this
    }

    fun orderBy(orders: String): SimpleSQL {
        query.append(" ORDER BY ").append(orders).append(" ")
        return this
    }

    fun setParam(key: String, param: Any?) {
        params[key] = param
    }

    fun getParams(): Map<String, Any?> {
        return this.params.toMap()
    }

    fun getQuery(): String {
        return query.toString()
    }

    fun getCountQuery(): String {
        return countQuery.toString()
    }
}
  • 使用 EntityManager 实现分页
kotlin
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.data.domain.Page
import org.springframework.data.domain.PageImpl
import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Component
import javax.persistence.EntityManager

/**
 * Created by pxc on 2019-11-25
 * Spring Data JPA复杂SQL分页
 */
@Component
class ComplexPageSQLUtils {

    @Autowired
    private lateinit var entityManager: EntityManager

    fun <T> execute(pageable: Pageable, simpleSQL: SimpleSQL, tClass: Class<T>, vararg params: Any): Page<T> {

        val createQuery = entityManager.createNativeQuery(simpleSQL.getQuery(), tClass)

        val createCountQuery = entityManager.createNativeQuery(simpleSQL.getCountQuery())

        simpleSQL.getParams().forEach { t, u ->
            createQuery.setParameter(t, u)
            createCountQuery.setParameter(t, u)
        }

        createQuery.firstResult = pageable.pageNumber * pageable.pageSize
        createQuery.maxResults = pageable.pageSize

        val resultList = createQuery.resultList as List<T>
        val aLong = createCountQuery.resultList[0].toString().toLong()

        return PageImpl<T>(resultList, pageable, aLong)

    }
}
  • 单元测试
kotlin
import com.linkershow.common.vo.InGetPromotionActivitysVO
import com.linkershow.common.vo.OutGetPromotionActivitysPageVO
import com.linkershow.common.vo.OutGetPromotionActivitysVO
import com.linkershow.data.entity.PromotionActivityDO
import com.linkershow.data.util.ComplexPageSQLUtils
import com.linkershow.data.util.SimpleSQL
import org.junit.Test
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.data.domain.PageRequest
import org.springframework.test.context.junit4.SpringRunner

@RunWith(SpringRunner::class)
@SpringBootTest
class ComplexPageSQLTest {

    @Autowired
    private lateinit var complexPageSQLUtils: ComplexPageSQLUtils

    /**
     * Spring Data JPA复杂SQL分页示例
     */
    @Test
    fun example() {

        val inGetPromotionActivitysVO = InGetPromotionActivitysVO("", 1, 2, 5, null)

        val pageRequest = PageRequest.of(0, 10)

        val simpleSQL = SimpleSQL()// 主SQL

        simpleSQL
                // 主查询结果集
                .select("a.*, (select shopName from shop where memID = a.memID limit 0, 1) shopName, " +
                " (select count(1) from promotionCoupon where couponBaseID in (select couponBaseID from promotionActivityCoupon where actID = a.id) and isUse = 0) receivedTotal, " +
                " (select GROUP_CONCAT(preferentialText SEPARATOR '&') from promotionCouponBase where id in (select couponBaseID from promotionActivityCoupon where actID = a.id)) preferentialText ")
                // 主表,支持多表联查
                .from("promotionActivity a")

        // 开始拼接WHERE条件
        val criteria = StringBuilder(" isDel = 0 ")

        if (inGetPromotionActivitysVO.name != null) {

            criteria.append(" and name like CONCAT('%', :name, '%') ")
            simpleSQL.setParam("name", inGetPromotionActivitysVO.name)
        }

        if (inGetPromotionActivitysVO.status != null) {

            criteria.append(" and status = :status ")
            simpleSQL.setParam("status", inGetPromotionActivitysVO.status)
        }

//        if (inGetPromotionActivitysVO.actType != null) {
//
//            criteria.append(" and actType = :actType ")
//            simpleSQL.setParam("actType", inGetPromotionActivitysVO.actType)
//        }
//
//
//        if (inGetPromotionActivitysVO.IsRecommend != null) {
//
//            criteria.append(" and isRecommend = :isRecommend ")
//            simpleSQL.setParam("isRecommend", inGetPromotionActivitysVO.IsRecommend)
//        }

        // 主条件
        simpleSQL.where(criteria.toString())

        // 主排序
        simpleSQL.orderBy("id")

        // 调用方法,返回分页
        val page = complexPageSQLUtils.execute(pageRequest, simpleSQL, PromotionActivityDO::class.java)

        // 生成结果
        val outGetPromotionActivitysPageVO = OutGetPromotionActivitysPageVO()
        outGetPromotionActivitysPageVO.create(page.isFirst, page.isLast, page.totalPages, page.totalElements, page.content as List<OutGetPromotionActivitysVO?>)

        println(outGetPromotionActivitysPageVO.list)
    }
}

Released under the MIT License.