jakartaee / data

Data-API
Apache License 2.0
105 stars 29 forks source link

Proposal for Criteria Feature in Jakarta Data[Vote] #460

Open otaviojava opened 8 months ago

otaviojava commented 8 months ago

As a ...

I need to be able to ...

Expanding upon the capabilities of the Jakarta Data Query Language (#458), this proposal introduces the Criteria feature to enable programmatically constructing queries within the Jakarta Data framework. The Criteria API provides developers with an object-oriented approach to building query criteria, offering greater flexibility and control over data retrieval operations.

Which enables me to ...

The primary goal of this proposal is to initiate a discussion and a vote regarding the inclusion of the Criteria feature in the Jakarta Data project.

Key Features:

  1. Programmatic Query Construction: The Criteria API allows developers to programmatically build query criteria using a fluent and object-oriented interface, eliminating the need for raw queries.

  2. Filtering and Logical Conditions: Developers can apply various filtration rules and logical conditions to the criteria query object, enabling precise data retrieval based on specific criteria.

  3. Integration with Jakarta Data Query Language: The Criteria feature seamlessly integrates with the Jakarta Data Query Language, providing an additional method for constructing queries within the framework.

  4. Integration with Metadata: Criteria queries can be combined with metadata-driven queries, offering developers multiple options for constructing queries based on their requirements.

  5. Integration with Repository: Introducing a new interface, CriteriaRepository, that extends the existing PageableRepository, and enables seamless integration of criteria-based queries with repository pattern implementations.

Additional information

Example Usage:

// Combined with Metadata
List<Car> cars = template
    .select(Car.class)
    .where(_Car.city.eq("Rome").and(_Car.active.isTrue()))
    .orderBy(_Car.name.asc())
    .result();

// Combined with Repository
@Repository
public interface CardEntityRepository extends PageableRepository<Car, String>, CriteriaRepository<Car> {

    default Page<Car> findAll(String city, Pageable pageable) {
        Specification<Car> criteria = (root, query, criteriaBuilder) -> {
            List<Predicate> predicates = new ArrayList<>();
            predicates.add(criteriaBuilder.equal(root.get("city"), city));
            return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
        };
        return findAll(criteria, pageable);
    }
}

// Pure Implementation
List<Cat> cats = template.createCriteria(Cat.class)
    .add(Predicate.like("name", "Iz%"))
    .addOrder(Order.asc("age"))
    .result();

Benefits:

  1. Simplified Query Construction: The Criteria API simplifies query construction by providing a fluent and object-oriented interface, reducing the complexity of query writing.

  2. Enhanced Flexibility: Developers have greater flexibility in defining query criteria programmatically, allowing for dynamic and adaptable data retrieval operations.

  3. Integration with Metadata and Query Language: The Criteria feature seamlessly integrates with metadata-driven queries and the Jakarta Data Query Language, providing a comprehensive solution for data access within the Jakarta Data framework.

  4. Repository Integration: The introduction of CriteriaRepository enables developers to leverage criteria-based queries within the repository pattern, promoting code organization and reusability.

graemerocher commented 8 months ago

seems specific to JPA criteria

njr-11 commented 8 months ago

A pattern like this is something to consider post v1.0 when more is added to the static metamodel to better enable it.

gavinking commented 8 months ago

A pattern like this is something to consider post v1.0 when more is added to the static metamodel to better enable it.

Agreed. This isn't something I would bite off for 1.0. I think really it's tricky to get right. (Speaking from hard experience here.)

gavinking commented 8 months ago

By the way, since this issue is open, I just want to record for the future that @njr-11 has proposed something that I think is really nice, in some ways quite a lot nicer than a "full" criteria query API. The idea is to use the static metamodel to express "restrictions" which can be passed to a repository methods.

Conceptually, something like this:

repo.books(_Book.title.like("Jakarta Data%"), 
           _Book.publicationDate.between(pastDate, LocalDate.now()))

Or, if you need to combine this with ordering:

repo.books(where(_Book.title.like("Jakarta Data%"), 
                 _Book.publicationDate.between(pastDate, LocalDate.now())),
           by(_Book.title.ascIgnoreCase(),_Book.isbn.asc())))

[Don't focus on the details here, I know @njr-11 has thought about this longer than I have and surely has more-developed ideas about it.]

Of course, this is much less expressive than a real criteria API. But:

Now, on the other hand, it's also completely compatible with a full criteria API, it's not really in competition at all. The objects returned by like() and between() could be criteria expressions.

Anyway, I would love to have this, and I think users would stampede for something like it.

otaviojava commented 8 months ago

That is nice! IIMHO: it would be faster than the query language.

pipinet commented 2 weeks ago

I wonder if it might be helpful to look at how go-jet approaches this?

pipinet commented 2 weeks ago

I have a very, very simple real-life scenario here.

select * from Post
where 
id in (select postId from PostTag where tagId in :tagIds)     #if tagIds is empty, return empty result
and status in (:statuses).   #if statuses is empty, ignore this filter

1、Is there any way to achieve repository more easily? 2、Is there any way to achieve PostQuery more semantic?

@Entity
@Table(name = "post")
public class PostEntity {
    @Id
    @UuidGenerator(style = TIME)
    private String id;
    private String status;
    private OffsetDateTime createdAt;

    ...getter/setter
}

@Entity
@Table(name = "post_tag")
public class PostTag {
    @Id
    @UuidGenerator(style = TIME)
    private String id;
    private String postId;
    private String tagId;

    ...getter/setter
}

public class PostQuery {
    private final Set<String> tagIds;
    private final Set<String> statuses;
      ...getter/setter
}

@Repository
public interface PostRepository extends CrudRepository<Post, String> {
    @Nonnull
    StatelessSession session();

    default Page<Post> pageBy(PostQuery query, PageRequest page) {
        if (query.getTagIds() == null || query.getTagIds().isEmpty()) {
            return new PageRecord<>(request, Collections.emptyList(), 0L);
        }
        CriteriaDefinition<Post> cd = new CriteriaDefinition<>(session().getFactory(), Post.class, 
            """
            select * from Post
            where id in (select postId from PostTag where tagId in :tagIds)
            """) {{
            var post = from(Post.class);
            if(query.getStatuses() != null && !query.getStatuses().isEmpty()){
                restrict(in(post.get(Post_.status), query.getStatuses()));
            }
            orderBy(desc(post.get(Post_.CREATED_AT)));
        }};
        int totalResults = session().createSelectionQuery(cd)
            .setParameter("tagIds", query.getTagIds())
            .getResultCount();
        List<Post> results = session().createSelectionQuery(cd)
            .setParameter("tagIds", query.getTagIds())
            .setFirstResult((int) (page.page() - 1) * page.size())
            .setMaxResults(page.size())
            .getResultList();
        return new PageRecord<>(page, results, totalResults);
    }
}
gavinking commented 2 weeks ago

@pipinet I believe you're looking for https://github.com/jakartaee/data/issues/829