Spring Data LadybugDB is a Spring Data-like integration framework for LadybugDB, providing familiar Spring patterns for graph database operations.
1. Overview
1.1. Architecture
The framework consists of several core components:
+------------------+ +--------------------+ +-------------------+
| Application | --> | LadybugDBTemplate | --> | ConnectionFactory |
+------------------+ +--------------------+ +-------------------+
| | |
v v v
+------------------+ +--------------------+ +-------------------+
| Repositories | | EntityRegistry | | Database |
+------------------+ +--------------------+ +-------------------+
1.2. Core Components
1.2.1. LadybugDBTemplate
The central class for executing Cypher queries. Provides methods for:
-
execute()- Execute write operations -
query()- Execute queries returning lists -
queryForObject()- Execute queries returning single results -
stream()- Memory-efficient streaming for large result sets
1.2.2. NodeRepository
A Spring Data-style repository interface providing CRUD operations:
-
save(),saveAll()- Persist entities -
findById(),findAll(),findAllById()- Retrieve entities -
delete(),deleteById(),deleteAll()- Remove entities -
count(),existsById()- Utility methods
2. Getting Started
2.1. Installation
This section covers how to add Spring Data LadybugDB to your project. The framework requires the main library dependency, the LadybugDB native library, and optionally the Cypher DSL and connection pooling libraries.
Maven Dependency
Add the following dependency to your pom.xml:
<dependency>
<groupId>com.thecookiezen</groupId>
<artifactId>spring-data-ladybugdb</artifactId>
<version>0.0.2</version>
</dependency>
Required Dependencies
Ensure you have the following dependencies in your project:
LadybugDB Native Library
<dependency>
<groupId>com.ladybugdb</groupId>
<artifactId>lbug</artifactId>
<version>0.15.1</version>
</dependency>
2.2. Quick Start
This guide will get you started with Spring Data LadybugDB in 5 minutes. By the end, you’ll be able to:
-
Create and configure a connection to LadybugDB
-
Define entity classes with annotations
-
Execute Cypher queries using the template
-
Perform CRUD operations with repositories
The framework follows familiar Spring patterns - if you’ve used Spring Data JPA or Spring Data Neo4j, you’ll feel right at home.
Create a Database Instance
First, create a LadybugDB database instance:
import com.ladybugdb.Database;
Database database = new Database(":memory:"); // In-memory database
// Or: new Database("/path/to/database"); // Persistent database
Create a Connection Factory
For development, use SimpleConnectionFactory:
import com.thecookiezen.ladybugdb.spring.connection.SimpleConnectionFactory;
SimpleConnectionFactory connectionFactory = new SimpleConnectionFactory(database);
For production, use PooledConnectionFactory:
import com.thecookiezen.ladybugdb.spring.connection.PooledConnectionFactory;
PooledConnectionFactory connectionFactory = new PooledConnectionFactory(database);
Create the Template
Create a LadybugDBTemplate with an EntityRegistry:
import com.thecookiezen.ladybugdb.spring.core.LadybugDBTemplate;
import com.thecookiezen.ladybugdb.spring.repository.support.EntityRegistry;
EntityRegistry registry = new EntityRegistry();
LadybugDBTemplate template = new LadybugDBTemplate(connectionFactory, registry);
Define Your Entity
Create an entity class with the @NodeEntity annotation:
import com.thecookiezen.ladybugdb.spring.annotation.NodeEntity;
import org.springframework.data.annotation.Id;
@NodeEntity(label = "Person")
public class Person {
@Id
private String name;
private int age;
// constructors, getters, setters
public Person() {}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// getters and setters...
}
Register Entity Mappers
Register the entity descriptor with mappers for reading and writing:
import com.thecookiezen.ladybugdb.spring.mapper.RowMapper;
import com.thecookiezen.ladybugdb.spring.mapper.EntityWriter;
import com.thecookiezen.ladybugdb.spring.mapper.ValueMappers;
import com.thecookiezen.ladybugdb.spring.repository.support.EntityDescriptor;
import java.util.Map;
RowMapper<Person> reader = (row) -> {
var p = row.getNode("n");
return new Person(
ValueMappers.asString(p.get("name")),
ValueMappers.asInteger(p.get("age"))
);
};
EntityWriter<Person> writer = (entity) -> Map.of("age", entity.getAge());
registry.registerDescriptor(Person.class, new EntityDescriptor<>(Person.class, reader, writer));
Create the Node Table
Execute DDL to create the node table:
template.execute("CREATE NODE TABLE Person(name STRING PRIMARY KEY, age INT64)");
Use the Template
Execute raw Cypher queries:
// Create a person
template.execute("CREATE (p:Person {name: $name, age: $age})",
Map.of("name", "Alice", "age", 30));
// Query persons
List<Person> people = template.query(
"MATCH (n:Person) RETURN n",
Person.class
);
Use the Repository
Create a repository for CRUD operations:
import com.thecookiezen.ladybugdb.spring.repository.support.SimpleNodeRepository;
SimpleNodeRepository<Person, Void, String> repository = new SimpleNodeRepository<>(
template, Person.class, Void.class,
registry.getDescriptor(Person.class), null
);
// CRUD operations
Person saved = repository.save(new Person("Bob", 25));
Optional<Person> found = repository.findById("Bob");
repository.deleteById("Bob");
Complete Example
The following example consolidates all the steps above into a single runnable class. It demonstrates the complete workflow from database creation to query execution:
import com.ladybugdb.Database;
import com.thecookiezen.ladybugdb.spring.connection.SimpleConnectionFactory;
import com.thecookiezen.ladybugdb.spring.core.LadybugDBTemplate;
import com.thecookiezen.ladybugdb.spring.repository.support.*;
public class QuickStart {
public static void main(String[] args) {
try (Database database = new Database(":memory:");
SimpleConnectionFactory factory = new SimpleConnectionFactory(database)) {
EntityRegistry registry = new EntityRegistry();
// Register mappers (see step 5)
registry.registerDescriptor(Person.class, personDescriptor);
LadybugDBTemplate template = new LadybugDBTemplate(factory, registry);
template.execute("CREATE NODE TABLE Person(name STRING PRIMARY KEY, age INT64)");
template.execute("CREATE (p:Person {name: 'Alice', age: 30})");
List<Person> people = template.query("MATCH (n:Person) RETURN n", Person.class);
System.out.println("Found " + people.size() + " people");
}
}
}
2.3. Spring Integration
Spring Data LadybugDB integrates seamlessly with Spring Framework through annotation-based configuration.
Enable LadybugDB Repositories
Use the @EnableLadybugDBRepositories annotation to enable automatic repository detection:
import com.thecookiezen.ladybugdb.spring.config.EnableLadybugDBRepositories;
import com.thecookiezen.ladybugdb.spring.core.LadybugDBTemplate;
import com.thecookiezen.ladybugdb.spring.connection.LadybugDBConnectionFactory;
import com.thecookiezen.ladybugdb.spring.repository.support.EntityRegistry;
import com.thecookiezen.ladybugdb.spring.transaction.LadybugDBTransactionManager;
import org.springframework.context.annotation.*;
@Configuration
@EnableLadybugDBRepositories(basePackages = "com.example.repositories")
public class LadybugDBConfig {
@Bean
public Database database() {
return new Database("/path/to/database");
}
@Bean
public LadybugDBConnectionFactory connectionFactory(Database database) {
return new PooledConnectionFactory(database);
}
@Bean
public EntityRegistry entityRegistry() {
EntityRegistry registry = new EntityRegistry();
// Register entity descriptors
return registry;
}
@Bean
public LadybugDBTemplate ladybugDBTemplate(
LadybugDBConnectionFactory connectionFactory,
EntityRegistry entityRegistry) {
return new LadybugDBTemplate(connectionFactory, entityRegistry);
}
@Bean
public LadybugDBTransactionManager transactionManager(
LadybugDBConnectionFactory connectionFactory) {
return new LadybugDBTransactionManager(connectionFactory);
}
}
Repository Interface
Define your repository interface extending NodeRepository:
import com.thecookiezen.ladybugdb.spring.annotation.NodeEntity;
import com.thecookiezen.ladybugdb.spring.annotation.Query;
import com.thecookiezen.ladybugdb.spring.repository.NodeRepository;
import org.springframework.data.annotation.Id;
import org.springframework.data.repository.query.Param;
import java.util.List;
import java.util.Optional;
@NodeEntity(label = "Person")
public class Person {
@Id
private String name;
private int age;
// constructors, getters, setters
}
public interface PersonRepository extends NodeRepository<Person, String, Void, Person> {
@Query("MATCH (n:Person) WHERE n.age > $minAge RETURN n")
List<Person> findByAgeGreaterThan(@Param("minAge") int minAge);
@Query("MATCH (n:Person) WHERE n.name = $name RETURN n")
Optional<Person> findByName(@Param("name") String name);
}
Using Repositories
Inject repositories into your services:
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
public class PersonService {
private final PersonRepository repository;
public PersonService(PersonRepository repository) {
this.repository = repository;
}
@Transactional
public Person createPerson(String name, int age) {
return repository.save(new Person(name, age));
}
public Optional<Person> findPerson(String name) {
return repository.findById(name);
}
public List<Person> findAdults() {
return repository.findByAgeGreaterThan(17);
}
@Transactional
public void deletePerson(String name) {
repository.deleteById(name);
}
}
@EnableLadybugDBRepositories Attributes
| Attribute | Description |
|---|---|
|
Packages to scan for repository interfaces |
|
Type-safe alternative to |
|
Name of the |
|
Postfix for custom repository implementations (default: |
|
Strategy for query creation (default: |
Transaction Management
The LadybugDBTransactionManager binds a connection to the current thread for the transaction duration:
import org.springframework.transaction.annotation.Transactional;
@Service
public class PersonService {
@Transactional
public void updateMultiplePersons(List<Person> persons) {
// All operations use the same connection
for (Person p : persons) {
repository.save(p);
}
}
}
| LadybugDB auto-commits each command. The transaction manager provides connection binding only, not atomic transactions. See Transactions and LadybugDB Transaction docs for details. |
Connection Pool Configuration
Configure the connection pool with custom settings:
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import java.time.Duration;
@Bean
public LadybugDBConnectionFactory connectionFactory(Database database) {
GenericObjectPoolConfig<Connection> config = new GenericObjectPoolConfig<>();
config.setMaxTotal(20);
config.setMaxIdle(10);
config.setMinIdle(5);
config.setMaxWait(Duration.ofSeconds(60));
config.setTestOnBorrow(true);
config.setTestWhileIdle(true);
config.setTimeBetweenEvictionRuns(Duration.ofMinutes(2));
config.setMinEvictableIdleDuration(Duration.ofMinutes(10));
return new PooledConnectionFactory(database, config);
}
3. Core Concepts
3.1. Connection Factories
Connection factories manage connections to the LadybugDB database. The framework provides two implementations:
SimpleConnectionFactory
Creates a new connection for each request and closes it when released. Best for:
-
Development and testing
-
Single-threaded applications
-
Low-traffic scenarios
import com.ladybugdb.Database;
import com.thecookiezen.ladybugdb.spring.connection.SimpleConnectionFactory;
try (Database database = new Database(":memory:");
SimpleConnectionFactory factory = new SimpleConnectionFactory(database)) {
// Get a connection
Connection connection = factory.getConnection();
try {
// Use the connection...
connection.query("MATCH (n) RETURN n");
} finally {
// Release the connection (closes it)
factory.releaseConnection(connection);
}
}
PooledConnectionFactory
Maintains a pool of connections for efficient reuse. Best for:
-
Production applications
-
Multi-threaded environments
-
High-traffic scenarios
import com.ladybugdb.Database;
import com.thecookiezen.ladybugdb.spring.connection.PooledConnectionFactory;
try (Database database = new Database("/path/to/database");
PooledConnectionFactory factory = new PooledConnectionFactory(database)) {
// Get a connection from the pool
Connection connection = factory.getConnection();
try {
// Use the connection...
} finally {
// Return the connection to the pool
factory.releaseConnection(connection);
}
// Monitor pool statistics
int active = factory.getNumActive(); // Currently in use
int idle = factory.getNumIdle(); // Available in pool
}
Pool Configuration
Customize pool behavior with GenericObjectPoolConfig:
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import java.time.Duration;
GenericObjectPoolConfig<Connection> config = new GenericObjectPoolConfig<>();
config.setMaxTotal(10); // Max connections
config.setMaxIdle(5); // Max idle connections
config.setMinIdle(2); // Min idle connections
config.setMaxWait(Duration.ofSeconds(30)); // Max wait time
config.setTestOnBorrow(true); // Validate on borrow
config.setTestOnReturn(false); // Validate on return
config.setTestWhileIdle(true); // Validate idle connections
config.setTimeBetweenEvictionRuns(Duration.ofMinutes(1)); // Eviction check interval
config.setMinEvictableIdleDuration(Duration.ofMinutes(5)); // Min idle time before eviction
PooledConnectionFactory factory = new PooledConnectionFactory(database, config);
Default Configuration
| Setting | Default | Description |
|---|---|---|
|
10 |
Maximum number of connections |
|
5 |
Maximum idle connections in pool |
|
2 |
Minimum idle connections maintained |
|
30 seconds |
Maximum wait time when pool is exhausted |
|
true |
Validate connections when borrowed |
|
true |
Validate idle connections periodically |
|
1 minute |
Interval between eviction runs |
|
5 minutes |
Minimum idle time before eviction |
LadybugDBConnectionFactory Interface
Both implementations implement LadybugDBConnectionFactory:
public interface LadybugDBConnectionFactory {
// Get a connection
Connection getConnection();
// Release a connection back to factory
void releaseConnection(Connection connection);
// Get the underlying database
Database getDatabase();
// Close the factory and release resources
void close();
}
Resource Management
Always close connections and factories when done:
try (Database database = new Database(":memory:");
SimpleConnectionFactory factory = new SimpleConnectionFactory(database)) {
LadybugDBTemplate template = new LadybugDBTemplate(factory, registry);
// Use template...
}
Or with Spring’s @PreDestroy:
@Bean(destroyMethod = "close")
public PooledConnectionFactory connectionFactory(Database database) {
return new PooledConnectionFactory(database);
}
3.2. LadybugDBTemplate
The LadybugDBTemplate is the central class for executing Cypher queries. It provides:
-
Automatic connection management
-
Parameterized query support
-
Result mapping through
RowMapper -
Memory-efficient streaming for large results
-
Transaction-aware connection binding
Creating a Template
import com.ladybugdb.Database;
import com.thecookiezen.ladybugdb.spring.connection.SimpleConnectionFactory;
import com.thecookiezen.ladybugdb.spring.core.LadybugDBTemplate;
import com.thecookiezen.ladybugdb.spring.repository.support.EntityRegistry;
Database database = new Database(":memory:");
SimpleConnectionFactory factory = new SimpleConnectionFactory(database);
EntityRegistry registry = new EntityRegistry();
LadybugDBTemplate template = new LadybugDBTemplate(factory, registry);
Execute Operations
Execute write operations (CREATE, MERGE, DELETE, SET):
Execute with parameters
import java.util.Map;
template.execute(
"CREATE (p:Person {name: $name, age: $age})",
Map.of("name", "Bob", "age", 25)
);
Execute with Statement (Cypher DSL)
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Node;
import org.neo4j.cypherdsl.core.Statement;
Node person = Cypher.node("Person").named("p")
.withProperties("name", Cypher.parameter("name"));
Statement statement = Cypher.merge(person).returning(person).build();
template.execute(statement, Map.of("name", "Charlie"));
Query Operations
Query returning a List
import java.util.List;
List<Person> people = template.query(
"MATCH (n:Person) WHERE n.age > $minAge RETURN n",
Map.of("minAge", 20),
Person.class
);
Query with custom RowMapper
import com.thecookiezen.ladybugdb.spring.mapper.RowMapper;
import com.thecookiezen.ladybugdb.spring.mapper.ValueMappers;
import java.util.List;
List<Person> people = template.query(
"MATCH (n:Person) RETURN n ORDER BY n.name",
(row) -> {
var node = row.getNode("n");
return new Person(
ValueMappers.asString(node.get("name")),
ValueMappers.asInteger(node.get("age"))
);
}
);
Query returning single result
import java.util.Optional;
Optional<Person> person = template.queryForObject(
"MATCH (n:Person) WHERE n.name = $name RETURN n",
Map.of("name", "Alice"),
Person.class
);
if (person.isPresent()) {
System.out.println("Found: " + person.get());
}
Query with Cypher DSL Statement
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Node;
import org.neo4j.cypherdsl.core.Statement;
Node n = Cypher.node("Person").named("n");
Statement statement = Cypher.match(n)
.where(n.property("age").gt(Cypher.parameter("minAge")))
.returning(n)
.build();
List<Person> people = template.query(statement, Map.of("minAge", 20), Person.class);
Streaming Results
For large result sets, use stream() for memory efficiency:
import java.util.stream.Stream;
try (Stream<Person> stream = template.stream(
"MATCH (n:Person) RETURN n",
Map.of(),
Person.class)) {
stream.forEach(person -> {
// Process each person
System.out.println(person.getName());
});
}
| Always use try-with-resources to ensure proper resource cleanup. The stream holds database resources until closed. |
Callback Execution
Execute custom logic with direct connection access:
import com.thecookiezen.ladybugdb.spring.core.LadybugDBCallback;
Long count = template.execute((connection) -> {
try (var result = connection.query("MATCH (n:Person) RETURN count(n) AS count")) {
if (result.hasNext()) {
return result.getNext().getValue(0).getValue();
}
return 0L;
}
});
Query For String List
Convenience method for single-column string results:
import java.util.List;
List<String> names = template.queryForStringList(
"MATCH (n:Person) RETURN n.name AS name ORDER BY name",
"name"
);
Extension Loading
Load extensions before executing queries:
import java.util.List;
List<Note> similarNotes = template.query(
new String[]{"vector"}, // Extensions to load
"MATCH (n:Note) WHERE vector_search(n.embedding, $query) < 0.5 RETURN n",
Map.of("query", queryVector),
Note.class
);
Parameter Types
The template automatically converts Java types to LadybugDB Values:
| Java Type | LadybugDB Type |
|---|---|
|
STRING |
|
INT64 |
|
INT64 |
|
DOUBLE |
|
BOOLEAN |
|
LIST (of FLOAT) |
|
LIST |
|
MAP |
|
(passed through) |
3.3. Transactions
Spring Data LadybugDB provides a transaction manager for connection binding, with important limitations due to LadybugDB’s architecture.
LadybugDBTransactionManager
The transaction manager binds a connection to the current thread:
import com.thecookiezen.ladybugdb.spring.transaction.LadybugDBTransactionManager;
import com.thecookiezen.ladybugdb.spring.connection.PooledConnectionFactory;
@Bean
public LadybugDBTransactionManager transactionManager(PooledConnectionFactory factory) {
return new LadybugDBTransactionManager(factory);
}
Important Limitations
| LadybugDB auto-commits each command immediately. The transaction manager provides connection binding only, not atomic transactions. |
No Rollback Support
@Transactional
public void updatePerson(Person person) {
template.execute("SET p.age = 30"); // Immediately committed
throw new RuntimeException("Error!"); // Cannot roll back previous operation
}
3.4. Entity Registry
The EntityRegistry manages entity descriptors that define how to read and write entities to/from the database.
EntityDescriptor
An EntityDescriptor<T> combines a RowMapper for reading and an EntityWriter for writing:
import com.thecookiezen.ladybugdb.spring.repository.support.EntityDescriptor;
import com.thecookiezen.ladybugdb.spring.mapper.RowMapper;
import com.thecookiezen.ladybugdb.spring.mapper.EntityWriter;
public record EntityDescriptor<T>(
Class<T> entityType,
RowMapper<T> reader,
EntityWriter<T> writer
) {}
Registering Descriptors
import com.thecookiezen.ladybugdb.spring.repository.support.EntityRegistry;
import com.thecookiezen.ladybugdb.spring.mapper.RowMapper;
import com.thecookiezen.ladybugdb.spring.mapper.EntityWriter;
import java.util.Map;
EntityRegistry registry = new EntityRegistry();
// Create reader (RowMapper)
RowMapper<Person> reader = (row) -> {
var node = row.getNode("n");
return new Person(
ValueMappers.asString(node.get("name")),
ValueMappers.asInteger(node.get("age"))
);
};
// Create writer (EntityWriter)
EntityWriter<Person> writer = (entity) -> Map.of(
"age", entity.getAge()
// Note: ID is handled separately
);
// Register the descriptor
registry.registerDescriptor(Person.class, new EntityDescriptor<>(Person.class, reader, writer));
Using Registered Descriptors
With LadybugDBTemplate
Once registered, use the entity class directly:
// Query using registered mapper
List<Person> people = template.query("MATCH (n:Person) RETURN n", Person.class);
// Query for single result
Optional<Person> person = template.queryForObject(
"MATCH (n:Person) WHERE n.name = $name RETURN n",
Map.of("name", "Alice"),
Person.class
);
With SimpleNodeRepository
Pass the descriptor to the repository constructor:
import com.thecookiezen.ladybugdb.spring.repository.support.SimpleNodeRepository;
SimpleNodeRepository<Person, Void, String> repository = new SimpleNodeRepository<>(
template,
Person.class,
Void.class,
registry.getDescriptor(Person.class),
null // No relationship descriptor
);
RowMapper Interface
The RowMapper<T> functional interface maps a QueryRow to an entity:
@FunctionalInterface
public interface RowMapper<T> {
T mapRow(QueryRow row) throws Exception;
}
Mapping Nodes
RowMapper<Person> personMapper = (row) -> {
// Get node properties
Map<String, Value> node = row.getNode("n");
String name = ValueMappers.asString(node.get("name"));
Integer age = ValueMappers.asInteger(node.get("age"));
return new Person(name, age);
};
Mapping Relationships
RowMapper<Follows> followsMapper = (row) -> {
// Get relationship data
RelationshipData rel = row.getRelationship("rel");
String id = rel.id().toString();
int since = ValueMappers.asInteger(rel.properties().get("since"));
// Get connected nodes
Map<String, Value> sourceNode = row.getNode("s");
Map<String, Value> targetNode = row.getNode("t");
Person from = new Person(
ValueMappers.asString(sourceNode.get("name")),
ValueMappers.asInteger(sourceNode.get("age"))
);
Person to = new Person(
ValueMappers.asString(targetNode.get("name")),
ValueMappers.asInteger(targetNode.get("age"))
);
return new Follows(id, from, to, since);
};
EntityWriter Interface
The EntityWriter<T> functional interface decomposes an entity into a property map:
@FunctionalInterface
public interface EntityWriter<T> {
Map<String, Object> decompose(T entity);
}
| The ID property is handled separately by the repository and should not be included in the writer output. |
Writer with Multiple Properties
EntityWriter<Document> documentWriter = (entity) -> {
Map<String, Object> props = new HashMap<>();
props.put("title", entity.getTitle());
props.put("content", entity.getContent());
props.put("createdAt", entity.getCreatedAt().toEpochMilli());
props.put("tags", entity.getTags());
props.put("embedding", entity.getEmbedment()); // float[]
return props;
};
QueryRow Interface
The QueryRow provides access to row data:
public interface QueryRow {
// Get value by column name
Value getValue(String column);
// Get value by column index
Value getValue(int index);
// Check if column exists
boolean containsKey(String column);
// Check value type
boolean isNode(String column);
boolean isRelationship(String column);
// Get structured data
Map<String, Value> getNode(String column);
RelationshipData getRelationship(String column);
// Get all column names
Set<String> keySet();
}
Registering Multiple Entities
EntityRegistry registry = new EntityRegistry();
registry.registerDescriptor(Person.class, personDescriptor);
registry.registerDescriptor(Company.class, companyDescriptor);
registry.registerDescriptor(Follows.class, followsDescriptor);
registry.registerDescriptor(WorksAt.class, worksAtDescriptor);
// Retrieve descriptors
EntityDescriptor<Person> personDesc = registry.getDescriptor(Person.class);
Error Handling
Attempting to query with an unregistered entity class:
try {
List<Unregistered> results = template.query("MATCH (n) RETURN n", Unregistered.class);
} catch (IllegalArgumentException e) {
// "No entity descriptor registered for com.example.Unregistered"
}
Best Practices
Column Naming
Use consistent column names in your queries and mappers:
// Query uses "n" for node
template.query("MATCH (n:Person) RETURN n", Person.class);
// Mapper expects "n"
RowMapper<Person> mapper = (row) -> {
var node = row.getNode("n"); // Must match query
// ...
};
Null Safety
Handle null values in mappers:
RowMapper<Document> mapper = (row) -> {
var node = row.getNode("n");
String title = ValueMappers.asString(node.get("title")); // Returns null if missing
Integer views = ValueMappers.asInteger(node.get("views")); // Returns null if missing
return new Document(
title,
views != null ? views : 0 // Default value
);
};
Complex Types
Use ValueMappers for collections and arrays:
EntityWriter<Document> writer = (entity) -> {
Map<String, Object> props = new HashMap<>();
props.put("tags", entity.getTags()); // List<String>
props.put("scores", entity.getScores()); // List<Integer>
props.put("embedding", entity.getEmbedment()); // float[]
return props;
};
RowMapper<Document> reader = (row) -> {
var node = row.getNode("n");
return new Document(
ValueMappers.asString(node.get("title")),
ValueMappers.asStringList(node.get("tags")),
ValueMappers.asFloatArray(node.get("embedding"))
);
};
4. Querying
4.1. Raw Cypher Queries
Execute Cypher queries directly using string-based query syntax with parameterized values.
Parameterized Queries
Always use parameters ($paramName) instead of string interpolation to prevent Cypher injection:
import java.util.Map;
// Execute a write operation
template.execute(
"CREATE (p:Person {name: $name, age: $age})",
Map.of("name", "Alice", "age", 30)
);
// Query for a list of results
List<Person> people = template.query(
"MATCH (n:Person) WHERE n.name IN $names RETURN n",
Map.of("names", List.of("Alice", "Bob", "Charlie")),
Person.class
);
Query Methods
execute() - Write Operations
For CREATE, MERGE, DELETE, SET operations and extensions:
// With parameters
template.execute(
"CREATE (p:Person {name: $name, age: $age})",
Map.of("name", "Bob", "age", 25)
);
// With extensions (e.g., vector search)
template.execute(
new String[]{"vector"},
"INSTALL vector",
Map.of()
);
query() - Return List
For queries returning multiple results:
import java.util.List;
List<Person> people = template.query(
"MATCH (n:Person) WHERE n.age > $minAge RETURN n ORDER BY n.name",
Map.of("minAge", 18),
Person.class
);
// With custom RowMapper
List<String> names = template.query(
"MATCH (n:Person) RETURN n.name AS name ORDER BY name",
(row) -> ValueMappers.asString(row.getValue("name"))
);
queryForObject() - Return Single
For queries expecting zero or one result:
import java.util.Optional;
Optional<Person> person = template.queryForObject(
"MATCH (n:Person) WHERE n.name = $name RETURN n",
Map.of("name", "Alice"),
Person.class
);
if (person.isPresent()) {
System.out.println("Found: " + person.get().getName());
}
Security
Cypher Injection Prevention
| Never concatenate user input into Cypher strings. |
// DANGEROUS - Vulnerable to injection
String unsafe = "Alice' OR 1=1 //";
template.execute("CREATE (p:Person {name: '" + unsafe + "'})");
// SAFE - Use parameters
template.execute(
"CREATE (p:Person {name: $name})",
Map.of("name", unsafe) // Safely escaped
);
Advanced Cypher Queries
For more complex queries (Pattern Matching, Aggregations, Subqueries, Updates), please consult the Neo4j Cypher Manual:
👉 Neo4j Cypher Manual
4.2. Cypher DSL
Neo4j’s Cypher DSL allows you to build queries programmatically instead of writing raw Cypher strings. This approach provides several benefits:
-
Type safety - Compiler catches errors before runtime
-
IDE support - Auto-completion and refactoring work naturally
-
Composability - Build complex queries from reusable parts
-
Readability - Fluent API mirrors Cypher syntax
Use the DSL when building dynamic queries or when you want compile-time validation. For simple, static queries, raw Cypher may be more concise.
Dependency
<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j-cypher-dsl</artifactId>
<version>2025.2.4</version>
</dependency>
Basic Usage
Here is a basic example of creating and executing a query using the Cypher DSL:
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Node;
import org.neo4j.cypherdsl.core.Statement;
import java.util.Map;
import java.util.List;
// 1. Define the node
Node person = Cypher.node("Person").named("p");
// 2. Build the statement
Statement statement = Cypher.match(person)
.where(person.property("age").gt(Cypher.parameter("minAge")))
.returning(person)
.orderBy(person.property("name").ascending())
.build();
// 3. Execute with LadybugTemplate
List<Person> adults = template.query(
statement,
Map.of("minAge", 18),
Person.class
);
Getting Cypher String
For debugging or logging, you can easily get the generated Cypher string:
String cypher = statement.getCypher();
System.out.println("Generated Cypher: " + cypher);
4.3. Row Mappers
Row mappers convert query result rows to domain objects.
RowMapper Interface
@FunctionalInterface
public interface RowMapper<T> {
T mapRow(QueryRow row) throws Exception;
}
QueryRow Interface
The QueryRow provides access to row data:
public interface QueryRow {
// Access by column name
Value getValue(String column);
Value getValue(int index);
// Type checking
boolean containsKey(String column);
boolean isNode(String column);
boolean isRelationship(String column);
// Structured access
Map<String, Value> getNode(String column);
RelationshipData getRelationship(String column);
// Column names
Set<String> keySet();
}
Basic Mapping
Scalar Values
import com.thecookiezen.ladybugdb.spring.mapper.RowMapper;
import com.thecookiezen.ladybugdb.spring.mapper.ValueMappers;
RowMapper<String> nameMapper = (row) ->
ValueMappers.asString(row.getValue("name"));
RowMapper<Integer> ageMapper = (row) ->
ValueMappers.asInteger(row.getValue("age"));
RowMapper<Long> countMapper = (row) ->
ValueMappers.asLong(row.getValue("count"));
Mapping Entities
RowMapper<Person> personMapper = (row) -> {
Map<String, Value> node = row.getNode("n");
return new Person(
ValueMappers.asString(node.get("name")),
ValueMappers.asInteger(node.get("age"))
);
};
List<Person> people = template.query(
"MATCH (n:Person) RETURN n",
personMapper
);
Multiple Columns
RowMapper<PersonDto> mapper = (row) -> {
return new PersonDto(
ValueMappers.asString(row.getValue("name")),
ValueMappers.asInteger(row.getValue("age")),
ValueMappers.asString(row.getValue("city"))
);
};
List<PersonDto> results = template.query(
"MATCH (n:Person) RETURN n.name AS name, n.age AS age, n.city AS city",
mapper
);
Mapping Relationships
RelationshipData
import com.thecookiezen.ladybugdb.spring.mapper.RelationshipData;
public record RelationshipData(
InternalID id, // Internal relationship ID
String labelName, // Relationship type (e.g., "KNOWS")
InternalID sourceId, // Source node ID
InternalID targetId, // Target node ID
Map<String, Value> properties // Custom properties
) {}
Mapping Relationship Entities
@RelationshipEntity(type = "FOLLOWS", nodeType = Person.class, sourceField = "from", targetField = "to")
public class Follows {
@Id
String name;
Person from;
Person to;
int since;
}
RowMapper<Follows> followsMapper = (row) -> {
// Get relationship data
RelationshipData rel = row.getRelationship("rel");
String name = ValueMappers.asString(rel.properties().get("name"));
int since = ValueMappers.asInteger(rel.properties().get("since"));
// Get connected nodes
Map<String, Value> sourceNode = row.getNode("s");
Map<String, Value> targetNode = row.getNode("t");
Person from = new Person(
ValueMappers.asString(sourceNode.get("name")),
ValueMappers.asInteger(sourceNode.get("age"))
);
Person to = new Person(
ValueMappers.asString(targetNode.get("name")),
ValueMappers.asInteger(targetNode.get("age"))
);
return new Follows(name, from, to, since);
};
List<Follows> follows = template.query(
"MATCH (s:Person)-[rel:FOLLOWS]->(t:Person) RETURN s, rel, t",
followsMapper
);
Relationship with Multiple Types
RowMapper<Connection> connectionMapper = (row) -> {
RelationshipData rel = row.getRelationship("r");
String type = rel.labelName(); // "KNOWS", "WORKS_WITH", etc.
Map<String, Value> source = row.getNode("a");
Map<String, Value> target = row.getNode("b");
return new Connection(
type,
ValueMappers.asString(source.get("name")),
ValueMappers.asString(target.get("name"))
);
};
Complex Mappings
Nested Objects
RowMapper<PersonWithCity> mapper = (row) -> {
Map<String, Value> personNode = row.getNode("p");
Map<String, Value> cityNode = row.getNode("c");
City city = new City(
ValueMappers.asString(cityNode.get("name")),
ValueMappers.asString(cityNode.get("country"))
);
return new PersonWithCity(
ValueMappers.asString(personNode.get("name")),
ValueMappers.asInteger(personNode.get("age")),
city
);
};
List<PersonWithCity> results = template.query(
"MATCH (p:Person)-[:LIVES_IN]->(c:City) RETURN p, c",
mapper
);
Lists and Arrays
RowMapper<Document> documentMapper = (row) -> {
Map<String, Value> node = row.getNode("n");
return new Document(
ValueMappers.asString(node.get("id")),
ValueMappers.asString(node.get("title")),
ValueMappers.asStringList(node.get("tags")),
ValueMappers.asFloatArray(node.get("embedding"))
);
};
Null Handling
RowMapper<Person> safeMapper = (row) -> {
Map<String, Value> node = row.getNode("n");
String name = ValueMappers.asString(node.get("name"));
Integer age = ValueMappers.asInteger(node.get("age"));
String city = ValueMappers.asString(node.get("city"));
return new Person(
name,
age != null ? age : 0, // Default value
city != null ? city : "Unknown" // Default value
);
};
Optional Relationships
RowMapper<PersonWithOptionalCity> mapper = (row) -> {
Map<String, Value> personNode = row.getNode("p");
Map<String, Value> cityNode = row.getNode("c"); // May be null
City city = null;
if (cityNode != null && !cityNode.isEmpty()) {
city = new City(
ValueMappers.asString(cityNode.get("name")),
ValueMappers.asString(cityNode.get("country"))
);
}
return new PersonWithOptionalCity(
ValueMappers.asString(personNode.get("name")),
Optional.ofNullable(city)
);
};
List<PersonWithOptionalCity> results = template.query(
"MATCH (p:Person) OPTIONAL MATCH (p)-[:LIVES_IN]->(c:City) RETURN p, c",
mapper
);
Type Checking
Use isNode() and isRelationship() for type-safe access:
RowMapper<Object> dynamicMapper = (row) -> {
if (row.isNode("result")) {
Map<String, Value> node = row.getNode("result");
return "Node: " + ValueMappers.asString(node.get("name"));
} else if (row.isRelationship("result")) {
RelationshipData rel = row.getRelationship("result");
return "Relationship: " + rel.labelName();
} else {
return "Value: " + row.getValue("result").getValue();
}
};
Registering with EntityRegistry
import com.thecookiezen.ladybugdb.spring.repository.support.EntityDescriptor;
import com.thecookiezen.ladybugdb.spring.repository.support.EntityRegistry;
EntityRegistry registry = new EntityRegistry();
RowMapper<Person> reader = (row) -> {
Map<String, Value> node = row.getNode("n");
return new Person(
ValueMappers.asString(node.get("name")),
ValueMappers.asInteger(node.get("age"))
);
};
EntityWriter<Person> writer = (entity) -> Map.of("age", entity.getAge());
registry.registerDescriptor(Person.class, new EntityDescriptor<>(Person.class, reader, writer));
// Now use with template
List<Person> people = template.query("MATCH (n:Person) RETURN n", Person.class);
Error Handling
RowMapper<Person> safeMapper = (row) -> {
try {
Map<String, Value> node = row.getNode("n");
return new Person(
ValueMappers.asString(node.get("name")),
ValueMappers.asInteger(node.get("age"))
);
} catch (Exception e) {
throw new RuntimeException("Failed to map Person row", e);
}
};
The template wraps mapping exceptions in CypherMappingException:
try {
List<Person> people = template.query("MATCH (n) RETURN n", Person.class);
} catch (LadybugDBTemplate.CypherMappingException e) {
logger.error("Mapping failed", e.getCause());
}
4.4. Value Mappers
ValueMappers is a utility class for converting LadybugDB Value objects to Java types.
Scalar Types
asString
Converts a Value to String. Returns null for null values.
String name = ValueMappers.asString(value);
List Types
Null Safety
All mappers handle null gracefully:
Value nullValue = null;
String result = ValueMappers.asString(nullValue); // Returns null
Value nodeValue = node.get("nonexistent");
Integer age = ValueMappers.asInteger(nodeValue); // Returns null
Usage in RowMappers
Node Properties
RowMapper<Person> mapper = (row) -> {
Map<String, Value> node = row.getNode("n");
return new Person(
ValueMappers.asString(node.get("name")),
ValueMappers.asInteger(node.get("age")),
ValueMappers.asBoolean(node.get("active")),
ValueMappers.asDouble(node.get("score"))
);
};
Direct Column Values
RowMapper<String> nameMapper = (row) ->
ValueMappers.asString(row.getValue("name"));
RowMapper<Long> countMapper = (row) ->
ValueMappers.asLong(row.getValue("count"));
Usage in EntityWriters
The template converts Java types to Values automatically. Common patterns:
EntityWriter<Document> writer = (entity) -> {
Map<String, Object> props = new HashMap<>();
props.put("title", entity.getTitle()); // String
props.put("views", entity.getViews()); // Integer
props.put("score", entity.getScore()); // Double
props.put("active", entity.isActive()); // Boolean
props.put("tags", entity.getTags()); // List<String>
props.put("embedding", entity.getEmbedment()); // float[]
return props;
};
Type Conversion Table
| Java Type | Mapper Method | LadybugDB Type |
|---|---|---|
|
|
STRING |
|
|
INT64 |
|
|
INT64 |
|
|
DOUBLE |
|
|
BOOLEAN |
|
|
LIST |
|
|
LIST |
|
|
LIST |
|
|
LIST |
|
|
LIST |
5. Repositories
5.1. Node Repository
SimpleNodeRepository provides CRUD operations for node entities, extending the standard Spring Data CrudRepository.
NodeRepository Interface
Your custom repository will typically extend NodeRepository, which gives you out-of-the-box CRUD and relationship management:
import org.springframework.data.repository.CrudRepository;
@NoRepositoryBean
public interface NodeRepository<T, ID, R, S> extends CrudRepository<T, ID> {
// Relationship operations
R createRelation(S source, T target, R relationship);
List<R> findRelationsBySource(S source);
List<R> findAllRelations();
void deleteRelation(R relationship);
void deleteRelationBySource(T source);
Optional<R> findRelationById(ID id);
}
CRUD Operations
Since NodeRepository extends CrudRepository, all standard standard operations are supported seamlessly without writing boilerplate:
-
save(entity)/saveAll(entities): Inserts or updates entities using CypherMERGE. -
findById(id)/findAll(): Retrieves entities mapping their graph properties to your Java object. -
count()/existsById(id): Lightweight aggregations to check entity presence. -
delete(entity)/deleteById(id)/deleteAll(…): UsesDETACH DELETEto safely remove nodes and their relationships.
Relationship Operations
In addition to CRUD, NodeRepository can handle custom relationship creation and retrieval. See Relationships for detailed documentation on entity relationships.
Person alice = repository.findById("Alice").orElseThrow();
Person bob = repository.findById("Bob").orElseThrow();
// Create a relationship
Follows mappedFollows = new Follows("alice_bob", alice, bob, 2020);
repository.createRelation(alice, bob, mappedFollows);
// Query relationships
List<Follows> relationsFromAlice = repository.findRelationsBySource(alice);
Entity Requirements and Registration
To properly manage an entity, it must:
1. Be annotated with @NodeEntity (label defaults to class name).
2. Have an @Id field mapped to the graph node’s primary key.
3. Define a no-argument constructor for instantiation.
If using LadybugDB programmatically outside of Spring’s component scan, you must register its EntityDescriptor with the EntityRegistry, providing a RowMapper and EntityWriter.
EntityRegistry registry = new EntityRegistry();
registry.registerDescriptor(Person.class,
new EntityDescriptor<>(Person.class, personReader, personWriter));
SimpleNodeRepository<Person, Void, String> repo = new SimpleNodeRepository<>(
template, Person.class, Void.class,
registry.getDescriptor(Person.class), null
);
When using @EnableLadybugDBRepositories in a standard Spring context, this registration is handled automatically based on your @NodeEntity classes.
5.2. Custom Queries
Define custom queries on repository interfaces using the @Query annotation. Spring Data LadybugDB processes these queries at runtime.
@Query Annotation
You can use the @Query annotation to supply custom Cypher for repository methods:
import com.thecookiezen.ladybugdb.spring.annotation.Query;
import org.springframework.data.repository.query.Param;
public interface PersonRepository extends NodeRepository<Person, String, Void, Person> {
@Query("MATCH (n:Person) WHERE n.age > $minAge RETURN n")
List<Person> findByAgeGreaterThan(@Param("minAge") int minAge);
@Query(value = "MATCH (n:Person {name: $name}) DETACH DELETE n", modifying = true)
void deleteByName(@Param("name") String name);
}
Parameter Binding
Always bind parameters with $paramName in Cypher and @Param("paramName") in your method signature.
@Query("MATCH (n:Person) WHERE n.name IN $names AND n.age = $age RETURN n")
List<Person> findByNamesAndAge(@Param("names") List<String> names, @Param("age") int age);
Return Types
The template automatically processes the results into common Java types:
-
List / Iterable: For multi-result sets (
List<Person>). -
Optional / Single Entity: Returns one instance, or
Optional.empty()/nullif not found. -
Scalars: Extract single columns like
List<String>,Integer, orLong. -
Void: For
modifying = truequeries.
Extension Loading
If your query requires LadybugDB extensions (like vector), declare them in the query annotation:
@Query(
value = "MATCH (n:Document) WHERE vector_search(n.embedding, $query, metric := 'cosine') < 0.5 RETURN n",
loadExtensions = {"vector"}
)
List<Document> findSimilarDocuments(@Param("query") float[] query);
Complex Queries and Projections
You can map results to records for custom projections:
public record PersonSummary(String name, int age) {}
@Query("MATCH (n:Person) RETURN n.name AS name, n.age AS age")
List<PersonSummary> findAllSummaries();
| Note that your projections should have matching field names with the returned Cypher aliases. |
5.3. Entity Mapping
Map Java classes to LadybugDB nodes and relationships using annotations. Spring Data LadybugDB provides straightforward annotations to handle graph-to-object mapping.
@NodeEntity
Marks a class as a node entity stored in a node table.
import com.thecookiezen.ladybugdb.spring.annotation.NodeEntity;
import org.springframework.data.annotation.Id;
@NodeEntity(label = "Person")
public class Person {
@Id
private String name;
private int age;
// Default constructor is required
public Person() {}
}
@Id
Marks the primary key field for the entity.
LadybugDB node tables require a primary key. The @Id field must match the table’s primary key definition.
|
@RelationshipEntity
Marks a class as a relationship entity connecting two nodes.
import com.thecookiezen.ladybugdb.spring.annotation.RelationshipEntity;
import org.springframework.data.annotation.Id;
@RelationshipEntity(
type = "KNOWS",
nodeType = Person.class,
sourceField = "from",
targetField = "to"
)
public class Knows {
@Id
private String id;
private Person from;
private Person to;
private int since;
public Knows() {}
}
5.4. Relationships
Create and manage relationships between nodes using the repository API.
Relationship Entity Definition
import com.thecookiezen.ladybugdb.spring.annotation.RelationshipEntity;
import org.springframework.data.annotation.Id;
@RelationshipEntity(
type = "FOLLOWS",
nodeType = Person.class,
sourceField = "from",
targetField = "to"
)
public class Follows {
@Id
String name;
Person from;
Person to;
int since;
public Follows() {}
public Follows(String name, Person from, Person to, int since) {
this.name = name;
this.from = from;
this.to = to;
this.since = since;
}
}
Creating Relationship Table
Before creating relationships, define the relationship table:
template.execute("CREATE NODE TABLE Person(name STRING PRIMARY KEY, age INT64)");
template.execute("CREATE REL TABLE FOLLOWS(FROM Person TO Person, name STRING PRIMARY KEY, since INT64)");
Registering Descriptors
Register both node and relationship descriptors:
EntityRegistry registry = new EntityRegistry();
registry.registerDescriptor(Person.class, personDescriptor);
registry.registerDescriptor(Follows.class, followsDescriptor);
Relationship Writer
EntityWriter<Follows> followsWriter = (entity) -> Map.of(
"name", entity.name,
"since", entity.since
// from/to handled by repository
);
Relationship Reader
RowMapper<Follows> followsReader = (row) -> {
RelationshipData rel = row.getRelationship("rel");
Map<String, Value> sourceNode = row.getNode("s");
Map<String, Value> targetNode = row.getNode("t");
String name = ValueMappers.asString(rel.properties().get("name"));
int since = ValueMappers.asInteger(rel.properties().get("since"));
Person from = new Person(
ValueMappers.asString(sourceNode.get("name")),
ValueMappers.asInteger(sourceNode.get("age"))
);
Person to = new Person(
ValueMappers.asString(targetNode.get("name")),
ValueMappers.asInteger(targetNode.get("age"))
);
return new Follows(name, from, to, since);
};
CRUD Operations
Create Relationship
SimpleNodeRepository<Person, Follows, String> repository = new SimpleNodeRepository<>(
template, Person.class, Follows.class,
personDescriptor, followsDescriptor
);
// First create nodes
Person alice = repository.save(new Person("Alice", 30));
Person bob = repository.save(new Person("Bob", 25));
// Create relationship
Follows follows = new Follows("alice_bob", alice, bob, 2020);
Follows created = repository.createRelation(alice, bob, follows);
The generated Cypher:
MATCH (s:Person {name: $sourceId}), (t:Person {name: $targetId})
MERGE (s)-[rel:FOLLOWS {name: $name}]->(t)
SET rel.since = $since
RETURN s, t, rel
Find Relationships By Source
Find all relationships from a specific node:
Person alice = repository.findById("Alice").orElseThrow();
List<Follows> relationships = repository.findRelationsBySource(alice);
for (Follows f : relationships) {
System.out.println("Alice follows " + f.to.name);
}
Update Relationship
Update by creating a new relationship with the same ID:
Follows existing = repository.findRelationById("alice_bob").orElseThrow();
existing.since = 2021; // Update year
Follows updated = repository.createRelation(existing.from, existing.to, existing);
Querying Relationships
Using Template
List<Follows> follows = template.query(
"MATCH (s:Person)-[rel:FOLLOWS]->(t:Person) RETURN s, rel, t",
followsReader
);
With Conditions
List<Follows> recentFollows = template.query(
"MATCH (s:Person)-[rel:FOLLOWS]->(t:Person) WHERE rel.since >= $year RETURN s, rel, t",
Map.of("year", 2020),
followsReader
);
Using Cypher DSL
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Node;
import org.neo4j.cypherdsl.core.Relationship;
Node s = Cypher.node("Person").named("s");
Node t = Cypher.node("Person").named("t");
Relationship rel = s.relationshipTo(t, "FOLLOWS").named("rel");
Statement statement = Cypher.match(rel)
.where(rel.property("since").gte(Cypher.parameter("year")))
.returning(s, t, rel)
.build();
List<Follows> follows = template.query(statement, Map.of("year", 2020), followsReader);
With Custom Query
public interface PersonRepository extends NodeRepository<Person, String, Follows, Person> {
@Query("MATCH (s:Person)-[rel:FOLLOWS]->(t:Person) WHERE s.name = $name RETURN s, rel, t")
List<Follows> findFollowsByPerson(@Param("name") String name);
@Query("MATCH (s:Person)-[rel:FOLLOWS]->(t:Person) WHERE rel.since >= $year RETURN s, rel, t")
List<Follows> findFollowsSinceYear(@Param("year") int year);
}
6. Reference
6.1. API Overview
This section provides a quick reference for the key packages and interfaces in Spring Data LadybugDB. Use this as a high-level guide to locate the right abstractions. For comprehensive method signatures, please consult the Javadocs and your IDE.
Core Package (com.thecookiezen.ladybugdb.spring.core)
Provides the primary template class (LadybugDBTemplate) for executing database operations. This is the main entry point for executing Cypher queries and managing core connections.
Connection Package (com.thecookiezen.ladybugdb.spring.connection)
Contains factory interfaces and implementations (LadybugDBConnectionFactory, SimpleConnectionFactory, PooledConnectionFactory) for managing database connections. Choose between simple (one connection per operation) or pooled (reusable connections) factories based on your performance requirements.
Mapper Package (com.thecookiezen.ladybugdb.spring.mapper)
Includes interfaces and utilities for converting between database rows and Java objects. Implement RowMapper for reading data and EntityWriter for writing data. The ValueMappers utility provides type-safe conversions for common data types.
Repository Package
The repository package provides Spring Data-style repository abstractions for CRUD operations.
-
com.thecookiezen.ladybugdb.spring.repository: ContainsNodeRepository, the primary interface for entity persistence. -
com.thecookiezen.ladybugdb.spring.repository.support: Contains implementation classes (SimpleNodeRepository) and infrastructure (EntityRegistry). -
com.thecookiezen.ladybugdb.spring.repository.query: Handles custom queries defined with the@Queryannotation.
Annotation Package (com.thecookiezen.ladybugdb.spring.annotation)
Defines custom annotations (@NodeEntity, @RelationshipEntity, @Query) for marking entity classes and defining custom queries. These annotations are processed at runtime to configure entity mapping behavior.