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()
}
Format
、Category
、Language
和 Author
这几个类非常简单明了,只包含 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
实例,它具有名为 price 的 int
类型属性。生成的查询包含一个 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查询,而无需编写重复代码。
共同学习,写下你的评论
评论加载中...
作者其他优质文章