public final class QueryDocumentation extends Base
The Query API is for reading specific resources from Composable Commerce. The resources can be sorted and fetched in batches.
First, you need to specify a query, for example:
Query<Category> query = CategoryQuery.of().byName(Locale.ENGLISH, "demo cat");
See the test code.
Second, you use the sphere client to execute a query request, for example:
CompletionStage<PagedQueryResult<Category>> future = client.execute(query);
See the test code.
The successful execution of a Query
results in PagedQueryResult
of a Composable Commerce resource or view.
While the Query
interface just contains information to execute a query,
the interface QueryDsl
also provides a domain specific language to tune a query.
For most of the HTTP API resources, you can find classes to support you in formulating valid queries (in a sub package queries).
The following snippet creates a query which selects all products without a specific order.
final ProductQuery query = ProductQuery.of();
See the test code.
For some model interfaces companion classes exist (the name of the interface in plural) which provide some default queries, for example for categories:
Query<Category> queryById = CategoryQuery.of().byId("the-id"); Query<Category> queryBySlug = CategoryQuery.of().bySlug(Locale.ENGLISH, "category-slug"); Query<Category> queryByName = CategoryQuery.of().byName(Locale.ENGLISH, "demo cat");
See the test code.
There is a query model class for each queryable model interface, e.g., there is io.sphere.sdk.categories.CategoryQueryModel
for io.sphere.sdk.categories.Category
, which contains a DSL to find and specify the queryable parameters.
QueryPredicate<Category> predicate = CategoryQueryModel.of().name().lang(locale).is("demo cat"); Query<Category> query = CategoryQuery.of().withPredicates(predicate);
See the test code.
The generic code looks verbose, but in the future it will enable powerful type-safe queries with IDE discovery even on deep nested data structures like products. (coming soon)
TheQueryDsl
class, used by the query model classes, sorts by ID by default and it has no offset
or limit specified. The following example shows how to specify sorting, limiting, and skipping pages.
QueryPredicate<Category> predicate = CategoryQueryModel.of().name().lang(locale).is("demo cat"); QuerySort<Category> sortByName = CategoryQueryModel.of().name().lang(locale).sort().desc(); QuerySort<Category> sortById = CategoryQueryModel.of().id().sort().asc(); List<QuerySort<Category>> sort = Arrays.asList(sortByName, sortById);//sort by name desc and then by ID if name is the same long offset = 1;//skip first element - not page long limit = 200;//collect at most 200 entities per request Query<Category> query = CategoryQuery.of(). withPredicates(predicate). withSort(sort). withOffset(offset). withLimit(limit);
See the test code.
QueryDsl
is an interface for immutable objects, so if you call withXyz(value)
it returns a new immutable object:
CategoryQuery query = CategoryQuery.of(); assertThat(query).isNotEqualTo(query.withLimit(30L)); assertThat(query.limit()).isEqualTo(Optional.empty()); assertThat(query.withLimit(30L).limit()).isEqualTo(Optional.of(30));
See the test code.
Query
is an interface you have the opportunity to code
a query without the domain specific language:
import com.fasterxml.jackson.core.type.TypeReference;
import io.sphere.sdk.client.HttpRequestIntent;
import io.sphere.sdk.client.SphereRequestUtils;
import io.sphere.sdk.http.HttpResponse;
import io.sphere.sdk.models.Base;
import io.sphere.sdk.queries.*;
import io.sphere.sdk.http.HttpMethod;
import io.sphere.sdk.json.SphereJsonUtils;
import java.util.Locale;
import static io.sphere.sdk.client.SphereRequestUtils.urlEncode;
public class CategoryByNameQuery extends Base implements Query<Category> { private final Locale locale; private final String name; public CategoryByNameQuery(final Locale locale, final String name) { this.locale = locale; this.name = name; } @Override public HttpRequestIntent httpRequestIntent() { return HttpRequestIntent.of(HttpMethod.GET, "/categories" + urlEncode("name(" + locale.toLanguageTag() + "=\"" + StringQuerySortingModel.escape(name) + "\")")); } @Override public PagedQueryResult<Category> deserialize(final HttpResponse httpResponse) { return SphereJsonUtils.readObject(httpResponse.getResponseBody(), new TypeReference<PagedQueryResult<Category>>() { }); } }
See the test code.
For most common product query use cases, like 'query by slug', helper methods exist as shown in the next example.
final ProductQuery queryBySlug = ProductQuery.of() .bySlug(CURRENT, ENGLISH, "blue-t-shirt");
See the test code.
ProductQuery
anymore but QueryDsl
which does not contain the method ProductQuery.bySlug(io.sphere.sdk.products.ProductProjectionType, java.util.Locale, String)
.
That is due to the implementation of the domain specific language, but it still enables you to configure pagination and sorting.
Important to know is that the QueryDsl
uses immutable objects, so calling ProductQuery.bySlug(io.sphere.sdk.products.ProductProjectionType, java.util.Locale, String)
does not change the internal state of the ProductQuery
, but it creates a new QueryDsl
object with the selected predicate.
For more advanced queries you have to use the Predicate API
. For example, for querying for names you can use:
final QueryPredicate<Product> predicate = ProductQueryModel.of().masterData().current().name() .lang(ENGLISH).isIn(asList("blue t-shirt", "blue jeans")); final ProductQuery query = ProductQuery.of().withPredicates(predicate);
See the test code.
The Predicate API looks verbose, but it prevents you from making typos and it makes the queryable attributes discoverable in your IDE.
You can still create predicates from strings, but you need to escape characters.final QueryPredicate<Product> safePredicate = ProductQueryModel.of().masterData().current().name() .lang(ENGLISH).isIn(asList("blue t-shirt", "blue jeans")); final QueryPredicate<Product> unsafePredicate = QueryPredicate.of("masterData(current(name(en in (\"blue t-shirt\", \"blue jeans\"))))"); assertThat(unsafePredicate).isEqualTo(safePredicate);
See the test code.
For creating predicates for resource <RESOURCE> there is a method model()
in a class <RESOURCE>Query (e.g., Product has ProductQuery).
For connecting predicates use Predicate#and(Predicate)
and Predicate#or(Predicate)
.
The following example shows a query for a product where either the name is "foo" or
the id is "bar".
final QueryPredicate<Product> nameIsFoo = ProductQueryModel.of().masterData().current().name() .lang(ENGLISH).is("foo"); final QueryPredicate<Product> idIsBar = ProductQueryModel.of().id().is("bar"); final ProductQuery query = ProductQuery.of().withPredicates(nameIsFoo.or(idIsBar));
See the test code.
To query for two conditions use the and
operator. The following example queries for the name "foo" and the membership in a category with id "cat1".
final QueryPredicate<Product> nameIsFoo = ProductQueryModel.of().masterData().current().name() .lang(ENGLISH).is("foo"); final Reference<Category> cat1 = Category.reference("cat1"); final QueryPredicate<Product> isInCat1 = ProductQueryModel.of().masterData().current() .categories().isIn(asList(cat1)); final ProductQuery query = ProductQuery.of().withPredicates(nameIsFoo.and(isInCat1));
See the test code.
Since the previous queries have a common path you can combine the two queries in a where
clause, like below. This is not supported for every resource, though.
final Reference<Category> cat1 = Category.reference("cat1"); final QueryPredicate<Product> nameIsFooAndIsInCat1 = ProductQueryModel.of().masterData().current() .where(cur -> cur.name().lang(ENGLISH).is("foo").and(cur.categories().isIn(asList(cat1)))); final ProductQuery query = ProductQuery.of().withPredicates(nameIsFooAndIsInCat1);
See the test code.
QueryPredicate.negate()
:
final QueryPredicate<Product> nameIsFoo = ProductQueryModel.of() .masterData().current().name().lang(ENGLISH).is("foo"); final QueryPredicate<Product> nameIsNotFoo = nameIsFoo.negate(); assertThat(nameIsNotFoo).isEqualTo(QueryPredicate.of("not(masterData(current(name(en=\"foo\"))))"));
See the test code.
m.not()
method which puts the negation at the beginning of the predicate:
final ProductQuery query = ProductQuery.of() .withPredicates(m -> m.not(m.masterData().current().name().lang(ENGLISH).is("foo"))); assertThat(query.predicates()).isEqualTo(asList(QueryPredicate.of("not(masterData(current(name(en=\"foo\"))))")));
See the test code.
Some implementations sort by ID by default, so even if you forget to specify any sorting options, the order of the results will be constant.
To specify the sorting use QueryDsl.withSort(java.util.List)
.
final QuerySort<Product> byNameAsc = ProductQueryModel.of().masterData().current().name() .lang(ENGLISH).sort().asc(); final ProductQuery query = ProductQuery.of().withSort(asList(byNameAsc));
See the test code.
final QuerySort<Product> byNameAsc = ProductQueryModel.of().masterData().current().name() .lang(ENGLISH).sort().asc(); final QuerySort<Product> byIdDesc = ProductQueryModel.of().id().sort().by(DESC); final ProductQuery query = ProductQuery.of().withSort(asList(byNameAsc, byIdDesc));
See the test code.
sort()
method.
If the SDK lacks of a method to create the sort expression, you can still provide it via a string:
final QuerySort<Product> safeSort = ProductQueryModel.of().masterData().current().name() .lang(ENGLISH).sort().asc(); final QuerySort<Product> unsafeSort = QuerySort.of("masterData.current.name.en asc"); assertThat(safeSort).isEqualTo(unsafeSort);
See the test code.
Assumption: in a product query all products are sorted by ID by default.
A query might produce more results than you want to consume or the system lets you consume. At the time of writing you can only fetch up to 500 objects at once.Imagine you have 15 products:
00 01 02 03 04 05 06 07 08 09 10 11 12 13 14
And following query:
final ProductQuery query = ProductQuery.of();
See the test code.
As long as you do not specify a limitation of how many resources should be fetched with one query
by QueryDsl.withLimit(Long)
, the Composable Commerce API will deliver up to 20 items
so that as a result all (15 products) will be loaded.
If you specify a limit of 4 to a query as shown below, this query will only load the first 4 products:
final ProductQuery query = ProductQuery.of().withLimit(4L);
See the test code.
|00 01 02 03|To load the next 4 products you need to define an offset by
QueryDsl.withOffset(Long)
.
final ProductQuery queryForFirst4 = ProductQuery.of().withLimit(4L); final ProductQuery queryForProductId04to07 = queryForFirst4.withOffset(4L); assertThat(queryForProductId04to07).isEqualTo(ProductQuery.of().withLimit(4L).withOffset(4L));
See the test code.
|04 05 06 07|To fetch the products with the ID 08 to 11 you need an offset-parameter of 8.
If you use an offset of 2 and a limit of 4 you will fetch products 02, 03, 04, 05.
00 01|02 03 04 05|06 07 08 09 10 11 12 13 14 skip 2|limit of 4 | not fetched
The fetched result of a query will be a PagedQueryResult
.
PagedQueryResult.getResults()
contains all fetched elements.io.sphere.sdk.queries.PagedQueryResult#size()
.PagedQueryResult.getOffset()
corresponds to the offset of the query.PagedQueryResult.getTotal()
is the amount of resources matching the query but do not necessarily need to be included in the PagedQueryResult
.So, for this example query with offset 2 and limit 4, the PagedQueryResult
will have offset 2, size 4 and total 15. But be careful, count can be smaller than limit in some cases; for example, if total is smaller than the limit (limit 500 but only 15 products). It can also be the case when total is not dividable by limit and the last elements are fetched (.e.g. |12 13 14| with offset 12 and limit 4).
For some cases, like iterating through a whole collection for imports/exports the offset parameter can be avoided by using a predicate id > $lastId.
import io.sphere.sdk.cartdiscounts.CartDiscountFixtures;
import io.sphere.sdk.models.Identifiable;
import io.sphere.sdk.products.Product;
import io.sphere.sdk.queries.PagedQueryResult;
import io.sphere.sdk.queries.PagedResult;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import static java.util.stream.Collectors.toList;
import static org.assertj.core.api.Assertions.assertThat;
public class QueryAllIntegrationTest extends QueryAllBase { //in production code it would higher #smalltestset private static final long PAGE_SIZE = 5; @BeforeClass public static void clean(){ CartDiscountFixtures.deleteDiscountCodesAndCartDiscounts(client()); } @Test public void useIdPredicateInsteadOfOffset() throws Exception { final ProductQuery seedQuery = ProductQuery.of() //the original predicate, which queries products for a certain product type //the idea works also for no predicate to get all products .withPredicates(m -> m.productType().is(productType)) //important, we sort by id, otherwise id > $lastId would not make sense .withSort(m -> m.id().sort().asc()) .withLimit(PAGE_SIZE) .withFetchTotal(false);//saves also resources and time final CompletionStage<List<Product>> resultStage = findNext(seedQuery, seedQuery, new LinkedList<>()); final List<Product> actualProducts = resultStage.toCompletableFuture().join(); assertThat(actualProducts).hasSize(createdProducts.size()); //!!! the underlying database has a different algorithm to sort by ID, it is a UUID, which differs from String sorting final List<Product> javaSortedActual = actualProducts.stream().sorted(BY_ID_COMPARATOR).collect(toList()); assertThat(javaSortedActual).isEqualTo(createdProducts); } private CompletionStage<List<Product>> findNext(final ProductQuery seedQuery, final ProductQuery query, final List<Product> products) { final CompletionStage<PagedQueryResult<Product>> pageResult = sphereClient().execute(query); return pageResult.thenCompose(page -> { final List<Product> results = page.getResults(); products.addAll(results); final boolean isLastQueryPage = results.size() < PAGE_SIZE; if (isLastQueryPage) { return CompletableFuture.completedFuture(products); } else { final String lastId = getIdForNextQuery(page); return findNext(seedQuery, seedQuery .plusPredicates(m -> m.id().isGreaterThan(lastId)), products); } }); } /** * Gets from a {@link PagedResult} the last ID if the result is not the last page. * This does only work correctly if it was sorted by ID in ascending order. * @param pagedResult the result to extract the id * @return the last ID, if present */ private <T extends Identifiable<T>> String getIdForNextQuery(final PagedResult<T> pagedResult) { final List<T> results = pagedResult.getResults(); final int indexLastElement = results.size() - 1; return results.get(indexLastElement).getId(); } }
See the test code.
The documentation for reference expansion have been moved to ReferenceExpansionDocumentation
since was also enabled for search and commands.