# Getting Started with Clotho Clotho is a Java OGM (Object-Graph Mapper) for ArcadeDB built on Apache TinkerPop/Gremlin. A "Clotho object" is not a single vertex — it is a **subgraph**: a root vertex plus a declared set of edges and adjacent vertices that are loaded atomically in a single Gremlin traversal. --- ## Prerequisites | Requirement | Version | |---|---| | Java | 21 | | Maven | 3.9+ | | ArcadeDB | any recent release | | Spring Boot | 3.x (optional — Clotho also works standalone) | Clotho is **schema-first**: it maps to a schema you have already created in ArcadeDB. It does not generate DDL. --- ## 1. Maven dependency ```xml com.binarygolem.clotho clotho-spring-boot-starter 0.1.0-SNAPSHOT ``` Use `clotho-core` instead if you are not using Spring Boot. --- ## 2. ArcadeDB schema Create the required types in ArcadeDB before starting your application. Use the ArcadeDB console, HTTP API, or any migration tool: ```sql -- Vertices CREATE VERTEX TYPE Person; CREATE PROPERTY Person.name STRING; CREATE PROPERTY Person.email STRING; CREATE PROPERTY Person.age INTEGER; CREATE VERTEX TYPE Company; CREATE PROPERTY Company.name STRING; CREATE PROPERTY Company.industry STRING; -- Edge CREATE EDGE TYPE WORKS_AT; CREATE PROPERTY WORKS_AT.role STRING; CREATE PROPERTY WORKS_AT.startYear INTEGER; ``` --- ## 3. Spring Boot configuration ```properties # application.properties clotho.gremlin.url=ws://localhost:8182/gremlin clotho.gremlin.username=root clotho.gremlin.password=playwithdata clotho.gremlin.traversal-source=g # Packages to scan for @VertexType, @EdgeType, @ClothoRepository clotho.packages=com.example.myapp # Whether unknown graph labels cause an exception (STRICT) or map to a generic type (IGNORE_AND_MAP_TO_GENERIC) clotho.unknown-type-behavior=STRICT # Default traversal direction for @Include fields that have no @Direction annotation clotho.default-direction=OUT ``` --- ## 4. Define entity classes ### Vertex type ```java @VertexType("Person") @Getter public class Person extends ClothoVertex { @Property("name") private @Nullable String name; @Property("email") private @Nullable String email; @Property("age") private @Nullable Integer age; // Eagerly loaded when Person is fetched — single Gremlin project() query @Include @Via("WORKS_AT") private @Nullable List jobs; // Manual setters call notifyFieldChanged() so Clotho tracks which fields changed. // Do NOT use Lombok @Setter on @Property, @Include, @OutVertex, @InVertex, or @Version fields. public void setName(@Nullable String name) { this.name = name; notifyFieldChanged("name"); } public void setEmail(@Nullable String email) { this.email = email; notifyFieldChanged("email"); } public void setAge(@Nullable Integer age) { this.age = age; notifyFieldChanged("age"); } public void setJobs(@Nullable List jobs) { this.jobs = jobs; notifyFieldChanged("jobs"); } } ``` ### Edge type ```java @EdgeType("WORKS_AT") @Getter public class WorksAt extends ClothoEdge { @Property("role") private @Nullable String role; @Property("startYear") private @Nullable Integer startYear; @OutVertex private @Nullable Person person; // populated when edge is loaded via @Include @InVertex private @Nullable Company company; // populated when edge is loaded via @Include public void setRole(@Nullable String role) { this.role = role; notifyFieldChanged("role"); } public void setStartYear(@Nullable Integer startYear) { this.startYear = startYear; notifyFieldChanged("startYear"); } public void setCompany(@Nullable Company company) { this.company = company; notifyFieldChanged("company"); } } ``` --- ## 5. Using the session `ClothoSession` is the primary unit of work. Open one per logical operation, then close it. Sessions are **not thread-safe** — use one per thread or virtual thread. ```java @Service @RequiredArgsConstructor public class PersonService { private final ClothoSessionFactory sessionFactory; /** Create a person with an employment edge. */ public void createPersonWithJob(String name, String role, String companyName) { try (ClothoSession session = sessionFactory.openSession()) { Company company = new Company(); company.setName(companyName); company.setIndustry("Technology"); WorksAt job = new WorksAt(); job.setRole(role); job.setStartYear(2024); job.setCompany(company); // @InVertex — resolved during edge creation Person person = new Person(); person.setName(name); person.setJobs(List.of(job)); session.save(person); // cascade: person → job → company (all created in order) System.out.println("Created: " + person.getId()); } } /** Load a person — jobs are populated atomically with the vertex. */ public Person findById(ARID id) { try (ClothoSession session = sessionFactory.openSession()) { return session.load(Person.class, id); // person.getJobs() is already populated — no extra round-trip } } /** Update — only the fields you changed are written. */ public void promoteEmployee(ARID personId, String newRole) { try (ClothoSession session = sessionFactory.openSession()) { Person person = session.load(Person.class, personId); if (person.getJobs() != null && !person.getJobs().isEmpty()) { person.getJobs().get(0).setRole(newRole); session.save(person); // only role is written; no other properties touched } } } /** Auto-save — dirty MANAGED entities are saved when the session closes. */ public void updateEmailAutoSave(ARID personId, String newEmail) { try (ClothoSession session = sessionFactory.openSession()) { Person person = session.load(Person.class, personId); person.setEmail(newEmail); // no explicit save() — session.close() persists dirty entities } } /** Delete a person — ArcadeDB also removes all incident edges. */ public void deletePerson(ARID id) { try (ClothoSession session = sessionFactory.openSession()) { Person person = session.load(Person.class, id); session.delete(person); } } } ``` --- ## 6. Session lifecycle ### What a session is A `ClothoSession` is a unit of work. It wraps a graph connection, keeps an **identity map** (same ARID → same Java instance within a session), tracks dirty fields, and auto-saves managed entities when it closes. Sessions are **not thread-safe** — never share one across threads. ### Short-lived sessions (recommended default) Open a session with try-with-resources. The session closes — and auto-saves any dirty entities — at the end of the block: ```java try (ClothoSession session = sessionFactory.openSession()) { Person alice = session.load(Person.class, id); alice.setEmail("new@example.com"); } // auto-saves alice here ``` This is the right pattern for web request handlers, background jobs, and any operation with a clear start and end. ### Long-lived sessions For a simple single-threaded process (a CLI tool, a startup task, a scheduled job that runs on one thread), you can keep one session open for as long as you need: ```java ClothoSession session = sessionFactory.openSession(); // ... do many things ... session.close(); // or call in a shutdown hook ``` The trade-off is that the **identity map grows indefinitely** — every entity loaded stays in memory until the session closes. For long-running processes that load many entities, call `session.clearCache()` periodically to release them without closing the session: ```java // After a large batch operation — drop all cached instances from memory. // The next load() for any of those IDs will re-fetch from the graph. session.clearCache(); ``` `clearCache()` does NOT flush dirty fields first. Call `session.save(entity)` explicitly for anything you want to keep before clearing. ### Do you ever need more than one session? **Multiple threads: yes, one per thread.** Sessions bind to the current thread via a `ThreadLocal`. Start a new session on each thread (or virtual thread) and close it when the thread's work is done. ```java // With virtual threads (Java 21+): try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { executor.submit(() -> { try (ClothoSession session = sessionFactory.openSession()) { // ... work on this virtual thread ... } }); } ``` **Multiple isolated operations on the same thread: open them sequentially.** Each `openSession()` call replaces the previous thread-local binding, so open only one session at a time on any given thread. Close the first before opening the second: ```java // First operation try (ClothoSession s1 = sessionFactory.openSession()) { session.load(Person.class, id); } // Second operation — clean identity map, independent unit of work try (ClothoSession s2 = sessionFactory.openSession()) { session.load(Person.class, id); // fresh load, not the cached s1 instance } ``` **Spring Boot — session-per-request:** The `clotho-spring-boot-starter` provides a `ClothoSessionFactory` bean. Wire it into your `@Service` classes and open a session per method (as shown in section 5), or use a request-scoped session bean if you want one session for the lifetime of an HTTP request: ```java @Bean @RequestScope public ClothoSession requestSession(ClothoSessionFactory factory) { return factory.openSession(); } ``` Declare the bean as `@RequestScope` and Spring will open it on the first injection and call `close()` (triggering auto-save) at the end of the request. ### Transactional sessions By default, each Gremlin traversal is auto-committed immediately by ArcadeDB. If a multi-step save fails midway, the graph is left in a partial state. Use a **transactional session** to wrap all operations in a single atomic transaction: ```java try (ClothoSession session = sessionFactory.openTransactionalSession()) { session.save(alice); session.save(acme); } // ← tx.commit() fires here; any exception triggers tx.rollback() ``` With the Spring Boot starter you can use `@Transactional` instead of managing the session manually. `ClothoTransactionManager` is registered automatically: ```java @Service @RequiredArgsConstructor public class PersonService { private final ClothoSessionFactory sessionFactory; @Transactional public void promote(ARID personId) { try (ClothoSession session = sessionFactory.openSession()) { // borrows tx session Person p = session.load(Person.class, personId); p.setRole("SENIOR"); } // session close is a no-op; Spring tx manager commits on method return } } ``` Inside a `@Transactional` method, `openSession()` detects the existing transaction and borrows its session rather than opening a new one. The transaction is committed (or rolled back) when the outermost `@Transactional` boundary exits. See [docs/TRANSACTIONS.md](TRANSACTIONS.md) for a complete guide including nested transactions, multi-manager setups, and the interaction with `@Version` optimistic locking. ### Summary | Scenario | Pattern | |---|---| | Web request / discrete operation | `try (ClothoSession s = factory.openSession())` | | Atomic multi-step write | `try (ClothoSession s = factory.openTransactionalSession())` | | Spring `@Transactional` | Annotate service method; use `openSession()` inside | | Single-threaded background process | Open once; call `clearCache()` after big batches | | Multi-threaded / virtual threads | One session per thread, opened and closed on that thread | | Spring HTTP request scope | `@Bean @RequestScope ClothoSession` | --- ## 7. Repository pattern Define a repository by extending `ClothoRepository` and annotating with `@ClothoRepository`. The Spring Boot starter registers it as a bean automatically. ```java @ClothoRepository public class PersonRepository extends ClothoRepository { public PersonRepository(ClothoSessionFactory factory) { super(factory); } // Built-in: findAll(), findById(), count(), save(), delete(), findWithPagination() public List findByName(String name) { return execute(g().V().has("Person", "name", name)); } public List findByAgeGreaterThan(int minAge) { return execute(allEntities(), t -> t.has("age", P.gt(minAge))); } public List findEmployed() { // people who have at least one outgoing WORKS_AT edge return execute(allEntities(), t -> t.where(__.outE("WORKS_AT"))); } public long countByIndustry(String industry) { // raw result — no Clotho mapping return this.executeRaw( g().V().has("Company", "industry", industry).count() ).get(0); } } ``` Inject the repository in your service: ```java @Service @RequiredArgsConstructor public class PersonService { private final PersonRepository personRepository; public List listSeniorEngineers() { return personRepository.findByAgeGreaterThan(35); } public List listAll() { return personRepository.findAll(); // inherited } public List listPage(long page, long pageSize) { return personRepository.findWithPagination(page * pageSize, pageSize); // inherited } } ``` --- ## 8. Ad-hoc traversals from a loaded entity Repository methods can traverse any edge in the graph — there is no requirement that the edge be declared as `@Include` on the entity class. Start from the loaded entity's ID and write a Gremlin traversal as usual. **Example: find co-workers** — people who share a company via `WORKS_AT`, but where `WORKS_AT` is not declared as `@Include` on `Person`: ```java @ClothoRepository public class PersonRepository extends ClothoRepository { // ... public List findCoworkers(Person person) { return execute( g().V(person.getId().asString()).as("self") .out("WORKS_AT") // Person → Company .in("WORKS_AT") // Company → all Persons at that company .where(P.neq("self")) // exclude the starting person ); } } ``` Call it with any already-loaded `Person`: ```java try (ClothoSession session = sessionFactory.openSession()) { Person alice = session.load(Person.class, aliceId); List coworkers = personRepository.findCoworkers(alice); // returned Person objects have their @Include fields populated (jobs, etc.) } ``` **Key point:** `execute()` maps every resulting vertex to its most-specific registered Clotho type and calls `populateAll()` on the result list, so the returned objects are fully hydrated (all eager `@Include` fields present) — regardless of how the traversal was built. The object boundary and the query traversal are completely independent concerns. ### Tracked results — execute with a session By default, `execute()` returns **detached** entities: they are not registered in any session and mutations are silently ignored. Pass a `ClothoSession` to get back **MANAGED** entities instead — dirty-field tracking fires, and the session auto-saves any changes when it closes: ```java public List findCoworkers(Person person, ClothoSession session) { return execute(session, g().V(person.getId().asString()).as("self") .out("WORKS_AT").in("WORKS_AT") .where(P.neq("self")) ); } ``` ```java try (ClothoSession session = sessionFactory.openSession()) { Person alice = session.load(Person.class, aliceId); List coworkers = personRepository.findCoworkers(alice, session); // Mutations are tracked — no explicit save() needed coworkers.get(0).setEmail("new@example.com"); } // ← session.close() auto-saves the dirty co-worker ``` `session.register(vertex)` is the underlying mechanism — it recursively marks the vertex and all entities reachable through its `@Include` boundary as MANAGED and adds them to the session's identity map. You can call it directly if you need finer control. For raw scalar results (counts, aggregates), use `executeRaw()`: ```java public long countCoworkers(Person person) { return this.executeRaw( g().V(person.getId().asString()) .out("WORKS_AT").in("WORKS_AT") .where(P.neq("self")) .count() ).get(0); } ``` --- ## 9. Object boundary with @Include `@Include @Via("EDGE_LABEL")` declares that a field is part of this object's boundary. All `@Include` fields on a vertex are loaded in a single atomic `project()` traversal — no N+1 queries, no risk of seeing inconsistent state between fields. ```java @VertexType("Department") @Getter public class Department extends ClothoVertex { @Property("name") private @Nullable String name; // EAGER (default): loaded with the vertex, always present @Include @Via("HAS_MANAGER") private @Nullable Person manager; // LAZY: skipped on initial load; call session.loadField() when needed @Include(fetchType = FetchType.LAZY) @Via("HAS_MEMBER") private @Nullable List members; } ``` Load a lazy field on demand: ```java try (ClothoSession session = sessionFactory.openSession()) { Department dept = session.load(Department.class, deptId); // dept.getMembers() is null here — field was declared LAZY session.loadField(dept, "members"); // dept.getMembers() is now populated log.info("Members: {}", dept.getMembers().size()); } ``` --- ## 10. Optimistic locking with @Version ArcadeDB maintains a `@version` counter on every record that increments on each write. Clotho reads it on load and includes it in update traversals, so concurrent writes are detected. ```java @VertexType("Order") @Getter public class Order extends ClothoVertex { @Property("status") private @Nullable String status; @Version // maps to ArcadeDB's built-in @version property private @Nullable Long version; public void setStatus(@Nullable String status) { this.status = status; notifyFieldChanged("status"); } } ``` If two sessions load the same Order and both try to save a change, the second save throws `OptimisticLockException`: ```java try (ClothoSession session = sessionFactory.openSession()) { Order order = session.load(Order.class, orderId); order.setStatus("SHIPPED"); session.save(order); } catch (OptimisticLockException e) { log.warn("Concurrent update detected for {} at version {}", e.getId(), e.getExpectedVersion()); // retry or report conflict } ``` --- ## 11. Subgraph queries Use `executeSubgraph()` when a traversal returns multiple entity types in a single result row. ```java @SubgraphType public class EmployeeProfile { Person person; // field name matches .as("person") in the Gremlin query @Alias("employer") Company company; // @Alias maps the field to a different Gremlin alias } ``` ```java @ClothoRepository public class EmployeeRepo extends ClothoRepository { public EmployeeRepo(ClothoSessionFactory factory) { super(factory); } public List findProfiles() { return executeSubgraph(EmployeeProfile.class, g().V().hasLabel("Person").as("person") .out("WORKS_AT").in("WORKS_AT").as("company") // traverse to Company .select("person", "company")); } } ``` --- ## 12. Type inheritance Define a class hierarchy in Java and tell Clotho which ArcadeDB parent types to wire: ```java @VertexType(value = "Employee", additionalParents = {"Person"}) public class Employee extends Person { @Property("employeeNumber") private @Nullable String employeeNumber; // ... } ``` When Clotho loads a vertex labeled `Employee`, it maps it to `Employee.class`. When it loads a vertex labeled `Person` (no `Employee` subtype), it maps it to `Person.class`. This "most specific type wins" rule is enforced by the `TypeRegistry`. --- ## Reference docs | Document | Contents | |---|---| | `docs/TYPE_SYSTEM.md` | Entity hierarchy, inheritance, label resolution | | `docs/OBJECT_BOUNDARY.md` | `@Include`/`@Via`, multi-hop, edge-only inclusion, cycle detection | | `docs/ANNOTATIONS.md` | Complete annotation reference | | `docs/QUERY_MODEL.md` | Repository pattern, `executeSubgraph`, result mapping | | `docs/SPRING_INTEGRATION.md` | Autoconfigure, properties reference | | `docs/ARCHITECTURE.md` | Module breakdown, session lifecycle, data flow | The `clotho-dev-app` module contains a runnable `FeatureDemo` that exercises all the patterns described here against a live ArcadeDB instance.