Giter Club home page Giter Club logo

ozayduman / spring-data-specification-builder Goto Github PK

View Code? Open in Web Editor NEW
9.0 1.0 0.0 255 KB

Specification-Builder is a client-oriented dynamic search query library that supports joins among multiple tables in a strongly-type manner for Spring Projects. This library simplifies writing type-safe queries for search screens by using `Spring Data JPA`'s `JpaSpecificationExecutor` and `hibernate-jpamodelgen`. As you might know, for each query screen you have to pass a specific DTO (Data Transfer Objects) and write specific queries using that DTO. This leads to boiler-plate, useless, repetitive code. By using this library you can get rid of that kind of code, and write fluent-style dynamic queries driven by client-side easily.

License: Apache License 2.0

Java 100.00%
spring spring-data-jpa specification specification-database meta-model

spring-data-specification-builder's Introduction

Specification Builder

GitHub license Maven Central JavaDoc GitHub issues Twitter

Specification-Builder is a client-oriented dynamic search query library that supports joins among multiple tables in a strongly-type manner for Spring Projects.
This library simplifies writing type-safe queries for search screens by using Spring Data JPA's JpaSpecificationExecutor and hibernate-jpamodelgen. As you might know foreach query screen you have to pass a specific DTO (Data Transfer Objects) and write specific queries using that DTO. This leads to boiler-plate, useless, repetitive code. By using this library you can get rid of that kind of code, and write fluent-style dynamic queries driven by client-side easily.

FEATURES

  • Client-oriented dynamic query generation by using fluent style programming.
  • You can use different properties for the client and the server-side. This feature enables us not to expose domain entities to external world directly.
  • You can restrict, and open individual properties for query operations.
  • You can use the same property names for both client-side and server-side.
  • You can combine the dynamic query generation with your custom specifications.
  • Client-side decides to what operations will take place depending on the operands put in the criteriaDTO or pageRequestDTO. On the client-side you can use the following operators:
    • equal to: EQ
    • not equal to: NOT_EQ
    • greater than: GT
    • greater than or equal to: GE
    • less thanLT
    • less than or equal to : LE
    • between : BT
    • in : IN
    • not in : NOT_IN
    • is null: NULL
    • is not null: NOT_NULL
    • is true: TRUE
    • is false: FALSE
    • like: LIKE
    • not like: NOT_LIKE
  • You can use all these operators also in joins if needed as well.

DOCUMENTATION

HOW TO USE

Just add the following maven dependency to your pom.xml file.

<dependency>
    <groupId>com.github.ozayduman</groupId>
    <artifactId>specification-builder</artifactId>
    <version>0.0.5</version>
</dependency>

For Gradle, use the following dependency:

implementation 'com.github.ozayduman:specification-builder:0.0.5'

For Scala SBT, use the following dependency:

libraryDependencies += "com.github.ozayduman" % "specification-builder" % "0.0.5"

USAGE

SERVER-SIDE

by using bind method you can enable properties to be used in dynamic query generation. Client is allowed to use the properties only bound via bind method. if DTO properties are different from the entity properties then you have to specify it as the first argument of the bind method e.g. bind("employeeName", Employee_.name). Otherwise, you can fell free to omit it e.g. bind(Employee_.name).

final Specification<Employee> specification = SpecificationBuilder.<Employee>of(criteriaDTO)
               .bind("employeeName", Employee_.name)
               .bind("employeeSurname", Employee_.surname)
               .bind("employeeEmail", Employee_.email)
               .bind("employeeBirthDate", Employee_.birthDate)
               .bindJoin("phoneNumber", Employee_.phones, Phone_.number)
               .build();

       var customerFromDB = employeeRepository.findOne(specification)
               .orElseThrow(() -> new NoSuchElementException());

If your dto and entity share common names for properties you can simply define as follows:

final Specification<Employee> specification = SpecificationBuilder.<Employee>of(criteriaDTO)
               .bind(Employee_.name)
               .bind(Employee_.surname)
               .bind(Employee_.email)
               .bind(Employee_.birthDate)
               .bindJoin(Employee_.phones, Phone_.number)
               .build();

       var customerFromDB = employeeRepository.findOne(specification)
               .orElseThrow(() -> new NoSuchElementException());

For joins, you should use bindJoin instead e.g. bindJoin("phoneNumber", Employee_.phones, Phone_.number) or bindJoin(Employee_.phones, Phone_.number) You can add custom specifications by using bindCustom method

PAGINATION

For returning query results page by page you should pass sort information via PageRequestDTO instead of CriteriaDTO and then use PageRequestBuilder as follows:

  final Specification<Employee> specification = SpecificationBuilder.<Employee>of(pageRequestDTO)
                .bind("employeeName", Employee_.name)
                .bind("employeeSurname", Employee_.surname)
                .bind("employeeEmail", Employee_.email)
                .bind("employeeBirthDate", Employee_.birthDate)
                .bindJoin("phoneNumber", Employee_.phones, Phone_.number)
                .build();

        var pageRequest = PageRequestBuilder.of(pageRequestDTO)
                .bindSort("employeeName", Employee_.name)
                .bindSort("phoneNumber", Phone_.number)
                .build();

  Page<Employee> page = employeeRepository.findAll(specification, pageRequest);

  PageResultDTO pageResultDTO = PageResultDTO.from(page, EmployeeMapper.INSTANCE::toDTO);

If you don't want to use map struct library, you can write it explicitly as follows:

  PageResultDTO pageResultDTO = PageResultDTO.from(page, e -> {
            EmployeeResponseDTO dto = new EmployeeResponseDTO();
            dto.setName(e.getName());
            dto.setSurname(e.getSurname());
            dto.setEmail(e.getEmail());
            return dto;
        });

CLIENT-SIDE

On the client side you should pass the property, its value, and operation that will be used in the query generation.
Notice that some operators take no arguments (e.g. NULL, NOT_NULL, TRUE), some takes single, multiple values or range values as operands. So, you should follow the constraints of each operator described below:

  • EQ, NOT_EQ, GT, GE, LT; LE these operators take only one value as an argument:
    {
      ..
      "operations": [
        {
          "property": "name",
          "operator": "EQ",
          "value": "Alice"
        },
        {
          "property": "age",
          "operator": "GE",
          "value": 18
        }
      ]
    }
    
  • IN, NOT_IN these operators take multi-value as an argument:
    {
      "operations": [
        {
          "property": "customerId",
          "operator": "IN",
          "value": [
            1,
            2,
            3,
            4,
            5
          ]
        }
      ]
    }
    
  • BT this operator takes range of values as an argument:
    {
      "operations": [
        {
          "property": "age",
          "operator": "BT",
          "value": {
            "low": 18,
            "high": 65
          }
        }
      ]
    }
    
  • NULL, NOT_NULL, TRUE, FALSE these operators take no value as an argument:
    {
      "operations": [
        {
          "property": "phoneNumber",
          "operator": "NOT_NULL"
        }
      ]
    }
    

Sort order, requested page, and page size information can be passed as follows:

{
  ..
  "sortFields": [
    {
      "property": "name",
      "direction": "ASC"
    },
    {
      "property": "surname",
      "direction": "DESC"
    }
  ],
  "page": 0,
  "size": 10
}

SAMPLE PROJECT

There is a sample project repository that demonstrates usage of specificaiton-builder.

HOW TO BUILD

HOW TO CONTRIBUTE

Fork, and send a pull request and keep your fork in sync with the upstream repository.

LICENSE

Specification Builder is open source and can be found on GitHub. It is distributed under the Apache 2.0 License.

spring-data-specification-builder's People

Contributors

ozayduman avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

spring-data-specification-builder's Issues

Support compound operations

client-side should be able to send (operation or operation) as a compound operation as follows:

.compound(or(Customer_.name, Customer_.surname))
.compound(or(
$("name", Customer_.name),
$("surname", Customer_.surname)
)

think about how to handle with the operations coming from CriteriaDTO. It will lead to confusion.

  1. Another solution would use CompoundOrOperation and with this solution client will populate it relating AbstractOperations
public class CompoundOrOperation  {
    private final List<AbstractOperation> operations;
    public CompoundOrOperation(AbstractOperation ...operations) {
        this.operations =  List.of(operations);
    }

    @Override
    protected EnumSet<Operator> allowedOperators() {
        return OR;
    }

    @Override
    public java.lang.Comparable<?>[] getOperands() {
..    }
}

How to search cross join table in @ManyToMany association and combine AND - OR condition in same query

I have relationship entity like this one

Entity relationship

The Client can login by one or switch login between among Accounts. A Account can login by one or other difference Clients.

Relation Client - Account is many-to-many, so I create a entity ClientAccount to store client and account id.

Client

public class Client {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "brand_name", nullable = false, unique = true)
    private String brandName;
    
    @Column(name = "status")
    private String status;
    
    @Column(name = "date_expire", nullable = false)
    private Long dateExpire;
    
    // bi-directional many-to-one association to ClientAccount
    @OneToMany(mappedBy = "client")
    @JsonManagedReference
    private Set<ClientAccount> clientAccounts = new HashSet<ClientAccount>();
}

Account

public class Account {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(columnDefinition = "bytea", unique = true, nullable = true)
    private String email;
    @Column(columnDefinition = "text", name = "user_name")
    private String userName;
    @Column(columnDefinition = "text", name = "first_name")
    private String firstName;
    @Column(columnDefinition = "text", name = "last_name")
    private String lastName;
    
    // bi-directional many-to-one association to ClientAccount
    @OneToMany(mappedBy = "account")
    @JsonManagedReference
    private Set<ClientAccount> clientAccounts;
}

ClientAccount

public class ClientAccount {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "account_id")
    private Long accountId;
    @Column(name = "client_id")
    private Long clientId;
    
    // bi-directional many-to-one association to Client
    @ManyToOne
    @MapsId("clientId")
    @JsonBackReference
    @JoinColumn(name = "client_id", insertable = true, updatable = true)
    private Client client;

    // bi-directional many-to-one association to Account
    @ManyToOne
    @MapsId("accountId")
    @JsonBackReference
    @JoinColumn(name = "account_id", insertable = true, updatable = true)
    private Account account;
}

In case need search by branchName or firstName or lastName or email via input keyword, how is implement?

I see in your guideline example don't show filter condition in query, how can I filter and combine between AND and OR in query while join multiple table in this case is 3 tables(Client, ClientAccount, Account)

Specification<Client> searchClientAccountByKeyword(String keyword) {
    // how can implement Client join ClientAccount join Account and combine filter condition by keyword?
    // Client.join.ClientAccount.join.Account.and(branchName equal keyword or firstName equal keyword or ,....)
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.