Skip to content

Query result mapping

Martin Ledvinka edited this page Jun 29, 2026 · 5 revisions

Query results can be mapped to objects in a couple of ways.

Note that currently only SPARQL queries are supported.

TypedQuery Result Mapping

Entity Mapping

By default, TypedQuery results are directly mapped to entities. The query should select only the entity identifier, JOPA will take care of loading the corresponding entity based on the query result type.

A find all query can thus look like this:

List<Person> result = em.createNativeQuery("SELECT ?x WHERE { ?x a foaf:Person . }", Person.class).getResultList();

What this does is that it runs the query and then loads the entity for each result row (basically corresponding to calling EntityManager.find). Query hints can modify this behavior to make JOPA select multiple relevant attributes in a single query and reconstruct the instances from them.

Entity Graphs

An EntityGraph can be used to specify which attributes of an entity should be loaded. This allows overriding the default eager/lazy fetch behavior. Moreover, when an EntityGraph is passed to a query as a hint, the query is automatically modified by JOPA so that the relevant instance data are returned by the query and additional repository calls are not necessary. See the Perfomance page for details.

Consider an object model based on the following UML class diagram:

image

Let's assume we are interested only in basic metadata of each person, and we do not need info about their phones (for example for a table with the list of all persons in the system, phone would be relevant only when the user would navigate to the person's detail page). We do not want to change the fetch type for Person.hasPhone. In that case, we could create an EntityGraph as follows and then pass it as fetch graph hint to the query API:

EntityManager em = // obtain entity manager
EntityGraph<Person> fetchGraph = em.createEntityGraph(Person.class);
fetchGraph.addAttributeNodes("firstName", "lastName", "username");

If our entity graph should also fetch data of a referenced entity, we need to add corresponding subgraphs. For example:

EntityManager em = // obtain entity manager
EntityGraph<Person> fetchGraph = em.createEntityGraph(Person.class);
fetchGraph.addAttributeNodes("firstName", "lastName", "username");
Subgraph<Phone> phoneGraph = fetchGraph.addSubgraph("hasPhone");
phoneGraph.addAttributeNodes("model", "serialNumber");

Metamodel attributes can be used instead of attribute names when constructing the entity graph as well.

Programmatic creation of entity graphs provides the most flexibility but can often be tedious. Another option is to declare the entity graph as a @NamedEntityGraph and refer to it by name (similar to how named queries work). Here is an equivalent named entity graph declaration and another one where the phones model and serial number is retrieved:

@NamedEntityGraphs({
    @NamedEntityGraph(name = "Person.findBare", attributeNodes = {
        @NamedAttributeNode("firstName"),
        @NamedAttributeNode("lastName"),
        @NamedAttributeNode("username")
    }),
    @NamedEntityGraph(name = "Person.findWithPhone", attributeNodes = {
        @NamedAttributeNode("firstName"),
        @NamedAttributeNode("lastName"),
        @NamedAttributeNode("username"),
        @NamedAttributeNode(value = "phone", subgraph = "Phone"),
    },  subgraphs = {
        @NamedSubgraph(name = "Phone", attributeNodes = {
            @NamedAttributeNode("model"),
            @NamedAttributeNode("serialNumber")
        })
    }),
})

Typically, you would place this annotation on the corresponding entity class, but it is not strictly required. Such named entity graph can be retrieved from an entity manager by its name:

EntityManager em = // obtain entity manager
EntityGraph<?> fetchGraph = em.getEntityGraph("Person.findBare");

Direct Mapping to Entity Attributes

Since version 2.9.0, JOPA can also map query results directly to an entity, without making any additional calls. This approach requires the query to project variables whose names correspond to the target entity attributes. So, for example:

List<Person> result = em.createNativeQuery("""
                                              SELECT ?x ?firstName ?lastName ?username WHERE {
                                                ?x a foaf:Person ;
                                                  foaf:givenName ?firstName ;
                                                  foaf:familyName ?lastName ;
                                                  foaf:accountName ?username ;
                                                  a ?types .
                                              }""", Person.class).getResultList();

This has the advantage of not requiring additional repository calls. However, note that this also has possible drawbacks:

  • Queries are by default run with inference, so the result may contain unintentional inferred data
  • If entity data are in named graphs, the query must specify them
  • If any of the attributes may be without value, the query must use OPTIONAL to specify that
  • The mapping does not extend to references, only the root entity can be mapped like this

It is thus generally advised to not use the results of such queries in update transactions.

Other Types

TypedQuery results can be also mapped to other, non-entity types. This mainly applies to primitive type wrappers, String or identifier types - URI and URL.

An example is a ASK query result mapping:

Boolean exists = em.createNativeQuery("ASK { ?x a foaf:Person . }", Boolean.class).getSingleResult();

SPARQL Result Set Mapping

JOPA also allows to map query results to non-entity objects or provide a custom mapping where, for example, object attributes can be fetched directly in the query. This is achieved using the SparqlResultSetMapping annotation, which works similarly to SqlResultSetMapping in JPA.

For instance, using a constructor mapping, an object can be constructed from the query projection variables by using the following mapping:

@SparqlResultSetMapping(name = "RdfsResource", 
   classes = {@ConstructorResult(targetClass = RdfsResource.class,
                                 variables = {                                                                   
                  @VariableResult(name = "uri", type = URI.class),
                  @VariableResult(name = "label", type = String.class),
                  @VariableResult(name = "comment", type = String.class)})})

A corresponding constructor must exist in the target class:

public RdfsResource(URI uri, String label, String comment) {
        this.uri = uri;
        this.label = label;
        this.comment = comment;
    }

Other means of using SparqlResultSetMapping include mapping to entities and variable mapping, again, similar to JPA. More info can be found in Javadoc.

Clone this wiki locally