为了账号安全,请及时绑定邮箱和手机立即绑定

Spring Boot中动态生成SQL查询的实现方法

动态查询就是在你使用软件时,程序根据你的输入即时生成的SQL查询。想象你有一个电商应用,并希望让用户根据自己的需要筛选商品,比如按类别、价格、颜色等条件。我们不需要为每种筛选组合单独编写查询,而是可以根据用户的请求自动生成相应的SQL语句。

这篇文章将引导你,在Spring Boot里使用Spring Data JPA, 创建动态的SQL查询。

配置环境

让我们从创建一个演示项目开始吧。你可以查看我在本指南中创建的示例(在GitHub上的链接:https://github.com/des-felins/edu-bookshop)。或者,你也可以使用你自己的应用。

这个演示项目是一个极简的网上书店应用,这是一个小型应用,并不具备企业级功能,界面也相当简单。正适合我们使用。

主要的实体类是 Book

    @Entity  //实体类注解
    @Table(name= "books")  //表名注解
    public class Book {  //书籍类

        @Id  //唯一标识符注解
        @Column(name = "id", nullable = false)  //列名注解,不允许为空
        @GeneratedValue(strategy = GenerationType.IDENTITY)  //自增主键策略
        private Long id;  //书籍ID

        @NotBlank  //非空注解
        @Column(name = "name", length = 150)  //列名注解,长度限制
        private String name;  //书名

        @NotNull  //非空注解
        @ManyToOne(cascade =  
                {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})  //级联操作类型
        @JoinColumn(name="category_id")  //关联列名
        private Category category;  //分类

        @NotNull  //非空注解
        @ManyToOne(cascade =  
                {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})  //级联操作类型
        @JoinColumn(name="author_id")  //关联列名
        private Author author;  //作者

        @NotNull  //非空注解
        @ManyToOne(cascade =  
                {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})  //级联操作类型
        @JoinColumn(name="language_id")  //关联列名
        private Language language;  //语言

        @ManyToOne(cascade =  
                {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})  //级联操作类型
        @JoinColumn(name="format_id")  //关联列名
        private Format format;  //格式

        @NotNull  //非空注解
        @Column(name = "price")  //列名注解
        private double price;  //价格

    //获取器、设置器、构造器、equals() 和 hashCode()

    }

FormatCategoryLanguageAuthor 这几个类非常简单明了,只包含 id 和 name。例如,Category 类的定义如下:

class Category:
    def __init__(self, id, name):
        self.id = id
        self.name = name

请注意,根据上下文要求,这里删除了代码示例,因为源文本中仅描述了类的特性。

    @Entity  
    @Table(name= "categories")  
    public class Category {  

        /**

* 表示分类的唯一标识符。
         */
        @Id  
        @Column(name = "id", nullable = false)  
        @GeneratedValue(strategy = GenerationType.IDENTITY)  
        private Long id;  

        /**

* 表示分类的名称。
         */
        @NotBlank  
        @Column(name = "name")  
        private String name;  

    //生成器、设置器、构造函数、equals()和hashCode()

    }
创建一个Specification类

要使用JPA构建动态SQL查询语句,我们可以利用Specification接口来为实体构建多个条件谓词,这些条件可以组合形成具有灵活条件的查询。

    public interface Specification<T> extends Serializable {  

        @Nullable  
        Predicate toPredicate(Root<T> root, @Nullable CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);  
    }

/
序列化 (Serializable) 接口的规范说明。
该接口包含一个方法 toPredicate,用于返回一个谓词 (Predicate)。
/
注:toPredicate 方法接收三个参数:

  • Root<T> root:根对象,代表泛型 T 的查询根。
  • @Nullable CriteriaQuery<?> query:查询对象,可为空。
  • CriteriaBuilder criteriaBuilder:用于构建查询条件的对象。
    此接口定义了规范,使开发者能够基于特定条件构建查询谓词。

为了使用JPA规范,我们让BookRepository接口实现JpaSpecificationExecutor

/**

* 包裹书籍仓库接口,继承了JpaRepository和JpaSpecificationExecutor接口。
 */
public interface BookRepository extends JpaRepository<Book, Long>, JpaSpecificationExecutor<Book> { }

现在,我们来创建一个规范类,用于定义 Book 实体的自定义规格:

    public class BookSpecification {  

        public static Specification<Book> 价格小于或等于指定价格(int price) {  
            return (root, query, criteriaBuilder) ->  
                    criteriaBuilder.lessThanOrEqualTo(root.get("price"), price);  
        }  
    }

hasPriceLessThanOrEqualTo() 方法中,返回一个 Specification 用于 Book,并构建一个预构建的查询。根对象是一个 Book 实例,它具有名为 priceint 类型属性。生成的查询包含一个 WHERE 子句来筛选价格小于或等于传入参数的书籍。

单凭一个参数来定规格其实用处不大,所以我们创建一些筛选条件。

/**

* 确保图书类别的名称与提供的名称匹配。
 */
public static Specification<Book> hasCategoryName(String name) {
    return (root, query, criteriaBuilder) -> 
            criteriaBuilder.equal(root  
                    .get("category")  
                    .get("name"), name);  
}

/**

* 确保图书语言的名称与提供的名称匹配。
 */
public static Specification<Book> hasLanguageName(String name) {
    return (root, query, criteriaBuilder) -> 
            criteriaBuilder.equal(root  
                    .get("language")  
                    .get("name"), name);  
}

/**

* 确保图书格式的名称与提供的名称匹配。
 */
public static Specification<Book> hasFormatName(String name) {
    return (root, query, criteriaBuilder) -> 
            criteriaBuilder.equal(root  
                    .get("format")  
                    .get("name"), name);  
}

注意,我们传递的是作为字符串的方法名。相反,你可以使用JPA 静态元模型生成器来创建类型安全的查询。因此,使用 JPA 模型,价格过滤的 Specification 可以这样定义:

// 示例代码

删除示例代码前的注释“// 示例代码”,并调整句子结构以避免重复:

注意,我们传递的是作为字符串的方法名。相反,你可以使用JPA 静态元模型生成器来创建类型安全的查询。使用 JPA 模型,价格过滤的 Specification 可以这样定义:

public static Specification<Book> 价格不超过(int 价格) {  
    // 返回一个规格,检查图书价格是否不超过给定价格  
    return (root, query, criteriaBuilder) ->  
           criteriaBuilder.lessThanOrEqualTo(root.get(Book_.price), 价格);  
}
从服务中调用 Specification 类

由于我们的 BookRepository 实现了 JpaSpecificationExecutor 接口,我们可以利用这些方法,比如:

查找所有满足指定规范的列表

首先,在我们的 Service 类中,我们可以添加一个名为 findAllFiltered() 的方法,并将所有过滤器参数传递进去。

public List<Book> 查找所有过滤的书籍(String 类别名称, String 语言名称, String 格式名称, int 最大价格) {
}

在方法体中,我们创建一个 Specification 对象实例,并在用户没有应用任何过滤器的情况下,将 null 作为参数传递给 where 方法。

    // 定义了一个名为 `spec` 的Specification对象,类型为 `Book`,初始化为 `Specification.where(null)`,这通常用于设置查询条件的起点。
    Specification<Book> spec = Specification.where(null);

然后,我们检查每个传递的参数是否为 null,并将它们添加到 Specification 对象中,通过调用 BookSpecification 类中的相应方法。

最后,我们将 Specification 对象传入 findAll(Specification<T> spec) 方法。

代码看起来像这样:

@Service
public class BookService {

    private final BookRepository bookRepository;

    public BookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    /**

* 根据类别、语言、格式和最高价格筛选书籍。

* @param categoryName 类别名称

* @param languageName 语言名称

* @param formatName 格式名称

* @param maxPrice 最高价格

* @return 符合条件的书籍列表
     */
    public List<Book> findAllFiltered(String categoryName,
                                      String languageName,
                                      String formatName,
                                      int maxPrice) {

        Specification<Book> spec = new Specification<Book>();

        if (categoryName) {
            spec = spec.and(BookSpecification.hasCategoryName(categoryName));
        }

        if (languageName) {
            spec = spec.and(BookSpecification.hasLanguageName(languageName));
        }

        if (formatName) {
            spec = spec.and(BookSpecification.hasFormatName(formatName));
        }

        if (maxPrice > 0) {
            spec = spec.and(BookSpecification.hasPriceLessThanOrEqualTo(maxPrice));
        }

        // 根据规格返回所有符合条件的书籍
        return bookRepository.findAll(spec);
    }

}
跑测试

让我们确保我们的代码按预期运行。我使用了这个包含测试数据的data.sql文件,帮助你理解这些预期数值的由来。整个测试套件的代码在这里可以找到这里

        @Test  
        void 根据类别查找所有书籍() {  
            List<Book> books = bookService.findAllFiltered(  
                    "类别", null, null, 0);  
            int 期望数量 = 2;  
            Assertions.assertEquals(期望数量, books.size());  
        }  

        @Test  
        void 根据类别和格式查找所有书籍() {  
            List<Book> books = bookService.findAllFiltered(  
                    "类别", null, "格式", 0);  
            int 期望数量 = 7;  
            Assertions.assertEquals(期望数量, books.size());  
        }  

        @Test  
        void 根据类别、语言、格式和价格查找所有书籍() {  
            List<Book> books = bookService.findAllFiltered(  
                    "类别", "语言", "格式", 29);  
            int 期望数量 = 1;  
            Assertions.assertEquals(期望数量, books.size());  
        }
最后来个总结

在这篇文章里,我们学到了如何使用Spring Data JPA Specification来创建动态的数据库查询语句,并且JPA Specification允许我们通过编程创建灵活的SQL查询,而无需编写重复代码。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消