各位观众,大家好!我是你们的老朋友,今天咱们聊聊Java Spring Data JPA里那些个“花里胡哨”但贼好用的复杂查询技巧。别怕,虽然标题看起来像高数,其实掌握了就是降维打击,让你在CRUD的世界里横着走。
开场白:别再只会findByXXX
了,来点真本事!
咱们用Spring Data JPA,最开始肯定是findByXXX
一把梭。简单是真简单,但稍微复杂点的需求,比如多条件组合、模糊匹配、排序分页一起上,findByXXX
就懵逼了。手动写SQL?也不是不行,但代码丑不说,维护起来更是噩梦。所以,咱们要掌握更高级的武器。
第一部分:Custom Repositories:我的地盘我做主
有时候,JPA自带的方法满足不了我们刁钻的需求。比如,有个业务逻辑特别复杂,需要调用存储过程,或者需要执行一些特殊的SQL语句。这时,我们就需要自定义Repository了。
-
步骤1:定义接口
首先,创建一个接口,继承JpaRepository或者其他Spring Data提供的Repository接口。在这个接口里,定义你自己的方法。
public interface UserRepository extends JpaRepository<User, Long> { // 自定义方法 List<User> findUsersByComplexCriteria(String name, int age); }
-
步骤2:实现接口
创建一个类,实现你定义的接口。注意,这个类的命名要有规范,通常是
接口名 + Impl
。import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.Query; import java.util.List; public class UserRepositoryImpl implements UserRepository { @PersistenceContext private EntityManager entityManager; @Override public List<User> findUsersByComplexCriteria(String name, int age) { String sql = "SELECT u FROM User u WHERE u.name LIKE :name AND u.age > :age"; Query query = entityManager.createQuery(sql, User.class); query.setParameter("name", "%" + name + "%"); query.setParameter("age", age); return query.getResultList(); } @Override public List<User> findAll() { // TODO Auto-generated method stub return null; } @Override public List<User> findAll(Sort sort) { // TODO Auto-generated method stub return null; } @Override public List<User> findAllById(Iterable<Long> ids) { // TODO Auto-generated method stub return null; } @Override public <S extends User> List<S> saveAll(Iterable<S> entities) { // TODO Auto-generated method stub return null; } @Override public void flush() { // TODO Auto-generated method stub } @Override public <S extends User> S saveAndFlush(S entity) { // TODO Auto-generated method stub return null; } @Override public void deleteAllInBatch(Iterable<User> entities) { // TODO Auto-generated method stub } @Override public void deleteAllByIdInBatch(Iterable<Long> ids) { // TODO Auto-generated method stub } @Override public void deleteAllInBatch() { // TODO Auto-generated method stub } @Override public User getOne(Long id) { // TODO Auto-generated method stub return null; } @Override public User getById(Long id) { // TODO Auto-generated method stub return null; } @Override public User getReferenceById(Long id) { // TODO Auto-generated method stub return null; } @Override public <S extends User> List<S> findAll(Example<S> example) { // TODO Auto-generated method stub return null; } @Override public <S extends User> List<S> findAll(Example<S> example, Sort sort) { // TODO Auto-generated method stub return null; } @Override public Page<User> findAll(Pageable pageable) { // TODO Auto-generated method stub return null; } @Override public <S extends User> S save(S entity) { // TODO Auto-generated method stub return null; } @Override public Optional<User> findById(Long id) { // TODO Auto-generated method stub return null; } @Override public boolean existsById(Long id) { // TODO Auto-generated method stub return false; } @Override public long count() { // TODO Auto-generated method stub return 0; } @Override public void deleteById(Long id) { // TODO Auto-generated method stub } @Override public void delete(User entity) { // TODO Auto-generated method stub } @Override public void deleteAllById(Iterable<? extends Long> ids) { // TODO Auto-generated method stub } @Override public void deleteAll(Iterable<? extends User> entities) { // TODO Auto-generated method stub } @Override public void deleteAll() { // TODO Auto-generated method stub } @Override public <S extends User> Optional<S> findOne(Example<S> example) { // TODO Auto-generated method stub return null; } @Override public <S extends User> Page<S> findAll(Example<S> example, Pageable pageable) { // TODO Auto-generated method stub return null; } @Override public <S extends User> long count(Example<S> example) { // TODO Auto-generated method stub return 0; } @Override public <S extends User> boolean exists(Example<S> example) { // TODO Auto-generated method stub return false; } }
-
步骤3:配置Spring
让Spring知道你的自定义Repository。有两种方式:
-
XML配置: (不推荐,现在都用注解了)
<jpa:repositories base-package="com.example.repository" entity-manager-factory-ref="entityManagerFactory" repository-impl-postfix="Impl"/>
-
Java配置: (推荐)
在你的配置类上添加
@EnableJpaRepositories
注解,并指定repositoryImplementationPostfix
属性。@Configuration @EnableJpaRepositories(basePackages = "com.example.repository", repositoryImplementationPostfix = "Impl") public class JpaConfig { // ... }
repositoryImplementationPostfix
属性指定了实现类的后缀,Spring会根据这个后缀找到对应的实现类。 -
-
步骤4:使用自定义方法
现在,你就可以在你的Service或者Controller里使用自定义的方法了。
@Service public class UserService { @Autowired private UserRepository userRepository; public List<User> getUsersByComplexCriteria(String name, int age) { return userRepository.findUsersByComplexCriteria(name, age); } }
第二部分:Specifications:灵活的查询条件
Specifications是Spring Data JPA提供的一种构建动态查询条件的方式。它可以让你像搭积木一样,组合各种查询条件。
-
步骤1:定义Specification接口
Specification接口只有一个方法:
toPredicate
。这个方法接受一个Root
、一个CriteriaQuery
和一个CriteriaBuilder
,返回一个Predicate
。Root
:代表查询的根对象,可以用来访问实体类的属性。CriteriaQuery
:代表整个查询,可以用来设置查询的类型、排序等。CriteriaBuilder
:用来构建查询条件,比如等于、大于、小于、模糊匹配等。Predicate
:代表一个查询条件。
public interface UserSpecification { static Specification<User> hasName(String name) { return (root, query, criteriaBuilder) -> criteriaBuilder.like(root.get("name"), "%" + name + "%"); } static Specification<User> hasAgeGreaterThan(int age) { return (root, query, criteriaBuilder) -> criteriaBuilder.greaterThan(root.get("age"), age); } }
-
步骤2:使用Specification
在你的Repository里,可以使用
JpaSpecificationExecutor
接口提供的findAll
方法,传入一个Specification对象。import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> { // ... }
@Service public class UserService { @Autowired private UserRepository userRepository; public List<User> getUsersByCriteria(String name, int age) { Specification<User> specification = Specification.where(UserSpecification.hasName(name)) .and(UserSpecification.hasAgeGreaterThan(age)); return userRepository.findAll(specification); } }
-
Specification的组合
Specification可以像搭积木一样,用
and
、or
方法组合起来,形成更复杂的查询条件。Specification<User> specification = Specification.where(UserSpecification.hasName(name)) .and(UserSpecification.hasAgeGreaterThan(age)) .or(UserSpecification.hasEmailLike("%@example.com"));
-
Specification的优势
- 动态性: 可以根据不同的条件,动态地构建查询条件。
- 可读性: 代码结构清晰,易于理解和维护。
- 可重用性: 可以将常用的查询条件封装成Specification,方便在不同的地方使用。
第三部分:Querydsl:类型安全的查询
Querydsl是一种类型安全的查询框架。它使用Java代码来构建查询,而不是字符串,可以避免SQL注入的风险,并且在编译时就能发现错误。
-
步骤1:添加依赖
在你的
pom.xml
文件中,添加Querydsl的依赖。<dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-jpa</artifactId> </dependency> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-apt</artifactId> <scope>provided</scope> </dependency>
-
步骤2:配置Maven插件
在你的
pom.xml
文件中,添加Querydsl的Maven插件。这个插件会在编译时生成Querydsl的Q类,用于类型安全的查询。<build> <plugins> <plugin> <groupId>com.mysema.maven</groupId> <artifactId>apt-maven-plugin</artifactId> <version>1.1.3</version> <executions> <execution> <goals> <goal>process</goal> </goals> <configuration> <outputDirectory>target/generated-sources/java</outputDirectory> <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor> </configuration> </execution> </executions> </plugin> </plugins> </build>
注意:
outputDirectory
必须是target/generated-sources/java
,插件才能正确生成Q类。processor
指定了用于生成Q类的Annotation Processor。 -
步骤3:生成Q类
执行
mvn compile
命令,Maven插件会自动生成Q类。Q类位于target/generated-sources/java
目录下。每个实体类都会生成一个对应的Q类。例如,User
实体类会生成一个QUser
类。 -
步骤4:使用Querydsl
在你的Repository里,可以使用
QuerydslPredicateExecutor
接口提供的findAll
方法,传入一个Predicate对象。import com.querydsl.core.types.Predicate; import org.springframework.data.querydsl.QuerydslPredicateExecutor; public interface UserRepository extends JpaRepository<User, Long>, QuerydslPredicateExecutor<User> { // ... }
import com.querydsl.jpa.impl.JPAQueryFactory; @Service public class UserService { @Autowired private UserRepository userRepository; @PersistenceContext private EntityManager entityManager; public List<User> getUsersByQuerydsl(String name, int age) { QUser user = QUser.user; Predicate predicate = user.name.like("%" + name + "%").and(user.age.gt(age)); return (List<User>) userRepository.findAll(predicate); } public List<User> getUsersByQuerydslAdvanced(String name, int age) { QUser user = QUser.user; JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager); return queryFactory.selectFrom(user) .where(user.name.like("%" + name + "%").and(user.age.gt(age))) .orderBy(user.age.desc()) .limit(10) .fetch(); } }
-
Querydsl的优势
- 类型安全: 使用Java代码构建查询,避免SQL注入的风险,并且在编译时就能发现错误。
- 可读性: 代码结构清晰,易于理解和维护。
- 强大的功能: 支持各种复杂的查询操作,比如聚合、分组、排序、分页等。
第四部分:实战案例:一个复杂的搜索功能
假设我们要做一个用户搜索功能,需要支持以下条件:
- 姓名模糊匹配
- 年龄范围
- 邮箱精确匹配
- 注册时间范围
- 按照年龄排序,可以升序或者降序
- 分页
我们可以使用Specification或者Querydsl来实现这个功能。
-
使用Specification实现
public interface UserSpecification { static Specification<User> hasNameLike(String name) { return (root, query, criteriaBuilder) -> criteriaBuilder.like(root.get("name"), "%" + name + "%"); } static Specification<User> hasAgeBetween(int minAge, int maxAge) { return (root, query, criteriaBuilder) -> criteriaBuilder.between(root.get("age"), minAge, maxAge); } static Specification<User> hasEmail(String email) { return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("email"), email); } static Specification<User> hasRegistrationDateBetween(Date startDate, Date endDate) { return (root, query, criteriaBuilder) -> criteriaBuilder.between(root.get("registrationDate"), startDate, endDate); } } @Service public class UserService { @Autowired private UserRepository userRepository; public Page<User> searchUsers(String name, Integer minAge, Integer maxAge, String email, Date startDate, Date endDate, String sortField, String sortDirection, int page, int size) { Specification<User> specification = Specification.where(null); if (name != null && !name.isEmpty()) { specification = specification.and(UserSpecification.hasNameLike(name)); } if (minAge != null && maxAge != null) { specification = specification.and(UserSpecification.hasAgeBetween(minAge, maxAge)); } if (email != null && !email.isEmpty()) { specification = specification.and(UserSpecification.hasEmail(email)); } if (startDate != null && endDate != null) { specification = specification.and(UserSpecification.hasRegistrationDateBetween(startDate, endDate)); } Sort sort = Sort.by(sortField); if (sortDirection.equalsIgnoreCase("desc")) { sort = sort.descending(); } Pageable pageable = PageRequest.of(page, size, sort); return userRepository.findAll(specification, pageable); } }
-
使用Querydsl实现
@Service public class UserService { @Autowired private UserRepository userRepository; @PersistenceContext private EntityManager entityManager; public Page<User> searchUsersByQuerydsl(String name, Integer minAge, Integer maxAge, String email, Date startDate, Date endDate, String sortField, String sortDirection, int page, int size) { QUser user = QUser.user; BooleanBuilder builder = new BooleanBuilder(); if (name != null && !name.isEmpty()) { builder.and(user.name.like("%" + name + "%")); } if (minAge != null && maxAge != null) { builder.and(user.age.between(minAge, maxAge)); } if (email != null && !email.isEmpty()) { builder.and(user.email.eq(email)); } if (startDate != null && endDate != null) { builder.and(user.registrationDate.between(startDate, endDate)); } Sort sort = Sort.by(sortField); if (sortDirection.equalsIgnoreCase("desc")) { sort = sort.descending(); } Pageable pageable = PageRequest.of(page, size, sort); return userRepository.findAll(builder, pageable); } }
代码解释:
BooleanBuilder
:用于构建动态的Predicate。PageRequest.of(page, size, sort)
:创建分页对象。userRepository.findAll(builder, pageable)
:执行查询,返回分页结果。
第五部分:总结与建议
今天我们学习了Custom Repositories、Specifications和Querydsl这三种Spring Data JPA的复杂查询技巧。
- Custom Repositories: 适用于需要执行特殊SQL语句或者调用存储过程的场景。
- Specifications: 适用于构建动态查询条件的场景,代码结构清晰,易于理解和维护。
- Querydsl: 适用于需要类型安全查询的场景,避免SQL注入的风险,并且在编译时就能发现错误。
建议:
- 在简单的查询场景下,可以使用
findByXXX
方法。 - 在稍微复杂一点的查询场景下,可以使用Specifications。
- 在需要类型安全查询或者需要执行复杂查询操作的场景下,可以使用Querydsl。
希望今天的讲座对大家有所帮助。记住,技术不是用来炫技的,而是用来解决问题的。选择合适的工具,才能事半功倍。下次再见!