Java `Spring Data JPA` `Custom Repositories` `Specification` `Querydsl` 复杂查询

各位观众,大家好!我是你们的老朋友,今天咱们聊聊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可以像搭积木一样,用andor方法组合起来,形成更复杂的查询条件。

    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。

希望今天的讲座对大家有所帮助。记住,技术不是用来炫技的,而是用来解决问题的。选择合适的工具,才能事半功倍。下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注