Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ Query features:
* filtering
* sorting
* pagination
* projections (select specific fields)
* textual query language
* support for REST APIs
* support for DTO layer
* native database expressions (JPA)
* and more...

All with ONE SINGLE LANGUAGE!
Expand Down Expand Up @@ -128,6 +130,51 @@ Java 14).

> Note the static imports to improve the readability.

#### Projections

Use `selectBy` to retrieve only specific fields instead of full entities:

```java
Query query = Querity.query()
.selectBy("firstName", "lastName", "address.city")
.filter(filterBy("lastName", EQUALS, "Skywalker"))
.build();
List<Map<String, Object>> results = querity.findAllProjected(Person.class, query);
// Each map contains only: {firstName: "...", lastName: "...", city: "..."}
```

#### Native expressions (JPA only)

For advanced use cases, JPA modules support native expressions using `CriteriaBuilder`:

**Native sort with expression:**

```java
// Sort by length of lastName
OrderSpecification<Person> orderSpec = (root, cb) -> cb.asc(cb.length(root.get("lastName")));
Query query = Querity.query()
.sort(sortByNative(orderSpec))
.build();
List<Person> results = querity.findAll(Person.class, query);
```

**Native select with expression:**

```java
// Select concatenated full name
SelectionSpecification<Person> fullNameSpec = AliasedSelectionSpecification.of(
(root, cb) -> cb.concat(cb.concat(root.get("firstName"), " "), root.get("lastName")),
"fullName"
);
Query query = Querity.query()
.select(selectByNative(fullNameSpec))
.build();
List<Map<String, Object>> results = querity.findAllProjected(Person.class, query);
// Each map contains: {fullName: "Luke Skywalker"}
```

> Native expressions are only available for JPA. MongoDB and Elasticsearch support `sortByNative` and `filterByNative` with their respective native types (`Order`, `Criteria`), but not expression-based projections.

#### Query language

The `querity-parser` module provides a simple query language to build a `Query` object,
Expand Down
11 changes: 11 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,17 @@
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco-maven-plugin.version}</version>
<configuration>
<excludes>
<exclude>**/QueryLexer.*</exclude>
<exclude>**/QueryParser.*</exclude>
<exclude>**/QueryParser$*</exclude>
<exclude>**/QueryParserBaseListener.*</exclude>
<exclude>**/QueryParserBaseVisitor.*</exclude>
<exclude>**/QueryParserListener.*</exclude>
<exclude>**/QueryParserVisitor.*</exclude>
</excludes>
</configuration>
<executions>
<execution>
<id>prepare-agent</id>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.github.queritylib.querity.api;

import lombok.*;
import lombok.extern.jackson.Jacksonized;

import java.util.Collections;
import java.util.List;

@Builder(toBuilder = true)
@Jacksonized
@Getter
@EqualsAndHashCode
@ToString
public class NativeSelectWrapper<T> implements Select {
@NonNull
@Singular
private List<T> nativeSelections;

@Override
public List<String> getPropertyNames() {
// Native selects don't have simple property names
return Collections.emptyList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,24 @@

import java.util.Arrays;
import java.util.List;
import java.util.Map;

public interface Querity {
<T> List<T> findAll(Class<T> entityClass, Query query);

<T> Long count(Class<T> entityClass, Condition condition);

/**
* Execute a projection query returning a list of maps with the selected properties.
*
* @param entityClass the entity class to query
* @param query the query with select clause
* @return a list of maps containing the selected properties
*/
default List<Map<String, Object>> findAllProjected(Class<?> entityClass, Query query) {
throw new UnsupportedOperationException("Projection queries are not supported by this implementation");
}

static Query.QueryBuilder query() {
return Query.builder();
}
Expand Down Expand Up @@ -70,6 +82,17 @@ static <T> NativeSortWrapper<T> sortByNative(T nativeSort) {
.build();
}

static SimpleSelect selectBy(String... propertyNames) {
return SimpleSelect.of(propertyNames);
}

@SafeVarargs
static <T> NativeSelectWrapper<T> selectByNative(T... nativeSelections) {
return NativeSelectWrapper.<T>builder()
.nativeSelections(Arrays.asList(nativeSelections))
.build();
}

static Query wrapConditionInQuery(Condition condition) {
return Querity.query()
.filter(condition)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
@Builder(toBuilder = true)
@Jacksonized
@Getter
@EqualsAndHashCode(of = {"filter", "pagination", "sort", "distinct"})
@ToString(of = {"filter", "pagination", "sort", "distinct"})
@EqualsAndHashCode(of = {"filter", "pagination", "sort", "distinct", "select"})
@ToString(of = {"filter", "pagination", "sort", "distinct", "select"})
public class Query {
private final Condition filter;
private final Pagination pagination;
Expand All @@ -23,6 +23,7 @@ public class Query {
@JsonIgnore
private List<QueryPreprocessor> preprocessors;
private boolean distinct;
private final Select select;

public boolean hasFilter() {
return filter != null && !filter.isEmpty();
Expand All @@ -36,6 +37,10 @@ public boolean hasSort() {
return Arrays.stream(sort).anyMatch(s -> true);
}

public boolean hasSelect() {
return select != null;
}

public List<Sort> getSort() {
return Arrays.asList(sort);
}
Expand All @@ -50,6 +55,7 @@ public static class QueryBuilder {
@SuppressWarnings({"java:S1068", "java:S1450"})
private Sort[] sort = new Sort[0];
private List<QueryPreprocessor> preprocessors = new ArrayList<>();
private Select select;

public QueryBuilder withPreprocessor(QueryPreprocessor preprocessor) {
this.preprocessors.add(preprocessor);
Expand All @@ -70,6 +76,16 @@ public QueryBuilder pagination(Integer page, Integer pageSize) {
this.pagination = Querity.paged(page, pageSize);
return this;
}

public QueryBuilder select(Select select) {
this.select = select;
return this;
}

public QueryBuilder selectBy(String... propertyNames) {
this.select = Querity.selectBy(propertyNames);
return this;
}
}

public Query preprocess() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.github.queritylib.querity.api;

import java.util.List;

/**
* Interface for select options.
* Implementations include {@link SimpleSelect} for property-based selection
* and {@link NativeSelectWrapper} for native selection.
*/
public interface Select {
/**
* Get the property names to select.
* @return list of property names
*/
List<String> getPropertyNames();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.github.queritylib.querity.api;

import lombok.*;
import lombok.extern.jackson.Jacksonized;

import java.util.Arrays;
import java.util.List;

@Builder(toBuilder = true)
@Jacksonized
@Getter
@EqualsAndHashCode
@ToString
public class SimpleSelect implements Select {
@NonNull
@Singular
private List<String> propertyNames;

public static SimpleSelect of(String... propertyNames) {
return SimpleSelect.builder()
.propertyNames(Arrays.asList(propertyNames))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
package io.github.queritylib.querity.api;

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

/**
* Marker interface for sort options.
* Interface for sort options.
* Implementations include {@link SimpleSort} for property-based sorting
* and {@link NativeSortWrapper} for native sorting.
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes({
@JsonSubTypes.Type(SimpleSort.class),
@JsonSubTypes.Type(NativeSortWrapper.class)
})
public interface Sort {
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.github.queritylib.querity.api;

import org.junit.jupiter.api.Test;

import java.util.List;

import static io.github.queritylib.querity.api.Querity.selectByNative;
import static org.assertj.core.api.Assertions.assertThat;

class NativeSelectWrapperTest {

@Test
void givenNativeSelections_whenSelectByNative_thenReturnNativeSelectWrapper() {
NativeSelectWrapper<String> wrapper = selectByNative("field1", "field2");

assertThat(wrapper).isNotNull();
assertThat(wrapper.getNativeSelections()).containsExactly("field1", "field2");
}

@Test
void givenNativeSelectWrapper_whenGetPropertyNames_thenReturnEmptyList() {
NativeSelectWrapper<String> wrapper = selectByNative("field1");

assertThat(wrapper.getPropertyNames()).isEmpty();
}

@Test
void givenNativeSelectWrapper_whenImplementsSelect_thenTrue() {
NativeSelectWrapper<String> wrapper = selectByNative("field1");

assertThat(wrapper).isInstanceOf(Select.class);
}

@Test
void givenTwoEqualNativeSelectWrappers_whenEquals_thenReturnTrue() {
NativeSelectWrapper<String> wrapper1 = selectByNative("field1", "field2");
NativeSelectWrapper<String> wrapper2 = selectByNative("field1", "field2");

assertThat(wrapper1).isEqualTo(wrapper2);
assertThat(wrapper1.hashCode()).isEqualTo(wrapper2.hashCode());
}

@Test
void givenNativeSelectWrapper_whenToString_thenContainsSelections() {
NativeSelectWrapper<String> wrapper = selectByNative("field1", "field2");

assertThat(wrapper.toString()).contains("field1", "field2");
}

@Test
void givenNativeSelectWrapper_whenToBuilder_thenCanModify() {
NativeSelectWrapper<String> original = selectByNative("field1");
NativeSelectWrapper<String> modified = original.toBuilder()
.nativeSelection("field2")
.build();

assertThat(original.getNativeSelections()).containsExactly("field1");
assertThat(modified.getNativeSelections()).containsExactly("field1", "field2");
}
}
Loading
Loading