Skip to content
Martin Ledvinka edited this page Apr 30, 2026 · 17 revisions

We wrote a paper comparing JOPA with other object-triple mapping libraries (such as AliBaba, Empire, KOMMA, RDFBeans). It presents:

  • A feature comparison of the libraries
  • A benchmark showcasing typical application operations - create, retrieve, retrieve all (instances of a specific type), update, delete

More information about the benchmark, the methodology, raw data and the original results can be found at https://kbss.felk.cvut.cz/web/otm-benchmark.

However, the benchmark is quite old. The following table thus provides a perspective on how the performance of JOPA in the benchmark has evolved since version 0.10.7 (the version used in the original benchmark). The benchmark was run with the following setup:

  • Linux Mint 21.3 (kernel 6.8.0-87-generic) - disabled network, no other applications running
  • AMD Ryzen 7 PRO 4750U
  • 32GB RAM
  • GraphDB 10.8.4
JOPA version Create   Retrieve   Retrieve all   Update   Delete  
  Mean/ms σ / ms Mean / ms σ / ms Mean / ms σ / ms Mean / ms σ / ms Mean / ms σ / ms
0.10.7 16694.17 181.4378 11930.94 871.2306 12077.37 921.4616 15142.52 455.5812 25580.1 959.1004
2.6.0 12338.22 637.955 12660.44 420.3243 12991.11 382.5462 15234.75 536.3642 9607.23 520.1247
2.7.0 11915.46 612.0381 12371.51 765.871 12521.72 650.8022 15111.65 522.3286 9819.639 518.1418
2.7.2* 11191.47 486.3428 12422.76 664.9563 12202.79 769.1189 14785.78 543.1824 9432.529 565.5442

*With enabled entity loading optimization (see below)

Entity Loading Query Optimization

By default, a query that returns entities is processed in two phases:

  1. The query is executed, returning only identifiers of the resulting entities
  2. Each resulting entity is loaded using the underlying access API (just as if EntityManager.find was called for the result element)

This makes query result loading typically a little slower and requires more requests to the repository (n+1 where n is the number of result rows). However, it gives JOPA better control over inferred/asserted data to be loaded, cache access and repository contexts.

Nevertheless, it is often more favorable to just load the entities directly from the query result (thus requiring only one request to the repository). Two optimizers are (as of 2.7.1) available for optimized entity loading from query results.

  • Unbound predicate and object optimizer adds, as its name suggests, a triple pattern of the form ?x ?xP ?xV into the query, making the query basically return triples with subject ?x. The entity is then loaded from them. However, this may retrieve more data than expected and could make the query run slow. Therefore, this pattern is used only if the target type has a @Properties field
  • Attribute enumerating optimizer adds optional groups (e.g., OPTIONAL { ?x ex:stringAttribute ?stringAttribute }) for each of the attributes of the target entity class. If the attribute is required (e.g., it has @ParticipationConstraints(nonEmpty=true)), the attribute triple pattern is added without the OPTIONAL (i.e., ?x ex:firstName ?firstName) This narrows the amount of data actually retrieved by the query and should improve the query performance. This is the default optimizer used when the target entity class does not have a @Properties field. This optimizer is also able to work with repository contexts specified by the descriptor passed to the query API.

Take the following typical query - select all instances of a type and see how it is modified by the optimizers:

SELECT ?x WHERE { ?x a ?type . }

# Unbound predicate and object optimizer
SELECT ?x ?xP ?xV WHERE { ?x a ?type . ?x ?xP ?xV . }

# Attribute enumerating optimizer (assume target entity class represents a person with firstName, familyName, accountName
SELECT ?x ?xfirstName ?xfamilyName ?xaccountName WHERE {
  ?x a ?type .
  OPTIONAL { ?x foaf:firstName ?xfirstName }
  OPTIONAL { ?x foaf:familyName ?xfamilyName }
  OPTIONAL { ?x foaf:accountName ?xaccountName }
}

Entity loading optimizers are not used when the query uses OFFSET or LIMIT (or the corresponding Query API methods setFirstResult or setMaxResults are used), because they both influence the number of query result rows and could thus skew the actual results.

It is also necessary to emphasize that using the loading optimizers means that all inferred data are included in the loaded entity (or the query has to be executed without inference at all), so the loaded entities may contain more data than if they were loaded using the default mechanism.

Entity loading query result optimization can be enabled for individual queries using the cz.cvut.kbss.jopa.query.enableEntityLoadingOptimizer query hint (use the QueryHints class for convenience).

From experience, this optimizer does not provide a significant performance boost, likely because of the OPTIONAL clauses in the query that make its execution more complex. So, try to make as many attributes required to reduce the number of OPTIONALs. However, it can significantly reduce the number of repository calls (if that is what you care about).

Fetch Graph-based Entity Loading

Since 2.10.0

A fetch graph may be passed to a query as a hint describing which parts of an object graph related to the query's root should be fetched. This allows JOPA to optimize the fetching by modifying the query in such a way that all the relevant data are loaded as part of the query execution and no additional repository calls are necessary. This has the potential of turning a O(1+n*m) complexity operation (1 for query execution, n root entities matching the query, m eagerly fetched references of each root entity in the object graph) into a O(1) operation (just run the query).

An instance of cz.cvut.kbss.jopa.model.EntityGraph is used to describe the fetch graph and can be obtained from the EntityManager for an entity class. When the EntityGraph instance is passed as a fetch graph hint to the query, JOPA will, based on the EntityGraph, modify the query so that all the attributes and references specified by the EntityGraph are retrieved as part of the query and the result loader will the reconstruct the corresponding entities from them.

Clone this wiki locally