在spring data jpa仓库方法查询中追加自定义条件

7

简短版

我正在寻找一种方法,可以在一个存储库类的所有findBy方法后添加特定条件。

完整版

假设我有一个产品实体和客户实体。它们都扩展了OwnerAwareEntity,并继承了ownerRef字段,该字段标识实体的所有者(可以是商家或合作伙伴)。我希望在运行时修改Product和Customer的findBy方法,以便它们附加一个额外的ownerRef条件。可以从用户会话中确定ownerRef值。

示例

提供公共ownerRef字段的父实体类

public class OwnerAwareEntity implements Serializable {

 private String ownerRef;

}

客户实体扩展 OwnerAwareEntity

public class Customer extends OwnerAwareEntity {

  private String firstname;

  private String mobile ;

}

扩展OwnerAwareEntity的产品实体
public class Product extends OwnerAwareEntity {

  private String code;

  private String name;

}

继承 OwnerAwareRepository 的产品和客户存储库类。
public interface OwnerAwareRepository extends JpaRepository {

}

public interface ProductRepository extends OwnerAwareRepository {

  Product findByCode(String code );

}

public interface CustomerRepository extends OwnerAwareRepository {

  Customer findByFirstname(String firstname );

}

这段代码被执行后,应该会产生下面类似的查询结果。
select P from Product P where P.code=?1 and P.ownerRef='aValue'
&
select C from Customer C where C.firstname=?1 and C.ownerRef='aValue'

我的做法是什么,才能实现附加条件?只有在父存储库为OwnerAwareRepository时,我才希望进行此附加。
2个回答

15

TL;DR: 我使用了Hibernate的@Filter,然后创建了一个Aspect来拦截方法

定义了一个基类实体,其结构如下

OwnerAwareEntity.java

import org.hibernate.annotations.Filter;
import org.hibernate.annotations.FilterDef;
import org.hibernate.annotations.ParamDef;    
import javax.persistence.Column;
import javax.persistence.MappedSuperclass;
import java.io.Serializable;

@MappedSuperclass
@FilterDef(name = "ownerFilter", parameters = {@ParamDef(name = "ownerRef", type = "long")})
@Filter(name = "ownerFilter", condition = "OWNER_REF = :ownerRef")
public class OwnerAwareEntity implements Serializable{

    @Column(name = "OWNER_REF",nullable = true)
    private Long ownerRef;

}

我们在这个实体上设置了过滤器。Hibernate的@Filter允许我们设置一个条件,以附加到select where子句中。
接下来,为OwnerAwareEntity类型的实体定义了一个基础存储库。
OwnerAwareRepository.java
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.NoRepositoryBean;

@NoRepositoryBean
public interface OwnerAwareRepository<T, ID extends java.io.Serializable> extends JpaRepository<T, ID> {

}

我将创建一个切面,它将拦截所有扩展OwnerAwareRepository的存储库中的方法。

OwnerFilterAdvisor.java

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.hibernate.Filter;
import org.hibernate.Session;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Aspect
@Component
@Slf4j
public class OwnerFilterAdvisor {

    @PersistenceContext
    private EntityManager entityManager;

    @Pointcut("execution(public * com.xyz.app.repository.OwnerAwareRepository+.*(..))")
    protected void ownerAwareRepositoryMethod(){

    }

    @Around(value = "ownerAwareRepositoryMethod()")
    public Object enableOwnerFilter(ProceedingJoinPoint joinPoint) throws Throwable{

        // Variable holding the session
        Session session = null;

        try {

            // Get the Session from the entityManager in current persistence context
            session = entityManager.unwrap(Session.class);

            // Enable the filter
            Filter filter = session.enableFilter("ownerFilter");

            // Set the parameter from the session
            filter.setParameter("ownerRef", getSessionOwnerRef());

        } catch (Exception ex) {

            // Log the error
            log.error("Error enabling ownerFilter : Reason -" +ex.getMessage());

        }

        // Proceed with the joint point
        Object obj  = joinPoint.proceed();

        // If session was available
        if ( session != null ) {

            // Disable the filter
            session.disableFilter("ownerFilter");

        }

        // Return
        return obj;

    }


    private Long getSessionOwnerRef() {

// Logic to return the ownerRef from current session

    }
}

该顾问被设置为拦截所有继承OwnerAwareRepository类的方法。在拦截时,从entityManager(当前持久化上下文)获取当前hibernate Session,并使用参数值“ownerRef”启用过滤器。

还创建了一个配置文件,以便扫描该顾问。

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages = {"com.xyz.app.advisors"})
public class AOPConfig {
}

一旦这些文件放置好了,您需要为需要拥有者感知的实体完成以下事项:

  1. 实体需要扩展OwnerAwareEntity
  2. 实体存储库类需要扩展OwnerAwareRepository

依赖关系

此设置需要在依赖项中添加spring aop。您可以将以下内容添加到pom.xml中。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

优点

  1. 适用于所有选择查询(findBy方法,findAll等)
  2. @Query方法也会被拦截
  3. 实现简单

注意事项

  • 删除或更新的where子句不受此过滤器影响。
  • 如果存储库包含保存/更新/删除方法,并且该方法未标记为@Transactional,则拦截器将出错(在这些情况下,您可以捕获并使方法正常进行)

当实体没有任何过滤器时,如何使用此解决方案? - Mohammad Mirzaeyan
1
@MohammadMirzaeyan:这个解决方案依赖于Hibernate的过滤器功能来注入自定义条件。通过使用谓词或拦截查询生成,可能还有其他可用的解决方案。不幸的是,由于基于过滤器的解决方案已经足够满足我的用例,我没有进行进一步的研究。 - Sandheep
绝对是一个不错的解决方案,但在我的代码库中,一些领域模型有条件,而另一些则没有。如果一个领域模型上没有任何过滤器,代码将会抛出异常,我们该如何处理呢?我在想也许你也可以解决这个问题。 - Mohammad Mirzaeyan
如果问题是只有一些域需要应用此功能,您可以创建一个基础域(例如在解决方案中的OwnerAwareEntity),该基础域具有过滤器,并使用基础域扩展要应用此功能的域。这样,只有扩展了OwnerAwareEntity(基础域)的域将应用过滤器。 - Sandheep

1
你可以在Spring Data JPA方法中使用QueryDSL的Predicate(或Specification)。
例如:
interface UserRepository extends CrudRepository<User, Long>, QueryDslPredicateExecutor<User> {
}

Predicate predicate = QUser.user.firstname.equalsIgnoreCase("dave")
    .and(user.lastname.startsWithIgnoreCase("mathews"));

userRepository.findAll(predicate);

要使用QueryDSL,请将以下内容添加到您的pom.xml文件中:

<dependencies>
//..
    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-jpa</artifactId>
        <version>4.1.4</version>
        <exclusions>
            <exclusion>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-api</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-apt</artifactId>
        <version>4.1.4</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>

        <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</outputDirectory>
                        <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

编译您的项目,即可获得实体的Q类。更多信息请参见此处

该类中的findBy方法仅为示例。存储库中可能有任意数量的findBy方法,并希望将其全部附加到特定条件。 - Sandheep
你的“任意数量的findBy”方法应该做什么?所有的findBy方法都用于静态地定义条件的组合。但是,QueryDslPredicateExecutor只有一个带有predicate参数的findAll方法,它使您能够在运行时构建任何条件的组合。 - Cepr0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接