DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • How to Identify the Underlying Causes of Connection Timeout Errors for MongoDB With Java
  • Build a REST API With Just 2 Classes in Java and Quarkus
  • High-Performance Reactive REST API and Reactive DB Connection Using Java Spring Boot WebFlux R2DBC Example
  • Four Essential Tips for Building a Robust REST API in Java

Trending

  • DuckDB for Python Developers
  • Context Is the New Schema
  • Java Backend Development in the Era of Kubernetes and Docker
  • Improving Java Application Reliability with Dynatrace AI Engine
  1. DZone
  2. Coding
  3. Java
  4. Translating OData Queries to MongoDB in Java With Jamolingo

Translating OData Queries to MongoDB in Java With Jamolingo

If you want to support dynamic API queries using OData in a Java application backed by MongoDB, Jamolingo provides a lightweight and framework-agnostic solution.

By 
Szymon Tarnowski user avatar
Szymon Tarnowski
DZone Core CORE ·
Apr. 09, 26 · Analysis
Likes (1)
Comment
Save
Tweet
Share
2.7K Views

Join the DZone community and get the full member experience.

Join For Free

Modern APIs often need to support dynamic filtering, sorting, and pagination without creating dozens of custom endpoints. One of the most widely used standards for this is OData (Open Data Protocol). OData has established itself as a powerful standard for building and consuming RESTful APIs. It provides a uniform way to query and manipulate data, offering clients unparalleled flexibility through system query options like $filter, $select, and $expand.

Example: 

Plain Text
 
GET /products?$filter=price gt 100 and category eq 'electronics'&$orderby=price desc&$top=10


This approach allows API consumers to control how data is filtered and retrieved while keeping the API surface simple.

However, implementing OData support in Java applications backed by MongoDB is not trivial. Developers must translate OData expressions into MongoDB queries, which requires parsing the query syntax into efficient MongoDB aggregation pipelines.

Why OData for APIs?

OData provides a standardized way for clients to query data through REST APIs.

Common query parameters include:

  • $filter
  • $orderby
  • $select
  • $top
  • $skip
  • $expand

Example request:

Plain Text
 
GET /orders?$filter=amount gt 500 and status eq 'PAID'


This eliminates the need for multiple endpoints, such as:

Plain Text
 
GET /orders/high-value GET /orders/paid GET /orders/high-value-paid


Instead, the client constructs the query dynamically.

The Challenge With MongoDB

MongoDB uses a completely different query syntax.

Example MongoDB filter:

JSON
 
{  "amount": { "$gt": 500 },  "status": "PAID" }


Mapping OData expressions to MongoDB queries requires:

  • Parsing OData syntax
  • Building a query AST
  • Translating operators
  • Producing BSON filters

Implementing this logic manually can quickly become complex.

Existed Solutions 

While OData is widely supported across various ecosystems, the "OData-to-NoSQL" bridge is often a missing link in Java.

  • Some enterprise platforms provide query translators, such as data virtualization tools that expose databases through OData interfaces. However, these solutions are typically heavy and not designed for embedding inside microservices. Most OData implementations in Java (like Olingo's JPA processor) are tightly coupled to SQL databases and JPA. Developers using MongoDB often find themselves writing manual, error-prone translation logic.
  • Node.js Ecosystem: Libraries like odata-v4-mongodb or odatafy-mongodb exist, but require a JavaScript runtime, which isn't ideal for enterprise Java applications.
  • C#/.NET: Microsoft's OData implementation is powerful but inherently bound to the .NET ecosystem and heavily optimized for Entity Framework.

Jamolingo

Jamolingo is a lightweight, framework-agnostic Java library designed to bridge the gap between the OData specification and MongoDB. By leveraging the industry-standard Apache Olingo library for OData parsing, Jamolingo provides a robust engine for translating OData concepts directly into MongoDB's native query language (BSON-based aggregation pipelines). It fills a void for Java developers, providing a type-safe, performant, and highly customizable translation layer that respects the nuances of both OData and MongoDB.

Key Features of Jamolingo

The main module project is the core module, which contains functionalities like:

  • Advanced filtering: Translates $filter expressions (including eq, ne, startswith, contains, endswith, etc, any/all collection operators, etc.) into MongoDB $match stages.
  • Projection and sorting: Effortlessly converts $select to $project and $orderby to $sort.
  • Paging: Built-in support for $top and $skip.
  • EDM mapping: Automatically maps your OData Entity Data Model (EDM) to MongoDB document structures, with support for field name overrides and complex nested types.

Jamolingo also includes a perf module that helps analyze the potential performance of MongoDB aggregation pipelines before they are executed. This capability can be particularly valuable for APIs that expose flexible query capabilities to clients.

One of the module’s key features is verifying whether the MongoDB query planner would use an index for a given aggregation pipeline. If the query does not reference indexed fields, the application can reject it early and return an informative error response to the API client. This helps prevent expensive or potentially harmful queries from being executed through a REST API.

However, even queries that use indexes are not always guaranteed to perform well. An indexed $match stage may still return large numbers of documents, which can lead to expensive processing in later stages of the aggregation pipeline. MongoDB also imposes a 100 MB in-memory limit for aggregation stages. If this limit is exceeded, the pipeline can still execute when disk usage is enabled, but it will no longer run entirely in memory, which may lead to noticeable performance degradation.

To help evaluate such situations, the perf module can construct a simplified pipeline consisting of the indexed $match stage (or the portion used by the query planner) followed by a $count stage. Executing this pipeline allows the application to estimate how many documents the full aggregation pipeline might process. Based on that estimate, the application can decide whether the query is likely to be too expensive to execute.

The ability to resolve the indexed $match stage from the aggregation pipeline is currently experimental, but it enables interesting optimization strategies. For example, once the indexed $match stage is identified, the main pipeline can be enhanced by introducing a $project stage immediately afterward to reduce the size of documents processed by later stages. In complex pipelines with many computation operations, this can significantly reduce memory usage and processing time. The pipeline may then operate on smaller documents, while the full documents are fetched later (for example, using $lookup) when returning paginated results.

Overall, the perf module introduces an interesting concept: allowing applications to estimate the cost of MongoDB aggregation pipelines before executing them, which can help protect APIs from inefficient or potentially expensive queries.

How to Start Using Jamolingo in Your Project

Assuming that you use Maven to build the project, add the following dependencies:

XML
 
<dependency>
    <groupId>com.github.starnowski.jamolingo</groupId>
    <artifactId>core</artifactId>
    <version>0.7.0</version>
</dependency>
<!-- Optional: for performance validation -->
<dependency>
    <groupId>com.github.starnowski.jamolingo</groupId>
    <artifactId>perf</artifactId>
    <version>0.7.0</version>
</dependency>


The code examples below are for a Spring framework application, but there is also a demo application that uses the Quarkus framework. 

Add a code component responsible for parsing and translating the request query part to the MongoDB aggregation pipeline:

Java
 
import com.github.starnowski.jamolingo.core.context.DefaultEdmMongoContextFacade;
import com.github.starnowski.jamolingo.core.operators.count.OdataCountToMongoCountParser;
import com.github.starnowski.jamolingo.core.operators.filter.ODataFilterToMongoMatchParser;
import com.github.starnowski.jamolingo.core.operators.orderby.OdataOrderByToMongoSortParser;
import com.github.starnowski.jamolingo.core.operators.select.OdataSelectToMongoProjectParser;
import com.github.starnowski.jamolingo.core.operators.skip.OdataSkipToMongoSkipParser;
import com.github.starnowski.jamolingo.core.operators.top.OdataTopToMongoLimitParser;
import java.util.ArrayList;
import java.util.List;
import org.apache.olingo.commons.api.edm.Edm;
import org.apache.olingo.server.api.OData;
import org.apache.olingo.server.api.ODataApplicationException;
import org.apache.olingo.server.api.uri.UriInfo;
import org.apache.olingo.server.api.uri.queryoption.expression.ExpressionVisitException;
import org.apache.olingo.server.core.uri.parser.Parser;
import org.apache.olingo.server.core.uri.parser.UriParserException;
import org.apache.olingo.server.core.uri.validator.UriValidationException;
import org.bson.conversions.Bson;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class ODataQueryService {

  @Autowired private Edm edm;

  @Autowired private DefaultEdmMongoContextFacade edmMongoContextFacade;

  private final ODataFilterToMongoMatchParser filterParser = new ODataFilterToMongoMatchParser();
  private final OdataSelectToMongoProjectParser selectParser =
      new OdataSelectToMongoProjectParser();
  private final OdataOrderByToMongoSortParser orderByParser = new OdataOrderByToMongoSortParser();
  private final OdataTopToMongoLimitParser topParser = new OdataTopToMongoLimitParser();
  private final OdataSkipToMongoSkipParser skipParser = new OdataSkipToMongoSkipParser();
  private final OdataCountToMongoCountParser countParser = new OdataCountToMongoCountParser();

  public static class QueryPlan {
    private final List<Bson> dataPipeline;
    private final List<Bson> countPipeline;
    private final boolean countRequested;

    public QueryPlan(List<Bson> dataPipeline, List<Bson> countPipeline, boolean countRequested) {
      this.dataPipeline = dataPipeline;
      this.countPipeline = countPipeline;
      this.countRequested = countRequested;
    }

    public List<Bson> getDataPipeline() {
      return dataPipeline;
    }

    public List<Bson> getCountPipeline() {
      return countPipeline;
    }

    public boolean isCountRequested() {
      return countRequested;
    }
  }

  public QueryPlan buildQueryPlan(String query)
      throws UriParserException,
          UriValidationException,
          ODataApplicationException,
          ExpressionVisitException {
    UriInfo uriInfo = new Parser(edm, OData.newInstance()).parseUri("examples2", query, null, null);
    List<Bson> filterStages =
        filterParser.parse(uriInfo.getFilterOption(), edmMongoContextFacade).getStageObjects();

    List<Bson> dataPipeline = new ArrayList<>(filterStages);
    // 2. $orderby -> $sort
    dataPipeline.addAll(
        orderByParser.parse(uriInfo.getOrderByOption(), edmMongoContextFacade).getStageObjects());

    // 3. $skip -> $skip
    dataPipeline.addAll(skipParser.parse(uriInfo.getSkipOption()).getStageObjects());

    // 4. $top -> $limit
    dataPipeline.addAll(topParser.parse(uriInfo.getTopOption()).getStageObjects());

    // 5. $select -> $project
    dataPipeline.addAll(
        selectParser.parse(uriInfo.getSelectOption(), edmMongoContextFacade).getStageObjects());

    List<Bson> countPipeline = new ArrayList<>(filterStages);
    countPipeline.add(new org.bson.Document("$count", "count"));

    boolean countRequested =
        uriInfo.getCountOption() != null && uriInfo.getCountOption().getValue();

    return new QueryPlan(dataPipeline, countPipeline, countRequested);
  }
}


And what is missing is just to add the RestController component.

Java
 
import com.github.starnowski.jamolingo.perf.ExplainAnalyzeResult;
import com.github.starnowski.jamolingo.perf.ExplainAnalyzeResultFactory;
import jakarta.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.bson.Document;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {

  @Autowired private ODataQueryService oDataQueryService;

  @Autowired private MongoTemplate mongoTemplate;

  @GetMapping("/query-with-dollar-parameters")
  public Map<String, Object> queryWithDollarParameterOperators(HttpServletRequest request)
      throws Exception {
    ODataQueryService.QueryPlan plan = oDataQueryService.buildQueryPlan(request.getQueryString());
    return executeQueryPlan(plan);
  }

  private Map<String, Object> executeQueryPlan(ODataQueryService.QueryPlan plan) {
    Map<String, Object> response = new LinkedHashMap<>();

    if (plan.isCountRequested()) {
      List<Document> countResult = new ArrayList<>();
      mongoTemplate.getCollection("items").aggregate(plan.getCountPipeline()).into(countResult);
      long totalCount =
          countResult.isEmpty() ? 0 : ((Number) countResult.get(0).get("count")).longValue();
      response.put("@odata.count", totalCount);
    }

    List<Document> results = new ArrayList<>();
    mongoTemplate.getCollection("items").aggregate(plan.getDataPipeline()).into(results);
    response.put("value", results);
    return response;
  }
}


To check the rest of the code and usage of the perf module, please check the demo application.

Conclusion

Supporting flexible queries in APIs can significantly improve developer and client experience, but implementing query parsing and translation manually is complex. 

Jamolingo is more than just a translator; it's a specialized toolset that empowers Java developers to build flexible, OData-compliant APIs on top of MongoDB without sacrificing performance or maintainability. Whether you are building a modern microservice or a complex data platform, Jamolingo offers a practical way to deliver that without introducing unnecessary complexity.

API MongoDB REST Java (programming language)

Opinions expressed by DZone contributors are their own.

Related

  • How to Identify the Underlying Causes of Connection Timeout Errors for MongoDB With Java
  • Build a REST API With Just 2 Classes in Java and Quarkus
  • High-Performance Reactive REST API and Reactive DB Connection Using Java Spring Boot WebFlux R2DBC Example
  • Four Essential Tips for Building a Robust REST API in Java

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook