如何为多个实体编写单个JPA规范

huangapple go评论75阅读模式
英文:

how to write a single jpa specification for multiple entities

问题

I am working on a Spring Boot - App that has multiple entities having some identical columns for filtering.

Currently, I have the same query defined in multiple repositories, so after doing some research, I've stumbled across an article about JPA - Specifications: https://spring.io/blog/2011/04/26/advanced-spring-data-jpa-specifications-and-querydsl/

So I made a generic class to build JPA-Specifications:

public final class GenericSpecifications<T> {
    public Specification whereNameLikeAndDateGreatherThan(String fieldName, String fieldDate, String name, LocalDate date) {
       return (root, query, builder) -> builder.lessThan(root.get(columnName), date);
    }
}

So in the service I can use:

repository.findAll(whereNameLikeAndDateGreatherThan(Person_.name, Person_.date, "Max", LocalDate.now()));

In this way, I have one query/specification in a central place and I don't need to write/maintain the same query on all repositories.

However, I have more complex queries, where I need to filter over multiple columns. This means that my methods, in my GenericSpecification-Class, become too bloated, since I need to pass multiple column names and the search-values, so I could end up with methods with 6 or more parameters.

I could define an Abstract-Entity class extended by all other entities. This abstract entity would have all the common fields in order to be sure that all the entities have the same columns. Then I can use these names for filtering, so I don't have to pass the field/column-names at all.

But, I am not sure if this is the cleanest approach to my problem. Do you know if there is a better way to do this?

英文:

I am working on a Spring Boot - App that has multiple entities having some identical columns for filtering.

Currently, I have the same query defined in multiple repositories, so after doing some research, I've stumbled across an article about JPA - Specifications: https://spring.io/blog/2011/04/26/advanced-spring-data-jpa-specifications-and-querydsl/

So I made a generic class to build JPA-Specifications:

public final class GenericSpecifications&lt;T&gt;
{
    public Specification whereNameLikeAndDateGreatherThan(String fieldName, String fieldDate, String name, LocalDate date)
    {
       return (root, query, builder) -&gt; builder.lessThan(root.get(columnName), date);
    }
}

So in the service I can use:

repository.findAll(whereNameLikeAndDateGreatherThan(Person_.name, Person_.date, &quot;Max&quot;, LocalDate.now());

In this way, I have one query/specification in a central place and I don't need to write/maintain the same query on all repositories.

However, I have more complex queries, where I need to filter over multiple columns.
This means that my methods, in my GenericSpecification-Class, become too bloated, since I need to pass multiple column names and the search-values, so I could end up with methods with 6 or more parameters.

I could define an Abstract-Entity class extended by all other entities.This abstract entity would have all the common fields in order to be sure that all the entities have the same columns.
Then I can use these names for filtering, so I don't have to pass the field/coulmn-names at all.

But, I am not sure if this is the cleanest approach to my problem.
Do you know if there is a better way to do this?

答案1

得分: 1

我认为最清晰的方法是使用继承,但是在规范创建器中使用,而不是在实体中。例如,类似以下的方式(没有尝试编译,所以可能不会编译通过,但应该能传达思路):

class BasicSpecificationBuilder<T> {
    public Specification<T> stringEqual(String fieldName, String value) {
        return (root, query, builder) ->
                builder.equal(root.<String>get(fieldName), value);
    }

    public Specification<T> dateAfter(String fieldName, LocalDate value) {
        return (root, query, builder) ->
                builder.<LocalDate>greaterThan(root.get(fieldName), value);
    }
}

// 根据实体类型和所需查询进行扩展
class ContractSpecificationBuilder<Contract> extends BasicSpecificationBuilder<Contract> {
    public Specification<Contract> contractsCreatedAfter(String partner, LocalDate date) {
        return (root, query, builder) ->
                stringEqual(Contract_.partnerName, partner)
                        .and(
                                dateAfter(Contract_.closeDate, date));
    }
}

class EmployeeSpecificationBuilder<Employee> extends BasicSpecificationBuilder<Employee> {
    public Specification<Employee> employeesJoinedAfter(String name, LocalDate date) {
        return (root, query, builder) ->
                stringEqual(Employee_.name, name)
                        .and(
                                dateAfter(Employee_.entryDate, date));
    }
}

这样,你在基类中有一组构建器方法,可以重复使用,并且查询不会因为它们被分离到每个实体中而变得复杂。像上面示例中可能会有一些代码重复 - 如果这些重复太多,你可以将这些常见组合重构到基类中,如下所示:

class BasicSpecificationBuilder<T> {
    public Specification<T> stringEqualAndDateAfter(String stringField, String stringValue, String dateField, LocalDate dateValue) {
        // 实现逻辑
    }
}

class ContractSpecificationBuilder<Contract> extends BasicSpecificationBuilder<Contract> {
    public Specification<Contract> contractsCreatedAfter(String partner, LocalDate date) {
        return stringEqualAndDateAfter(Contract_.partnerName, partner, Contract_.closeDate, date);
    }
}

这是一种品味和代码质量设置的问题(我们在SonarQube中设置了一个代码重复度的限制,但我不认为这会超出限制)。

由于这些都是工厂方法,你也可以使用提供静态方法的类来实现几乎相同的功能,而将“基本”方法作为静态实用方法包含在基类中。不过,我不太喜欢泛型静态方法的语法。

这一切都是基于你是否阅读了Baeldung关于如何使用Specification并且不喜欢那种方法。

英文:

I think the cleanest approach is to use inheritance, but in the specification creator, not the entities. So for example something like (didn't try if it compiles so it probably doesn't, but should give the idea):

class BasicSpecificationBuilder&lt;T&gt; {
    public Specification&lt;T&gt; stringEqual(String fieldName, String value) {
        // root is Root&lt;T&gt; here, don&#39;t know if this needs to be specified
        return (root, query, builder) -&gt; 
                builder.equal(root.&lt;String&gt;get(fieldName), value);
        }
    }
    public Specification&lt;T&gt; dateAfter(String fieldName, LocalDate value) {
        return (root, query, builder) -&gt;
                builder.&lt;LocalDate&gt;greaterThan(root.get(fieldName), value);
    }
}
// extend per entity type and required queries
class ContractSpecificationBuilder&lt;Contract&gt; extends BasicSpecificationBuilder&lt;Contract&gt; {
    public Specification&lt;Contract&gt; contractsCreatedAfter(String partner, LocalDate date) {
        return (root, query, builder) -&gt; 
            stringEqual(Contract_.partnerName, partner)
                .and(
            dateAfter(Contract_.closeDate, date));
    }
}
class EmployeeSpecificationBuilder&lt;Employee&gt; extends BasicSpecificationBuilder&lt;Employee&gt; {
    public Specification&lt;Employee&gt; employeesJoinedAfter(String name, LocalDate date) {
        return (root, query, builder) -&gt;
            stringEqual(Employee_.name, name)
                .and(
            dateAfter(Employee_.entryDate, date));
    }
}

This way you have a collection of builder methods in the base class you can reuse, and queries that don't explode because they're separated per entity. There may be a little code duplication as in the example above - if there's too many of those, you can refactor these common combinations into the base class.

class BasicSpecificationBuilder&lt;T&gt; {
    public Specification&lt;T&gt; stringEqualAndDateAfter(String stringField, String stringValue, String dateField, LocalDate dateValue) {
    public Specification&lt;Employee&gt; employeesJoinedAfter(String name, LocalDate date) {
        return (root, query, builder) -&gt;
            stringEqual(stringField, name)
                .and(
            dateAfter(dateField, date));
    }
}
class ContractSpecificationBuilder&lt;Contract&gt; extends BasicSpecificationBuilder&lt;Contract&gt; {
    public Specification&lt;Contract&gt; contractsCreatedAfter(String partner, LocalDate date) {
        return stringEqualAndDateAfter(Contract_.partnerName, partner, Contract_.closeDate, date);
    }
}

That's a matter of taste and code quality settings (we had a code duplication measure in SonarQube with a limit, but I don't think this would have crossed the limit).

Since these are all factory methods, you can do pretty much the same thing with classes providing static methods and the "base" class containing the basic methods as static utility methods. I kind of dislike the syntax for generic static methods though.

That's all assuming you read the Baeldung intro on how to use Specification and didn't like that approach.

huangapple
  • 本文由 发表于 2020年7月31日 02:51:47
  • 转载请务必保留本文链接:https://go.coder-hub.com/63179554.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定