Java API (Remote)
ArcadeDB comes with a convenient Java library to work with a remote database.
RemoteServer Class
The RemoteServer class is used to manage remote server connections and databases.
The server administration calls (databases, users, groups and API tokens) prefer the cluster leader and wait out an election in progress. If the server becomes unreachable, they switch to another server of the cluster only when arcadedb.network.sameServerErrorRetries is greater than 0 (the default is 0). A write whose answer was lost is never sent again, because the server may already have applied it. createApiToken refuses to send the token over plain HTTP to a non-loopback host, whichever server the request is sent to, unless setAllowInsecureApiTokenTransport(true) is set.
(Since v26.10.2.) acceptDivergedDatabase(databaseName) and acceptStaleSnapshot() are the operator overrides for a
node of a replicated cluster that has no peer left to resync from. Both are root only and accept the node’s local copy
as the truth without a resync, so the entries that were skipped are not replayed. acceptDivergedDatabase lifts the
quarantine, and its read floor, standing on one database, through POST /api/v1/cluster/accept-diverged/{database}.
It is allowed on a node that is the only voter of its cluster, or when every voter holds the same database quarantined,
since no node then has a copy to resync from. It returns result, database, localServer and appliedIndex, plus
divergenceCause and readFloor when they stood. acceptStaleSnapshot lifts the node-wide stale-snapshot read floor,
through POST /api/v1/cluster/accept-stale-snapshot, on a sole voter only, and returns result, localServer,
readFloor, snapshotIndex and appliedIndex. The server answers 404 when nothing stands to be lifted (for
acceptDivergedDatabase, when the database carries no quarantine and no read floor) and 409 where a resync from a
peer is still the way out; acceptStaleSnapshot also answers 409 while a snapshot download is running. The gRPC
client RemoteGrpcServer has the same two methods, returning the AcceptDivergedDatabaseResponse and
AcceptStaleSnapshotResponse messages and raising its not-found and failed-precondition errors for the same cases (see
gRPC API). When to use them is described in Lifting a quarantine without a resync
and Lifting the stale-snapshot read floor.
(Since v26.10.2.) A create refused because the name is already taken raises
com.arcadedb.remote.RemoteAlreadyExistsException: a user (createUser), an API token (createApiToken) or a
database (create). The server answers these with 409 Conflict, and the gRPC server with ALREADY_EXISTS;
RemoteGrpcServer raises the same exception from createUser, createApiToken and createDatabase, where it used to
report a duplicated key (or, for createDatabase, a plain RuntimeException). It extends RemoteException, so code
that caught RemoteException for these cases keeps working, and a provisioning tool can catch it to treat "already
there" as success. In 26.10.1 and earlier a taken user name answered 403 (Security error) and a taken database name
400. Restoring a backup into a database name that is taken still answers 400.
RemoteDatabase Class
The RemoteDatabase class is used to work with a remote database instance.
Add the following dependencies in your Maven pom.xml file under the tag <dependencies>:
<dependency>
<groupId>com.arcadedb</groupId>
<artifactId>arcadedb-engine</artifactId>
<version>26.10.2</version>
</dependency>
<dependency>
<groupId>com.arcadedb</groupId>
<artifactId>arcadedb-network</artifactId>
<version>26.10.2</version>
</dependency>
10-Minute Tutorial
Connect to a remote database
There are 2 main classes that handle the connection to the remote database by translating the Java API into http-calls:
-
RemoteServer, to create new database on a remote server -
RemoteDatabase, to work with a remote database instance
We start by creating a RemoteServer:
RemoteServer server = new RemoteServer("localhost", 2480, "root", "playwithdata");
Next, let’s create a RemoteDatabase instance pointing to the database "mydb" on ArcadeDB Server located on "localhost", port 2480 — the HTTP port, which is what RemoteDatabase speaks. User and passwords are respectively "root" and "playwithdata":
RemoteDatabase database = new RemoteDatabase("localhost", 2480, "mydb", "root", "playwithdata");
The RemoteDatabase is not thread-safe. It holds a backend session. If a thread closes a transaction, it also closes the session, so that any request performed by a concurrent thread using the same RemoteDatabase instance leads to unexpected behavior. To avoid concurrency problems you should not hold RemoteDatabase instances as fields on the heap, but rather create a new instance in method scope as a variable on the stack.
|
RemoteImmutableVertex and RemoteMutableVertex
Vertices retrieved through RemoteDatabase are instances of RemoteImmutableVertex (or RemoteMutableVertex after calling modify()).
These classes expose graph-traversal methods that translate directly into SQL queries over HTTP — there is no per-vertex round trip.
Traversal methods
| Method | Description |
|---|---|
|
Returns all neighbor vertices, transparently paginating in batches of |
|
Same as above but with an explicit page size. Use to tune round-trip vs. payload trade-off for supernodes. |
|
Returns a single page of up to |
|
Returns all connected edges, transparently paginating with |
|
Same as above but with an explicit page size. |
|
Returns a single page of up to |
|
Returns the number of connected edges (single HTTP call, no result materialization). |
DEFAULT_PAGE_SIZE = 2_000 is the batch size used by getVertices() / getEdges(). Values up to 20,000 are safe against the server’s default result limit.
Example
RemoteDatabase db = new RemoteDatabase("localhost", 2480, "mydb", "root", "playwithdata");
// standard traversal -- paginates automatically, no cast needed
Vertex hub = db.lookupByRID(new RID(db, "#1:0"), true);
for (Vertex neighbor : hub.getVertices(Vertex.DIRECTION.OUT, "Knows")) {
System.out.println(neighbor.getString("name"));
}
// supernode -- explicit page size requires cast to the concrete remote type
RemoteImmutableVertex hubRemote = (RemoteImmutableVertex) hub;
for (Vertex neighbor : hubRemote.getVerticesPaged(Vertex.DIRECTION.OUT, 5_000, "Knows")) {
System.out.println(neighbor.getString("name"));
}
Each page uses a SKIP/LIMIT SQL clause, so the edge list is re-scanned from the beginning on every page.
For supernodes with millions of edges prefer a SQL query with a server-side WHERE filter to narrow results before fetching them.
|
The database, which name you have given as the third parameter in the constructor is not automatically created nor is it checked whether it exists. Before you use the RemoteDatabase make sure that the database exists, or, if it doesn’t, create it:
if (!server.exists("mydb"))
server.create();
For a list of all databases hosted by the server use:
List<String> databases = server.databases();
assert(databases.contains("mydb"));
Now that we have the database created we may create the schema (but we may also work without):
String schema =
"""
create vertex type Customer if not exists;
create property Customer.name if not exists string;
create property Customer.surname if not exists string;
create index if not exists on Customer (name, surname) unique;
""";
database.command("sqlscript", schema);
As language we have used sqlscript, which has the advantage that all the statements in the script are executed within one single transaction. Of course you can also send these requests line by line with sql as language. If you go line by line it is important to enclose the requests in a common transaction:
database.begin();
database.command("sql", "create vertex type Customer if not exists");
database.command("sql", "create property Customer.name if not exists string");
database.command("sql", "create property Customer.surname if not exists string");
database.command("sql", "create index if not exists on Customer (name, surname) unique");
database.commit();
Or:
database.transaction(() -> {
...
});
The transaction method encloses the lambda function with a begin - commit, or a rollback in case of any exception.
If the transaction fails because another client modified the same records, it is rolled back and the lambda is executed again, up to arcadedb.txRetries times (3 by default). There is one exception: when transaction(lambda, true) is called from inside a transaction you opened yourself, the error is thrown to you instead of being retried, because re-executing the lambda inside your still open transaction would leave the effects of the failed attempt behind. In that case catch the exception and retry the whole unit of work.
The same happens when the server refuses the transaction before running it: the 503 a node answers while it installs a snapshot, a leader change, or a begin whose connection could not be established. Each retry waits first, with the exponential backoff the embedded transaction() uses (arcadedb.txRetryDelayBase doubling up to arcadedb.txRetryDelay), and when the refusal carries a Retry-After header the retry waits at least that long, up to arcadedb.network.retryAfterMaxWait (30 seconds by default). A duplicated key is retried only once, since failing twice means the duplicate is genuine. A commit the server answered with an error also releases the server-side session right away instead of leaving it to the session timeout. (Since v26.10.1: before, the retries ran back to back, so a node installing a snapshot used up the whole budget within milliseconds.)
A commit whose connection drops after the request was sent is different: the server may already have applied it. Over gRPC such a commit (status UNAVAILABLE with no error from the server) raises a TransactionException and is not retried, so the work is never applied twice. Read the data back to find out whether the commit landed. (Since v26.10.1: before, it was reported as a retryable conflict.)
(Since v26.10.2.) The gRPC client draws the same line for the other writes whose answer can be lost. A bare
UNAVAILABLE (no error class from the server, and not a failure to connect) on an admin write of RemoteGrpcServer
(creating or dropping a database, a user or a token, and the other server writes) or on a streaming write of
RemoteGrpcDatabase (ingestStream, ingestBidi, timeSeriesWriteStream, graph batch loads) raises a
non-retryable RemoteException whose message says the server may already have applied the request. A streaming write
commits as it goes, so any part of it, from none to all, may be durable. Reads, and validate_only dry runs, stay
retryable. A client-side deadline that expires before the server answers raises a TimeoutException whose message says
that the write may already have been applied. Before, these failures were reported as retryable, so a caller that
retried could apply a write twice.
A statement stopped on the server (by the terminate query server command, Cypher
TERMINATE TRANSACTIONS or Studio) raises com.arcadedb.exception.QueryTerminatedException, over HTTP and over gRPC
alike. It is not retried by transaction(): somebody asked for the work to stop. (Since v26.10.2.)
Now that the schema is in place, we will insert some data:
database.command("sql", "insert into Customer(name, surname) values(?, ?)", "Jay", "Miner");
database.command("sql",
"update Customer set lastcall = :lastcall where name = :name and surname = :surname",
Map.of("lastcall", Date.from(Instant.now()), "name", "Jay", "surname", "Miner"));
The first command creates a new vertex of type 'Customer'. The second command updates this vertex by adding a new value for the property 'lastcall'. The first command uses unnamed parameters where the values are mapped by their order in the parameter list. Whereas the second command uses named parameters, which are mapped by the parameter names.
We can now query some customer data from our database:
ResultSet resultSet = database.query("sql", "select from Customer where name = :name",
Map.of("name", "Jay"));
Map<String, Object> customer = resultSet.stream().findFirst().map(Result::toMap).orElse(Map.of());
The query returns a ResultSet, which holds an array of objects of type Result, which can be read as Map:
{name=Jay, @cat=d, @rid=#1:0, lastcall=1675961747290, surname=Miner, @type=Customer}
Streaming results
(Since v26.10.1.) query() and command() wait until the server has serialized the whole result, and keep it in
memory. For a large result use queryStream() or commandStream() instead: they ask the server for the
NDJSON encoding and return a ResultSet that reads each row off the connection as it
arrives, so neither the server nor the client holds more than one row at a time.
database.setMaxResultRows(-1); // no row cap: stream the whole result
try (ResultSet resultSet = database.queryStream("sql", "select from Customer where surname = ?", "Miner")) {
while (resultSet.hasNext()) {
Result customer = resultSet.next();
...
}
}
-
The
ResultSetkeeps an HTTP connection open until it is read to the end or closed, so open it in a try-with-resources. It can be read only once. -
hasNext()returnsfalseonly after the server sent thestatstrailer. A failure on the server raises the exception the buffered call would have raised, and a stream that ends without its trailer (a dropped connection) raises aRemoteException: a short result is never returned silently. -
Without
setMaxResultRows()the server’s default cap applies (arcadedb.server.httpQueryDefaultLimit, 20,000 rows), unless the statement has its ownLIMIT. When the cap drops rows, the client logs a warning andhasNext()returnsfalse. CallsetMaxResultRows(-1)to stream every row, up to the server’s hard ceilingarcadedb.server.httpQueryMaxResultRows. -
commandStream()takes read-only statements only. The server refuses a statement that writes, because its rows would reach the client before it committed. Usecommand()for writes. -
The response has no
explainor write statistics. Usequery()orcommand()when you need them.