clotho/docs/QUERY_MODEL.md
2026-05-15 00:09:01 +02:00

5.8 KiB

Query Model

Philosophy

Graph databases excel at complex relationship queries. That complexity belongs in the developer's Gremlin code, not inside the ORM. Clotho's query model is a bridge:

  • Developer writes a Gremlin traversal
  • Clotho executes it and maps the results back to registered Java types
  • Developer injects the repository interface like any Spring bean

@ClothoRepository Interfaces

The primary way to define queries. Clotho generates a dynamic proxy at startup.

@ClothoRepository
public interface EmployeeQueries {

    @Query("g.V().hasLabel('Employee').has('dept',:dept)")
    List<Employee> findByDepartment(@Param("dept") String dept);

    @Query("""
        g.V().has('Employee','id',:id)
         .repeat(out('MANAGES')).times(2).emit()
         .dedup()
    """)
    List<Employee> findTwoLevelsOfReports(@Param("id") String managerId);
}
@Autowired EmployeeQueries employeeQueries;

List<Employee> staff = employeeQueries.findByDepartment("Engineering");

Parameter Binding

Use :paramName placeholders in the Gremlin string and @Param("paramName") on the corresponding method parameter.

@Query("g.V().has('Order','status',:status).has('total',gt(:minTotal))")
List<Order> findByStatusAndMinTotal(
    @Param("status") String status,
    @Param("minTotal") BigDecimal minTotal);

Clotho substitutes parameters before executing the traversal. Parameter types are passed as-is to Gremlin — use Java types that Gremlin/ArcadeDB understands (String, int, long, double, boolean, LocalDate, etc.).


Return Type Rules

The declared return type tells Clotho how to map the traversal result.

@VertexType or @EdgeType class

Clotho maps each result element to the most specific registered class matching its label. The declared type is the upper bound.

// May return Employee, HourlyEmployee, SalariedEmployee — all ClothoVertex subtypes
List<Employee> findAll();

// Explicitly mixed: any registered @VertexType
List<ClothoVertex> findAllVertices();

When the return type is a single object (not a List), Clotho returns the first result or null if none.

@Nullable
@Query("g.V().has('Employee','id',:id)")
Employee findById(@Param("id") String id);

@SubgraphType class

When the return type is annotated @SubgraphType, Clotho expects the traversal to return a select() map. It maps each key to the matching field by name (or by @Alias).

@Query("""
    g.V().has('Employee','id',:id).as('employee')
     .out('MANAGED_BY').as('manager')
     .out('IN_DEPT').as('dept')
     .select('employee','manager','dept')
""")
EmployeeProfile loadProfile(@Param("id") String id);
@SubgraphType
public class EmployeeProfile {
    Employee employee;              // bound to key "employee"
    Manager manager;                // bound to key "manager"
    @Alias("dept") Department department;  // bound to key "dept"
}

If the traversal returns a list of select maps, declare List<EmployeeProfile>.

Raw result (no mapping)

Set mapResult = false to receive the Gremlin result directly without Clotho mapping. Return type can be a scalar, List<Object>, or any type Gremlin produces.

@Query(value = "g.V().hasLabel('Employee').count()", mapResult = false)
long countEmployees();

@Query(value = "g.V().has('Order','id',:id).values('status')", mapResult = false)
String getOrderStatus(@Param("id") String id);

Named Queries on @VertexType Classes

An alternative to repository interfaces for simple per-type queries. Place @Query directly on the vertex class with a name attribute:

@VertexType("Order")
@Query(name = "openForCustomer",
       gremlin = "g.V().has('Customer','id',:custId).out('PLACED').has('status','OPEN')")
public class Order extends ClothoVertex { ... }

Invoke via the session:

List<Order> open = session.query(Order.class, "openForCustomer",
    Map.of("custId", customerId));

Note on magic strings: the query name "openForCustomer" is a raw string. Prefer defining it as a constant on the class:

@VertexType("Order")
@Query(name = Order.QUERY_OPEN_FOR_CUSTOMER, ...)
public class Order extends ClothoVertex {
    public static final String QUERY_OPEN_FOR_CUSTOMER = "openForCustomer";
}

// Call site:
session.query(Order.class, Order.QUERY_OPEN_FOR_CUSTOMER, params);

Multiple @Query annotations on a class are allowed (the annotation is @Repeatable).


Gremlin Traversal Source

The GraphTraversalSource (g) is managed by ClothoSessionFactory. It is configured once and shared across sessions. Clotho configures the traversal source to use the remote ArcadeDB connection.

For advanced cases where a developer needs direct traversal source access:

@Autowired ClothoSessionFactory factory;

GraphTraversalSource g = factory.traversalSource();
// Write raw Gremlin, then map manually:
List<ClothoVertex> results = factory.currentSession()
    .mapVertices(ClothoVertex.class, g.V().has("name", "Alice").out("KNOWS"));

Mapping Detail: How Clotho Maps a Gremlin Vertex to a Java Object

  1. Read the vertex label from the TinkerPop Vertex object
  2. Look up the label in the TypeRegistry → find the most specific registered class
  3. Construct an instance of that class (via reflection; Lombok builder support planned)
  4. Set the ARID from the vertex ID
  5. For each @Property field on the class: read the property from the vertex, set the field
  6. For each @Include field: recursively execute the boundary traversal (see OBJECT_BOUNDARY.md)
  7. Register the instance in the session identity map keyed by ARID
  8. Return the assembled instance

If the same ARID is encountered again during step 6, the identity map returns the already- assembled instance without re-fetching from the DB.