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.

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.1</version>
</dependency>
<dependency>
    <groupId>com.arcadedb</groupId>
    <artifactId>arcadedb-network</artifactId>
    <version>26.10.1</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:

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

getVertices(direction, edgeTypes…​)

Returns all neighbor vertices, transparently paginating in batches of DEFAULT_PAGE_SIZE (2,000) records per HTTP call.

getVerticesPaged(direction, pageSize, edgeTypes…​)

Same as above but with an explicit page size. Use to tune round-trip vs. payload trade-off for supernodes.

getVertices(direction, limit, skip, edgeTypes…​)

Returns a single page of up to limit neighbor vertices starting at offset skip. Useful for implementing your own pagination.

getEdges(direction, edgeTypes…​)

Returns all connected edges, transparently paginating with DEFAULT_PAGE_SIZE per HTTP call.

getEdgesPaged(direction, pageSize, edgeTypes…​)

Same as above but with an explicit page size.

getEdges(direction, limit, skip, edgeTypes…​)

Returns a single page of up to limit edges starting at offset skip.

countEdges(direction, edgeTypes…​)

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.)

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}