195 lines
5.8 KiB
Markdown
195 lines
5.8 KiB
Markdown
# 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.
|
|
|
|
```java
|
|
@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);
|
|
}
|
|
```
|
|
|
|
```java
|
|
@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.
|
|
|
|
```java
|
|
@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.
|
|
|
|
```java
|
|
// 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.
|
|
|
|
```java
|
|
@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`).
|
|
|
|
```java
|
|
@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);
|
|
```
|
|
|
|
```java
|
|
@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.
|
|
|
|
```java
|
|
@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:
|
|
|
|
```java
|
|
@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:
|
|
```java
|
|
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:
|
|
|
|
```java
|
|
@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:
|
|
|
|
```java
|
|
@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.
|