GithubHelp home page GithubHelp logo

marcopotok / jpa-predicate-builder Goto Github PK

View Code? Open in Web Editor NEW
4.0 1.0 1.0 95 KB

Lightweight layer on top of JPA for easy query construction in Java

License: MIT License

Java 100.00%
jakarta-persistence java jpa specifications javax-persistence jakarta javax

jpa-predicate-builder's Introduction

JPA Predicate Builder

Build Maven Deploy Maven Central codecov

A lightweight layer on top of JPA for easy query construction in Java.

Project Description

Writing query for java applications faces many challenges, mainly cleanness of code, maintainability and performances. This project aims to address all three of these with a simple builder created on top of JPA APIs. Other libraries already exists, but usually make the integration difficult and don't always work with other libraries (e.g. Lombok). In this case we wanted a library with no extra dependencies and ready to go.

Key features:

  • predicates concatenation with a builder style
  • works seamlessly with Spring Data JPA Specifications
  • implementation agnostic
  • remove duplicated joins
  • easy fetch of related entities (prefetching)

Versions and compatibility

The JPA APIs were originally exposed under the package javax.persistence replaced in 2019 by the Jakarta's package jakarta.persistence. The Predicate Builder supports both versions of JPA APIs with the following versions:

  • 1.x.x: Javax Persistence
  • 2.x.x: Jakarta Persistence

How to install

Just add the dependency to your pom file.

How to use

To create your predicate, start with one constructor or factory method available, for example:

PredicateBuilder<User> builder = PredicateBuilder.of(User.class);

Let's see a more complete example:

class UserService {

    private final EntityManager entityManager;

    public UserService(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    private Collection<User> getUserWithName(String name) {
        Session session = entityManager.unwrap(Session.class);
        CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder();
        CriteriaQuery<User> query = criteriaBuilder.createQuery(User.class);
        Root<User> root = query.from(User.class);
        query.where(PredicateBuilder.of(User.class).withProperty("name", name).build(root, query, criteriaBuilder));
        return session.createQuery(query).getResultList();
    }
}

Specification (Spring Data JPA)

Another way to get the same result with Specifications:

class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    private Collection<User> getUserWithName(String name) {
        Specification<User> specification = PredicateBuilder.of(User.class).withProperty("name", name)::build;
        return userRepository.findAll(specification);
    }
}

Joins

In order to filter by an attribute of a relation, use the dot notation. For example, if you want to find all the orders of a user, you can write:

class OrderService {

    private final OrderRepository orderRepository;

    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    private Collection<Order> getOrdersOfUser(String userId) {
        Specification<Order> specification = PredicateBuilder.of(Order.class).withProperty("user.id", userId)::build;
        return orderRepository.findAll(specification);
    }
}

Prefetch

To avoid multiple queries with a lazy relationship with another entity, you can use the prefetch method.

class OrderService {

    private final OrderRepository orderRepository;

    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    private Collection<Order> getOrdersOfUser(String userId) {
        Specification<Order> specification = PredicateBuilder.of(Order.class)
                .prefetch("user.[nested.deep,other.deep]")
                .withProperty("user.id", userId)::build;
        return orderRepository.findAll(specification);
    }
}

In this case the engine will perform a fetch on user, user.nested, user.nested.deep, user.other and user.other.deep. Note that the fetch with the user entity is not duplicated.

Complex example

With REST API it is often necessary to expose multiple optional filters. In this case the Predicate Builder is useful because null (optional) values are handled natively.

Let's assume we have a value object OrderRequest with the following optional fields:

  • ids: Collection<Long>
  • type: String
  • userId: Long
  • userName: String
  • fromDate: Instant
  • toDate: Instant

With Predicate Builder

class OrderService {

    private final EntityManager entityManager;

    public OrderService(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    private Collection<Order> getOrders(OrderRequest request) {
        Session session = entityManager.unwrap(Session.class);
        CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder();
        CriteriaQuery<Order> query = criteriaBuilder.createQuery(Order.class);
        Root<Order> root = query.from(Order.class);

        PredicateBuilder<Order> builder = PredicateBuilder.of(Order.class)
                .prefetch("user.profile")
                .withPropertyIn("id", request.ids)
                .withProperty("type", request.type)
                .withProperty("user.id", request.userId)
                .withPropertyLikeIgnoreCase("user.name", request.userName)
                .withPropertyAfterInclusive("date", request.fromDate)
                .withPropertyBeforeInclusive("date", request.toDate);

        query.where(builder.build(root, query, criteriaBuilder));
        return session.createQuery(query).getResultList();
    }
}

Without Predicate Builder:

class OrderService {

    private final EntityManager entityManager;

    public OrderService(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    private Collection<Order> getOrders(OrderRequest request) {
        Session session = entityManager.unwrap(Session.class);
        CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder();
        CriteriaQuery<Order> query = criteriaBuilder.createQuery(Order.class);
        Root<Order> root = query.from(Order.class);

        List<Predicate> predicates = new LinkedList<>();
        if (request.ids != null) {
            predicates.add(root.get("id").in(request.ids));
        }
        if (request.type != null) {
            predicates.add(criteriaBuilder.equal(root.get("type"), request.type));
        }
        if (request.userId != null || request.userName != null) {
            Join<Object, Object> userJoin = root.join("user", JoinType.LEFT);
            if (request.userId != null) {
                predicates.add((criteriaBuilder.equal(userJoin.get("id"), request.userId)));
            }
            if (request.userName != null) {
                predicates.add((criteriaBuilder.like(criteriaBuilder.upper(userJoin.get("name")),
                        request.userName.toUpperCase(Locale.ROOT))));
            }
        }
        if (request.fromDate != null) {
            predicates.add((criteriaBuilder.greaterThanOrEqualTo(root.get("date"), request.fromDate)));
        }
        if (request.toDate != null) {
            predicates.add((criteriaBuilder.lessThanOrEqualTo(root.get("date"), request.toDate)));
        }
        root.fetch("user", JoinType.LEFT).fetch("profile", JoinType.LEFT);
        criteriaQuery.where(predicates.toArray(Predicate[]::new));

        return session.createQuery(query).getResultList();
    }
}

Limitations and further improvements

Current limitation and possible future improvements:

  • Only left joins -> possible auto detection
  • Add field checking at build time

jpa-predicate-builder's People

Contributors

m-potok avatar marcopotok avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar

Forkers

alexparlett

jpa-predicate-builder's Issues

Improve readme

Improve readme:

  • add examples on how the PredicateBuilder simplifies the query construction
  • fix typos
  • construction of sentences

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.