Was this page helpful?
ScyllaDB Java Driver is available under the Apache v2 License. ScyllaDB Java Driver is a fork of DataStax Java Driver. See Copyright here.
Caution
Java Driver 3.x is deprecated and in maintenance mode — it receives critical bug fixes only. New projects and active development should use Java Driver 4.x; see the migration guide.
Use PreparedStatement for queries that are executed multiple times in your application:
PreparedStatement prepared = session.prepare(
"insert into product (sku, description) values (?, ?)");
BoundStatement bound = prepared.bind("234827", "Mouse");
session.execute(bound);
session.execute(prepared.bind("987274", "Keyboard"));
When you prepare the statement, Cassandra will parse the query string, cache the result and return a unique identifier
(the PreparedStatement object keeps an internal reference to that identifier):
client driver Cassandra
--+------------------------+----------------+------
| | |
| session.prepare(query) | |
|----------------------->| |
| | PREPARE(query) |
| |--------------->|
| | |
| | |
| | | - compute id
| | | - parse query string
| | | - cache (id, parsed)
| | |
| | PREPARED(id) |
| |<---------------|
| PreparedStatement(id) | |
|<-----------------------| |
When you bind and execute a prepared statement, the driver will only send the identifier, which allows Cassandra to skip the parsing phase:
client driver Cassandra
--+---------------------------------+---------------------+------
| | |
| session.execute(BoundStatement) | |
|-------------------------------->| |
| | EXECUTE(id, values) |
| |-------------------->|
| | |
| | |
| | | - get cache(id)
| | | - execute query
| | |
| | ROWS |
| |<--------------------|
| | |
|<--------------------------------| |
You should prepare only once, and cache the PreparedStatement in your application (it is thread-safe). If you call
prepare multiple times with the same query string, the driver will log a warning.
If you execute a query only once, a prepared statement is inefficient because it requires two roundtrips. Consider a simple statement instead.
Parameters can be either anonymous or named (named parameters are only available with native protocol v2 or above):
ps1 = session.prepare("insert into product (sku, description) values (?, ?)");
ps2 = session.prepare("insert into product (sku, description) values (:s, :d)");
To turn the statement into its executable form, you need to bind it to create a BoundStatement. As shown previously, there is a shorthand to provide the parameters in the same call:
BoundStatement bound = ps1.bind("324378", "LCD screen");
You can also bind first, then use setters, which is slightly more explicit:
// Positional setters:
BoundStatement bound = ps1.bind()
.setString(0, "324378")
.setString(1, "LCD screen");
// Named setters:
BoundStatement bound = ps2.bind()
.setString("s", "324378")
.setString("d", "LCD screen");
Named setters also work when the query uses anonymous ? markers: the
server synthesizes a name for each one, usually after the column it
applies to.
// Works, but relies on a name the server made up:
BoundStatement bound = ps1.bind()
.setString("sku", "324378")
.setString("description", "LCD screen");
Avoid relying on this. Synthesized names are not part of any API
contract, and their spelling differs between server release lines, not
just between successive versions. For
SELECT ... WHERE application_id IN ?, ScyllaDB’s 2024.1 releases name
the marker in(application_id), while 2026.1.8 names it
IN(application_id); the lowercase spelling is restored in 2026.1.12 and
2026.2.6. Apache Cassandra names it in(application_id). The regression
was reported against a driver that matches these names exactly: the
hardcoded lowercase spelling stopped resolving, the variable went out
unset, and the server rejected the request with Unexpected unset value for bind variable 1. This driver is more forgiving, but only on some of
its lookup paths — see below. The spelling can even vary from node to
node during a rolling upgrade, because the driver keeps whichever
metadata the node that served the PREPARE sent back.
The rule of thumb: bind ? markers positionally, and use named setters
only for markers you named yourself with :name.
If you address a synthesized name anyway, note that the name setters match
case-insensitively, so setList("in(pk)", ...) still finds a variable the
server called IN(pk) — a change of case alone is survivable. Double
quoting the name, as in setList("\"in(pk)\"", ...), opts into an exact
match instead: it does not resolve, and the setter throws
IllegalArgumentException rather than quietly leaving the variable unset.
Note that the exception comes from the setter — querying the metadata
directly, with ps.getVariables().getIndexOf(...), reports the same miss
as -1.
Finally, a named setter writes every variable that matches the name,
not just the first one. Names are therefore ambiguous whenever a query
mentions the same column more than once, for example: select * from sales where sku = ? and date > ? and date < ?, where both range markers are
named date. Use positional setters in those cases.
For native protocol V3 or below, all variables must be bound. With native
protocol V4 or above, variables can be left unset, in which case they
will be ignored server side (no tombstones will be generated). If you’re
reusing a bound statement you can use the unset method to unset variables
that were previously set:
BoundStatement bound = ps1.bind()
.setString("sku", "324378")
.setString("description", "LCD screen");
// Using the unset method to unset previously set value.
// Positional setter:
bound.unset(1);
// Named setter:
bound.unset("description");
A bound statement also has getters to retrieve the values. Note that this has a small performance overhead since values are stored in their serialized form.
BoundStatement is not thread-safe. You can reuse an instance multiple times with different parameters, but only
from a single thread and only if you use synchronous calls:
BoundStatement bound = ps1.bind();
// This is safe:
bound.setString("sku", "324378");
session.execute(bound);
bound.setString("sku", "324379");
session.execute(bound);
// This is NOT SAFE. executeAsync runs concurrently with your code, so the first execution might actually read the
// values after the second setString call, and you would insert 324381 twice:
bound.setString("sku", "324380");
session.executeAsync(bound);
bound.setString("sku", "324381");
session.executeAsync(bound);
Also, make sure you don’t accidentally reuse parameters from previous executions.
Cassandra does not replicate prepared statements across the cluster. It is the driver’s responsibility to ensure that each node’s cache is up to date. It uses a number of strategies to achieve this:
When a statement is initially prepared, it is first sent to a single node in the cluster (this avoids hitting all nodes in case the query string is wrong). Once that node replies successfully, the driver re-prepares on all remaining nodes:
client driver node1 node2 node3
--+------------------------+----------------+--------------+------+---
| | | | |
| session.prepare(query) | | | |
|----------------------->| | | |
| | PREPARE(query) | | |
| |--------------->| | |
| | | | |
| | PREPARED(id) | | |
| |<---------------| | |
| | | | |
| | | | |
| | PREPARE(query) | |
| |------------------------------>| |
| | | | |
| | PREPARE(query) | |
| |------------------------------------->|
| | | | |
|<-----------------------| | | |
The prepared statement identifier is deterministic (it’s a hash of the query string), so it is the same for all nodes.
if a node crashes, it loses all of its prepared statements. So the driver keeps a client-side cache; anytime a node is marked back up, the driver re-prepares all statements on it;
finally, if the driver tries to execute a statement and finds out that the coordinator doesn’t know about it, it will re-prepare the statement on the fly (this is transparent for the client, but will cost two extra roundtrips):
client driver node1
--+-------------------------------+------------------------------+--
| | |
|session.execute(boundStatement)| |
+------------------------------>| |
| | EXECUTE(id, values) |
| |----------------------------->|
| | |
| | UNPREPARED |
| |<-----------------------------|
| | |
| | |
| | PREPARE(query) |
| |----------------------------->|
| | |
| | PREPARED(id) |
| |<-----------------------------|
| | |
| | |
| | EXECUTE(id, values) |
| |----------------------------->|
| | |
| | ROWS |
| |<-----------------------------|
| | |
|<------------------------------| |
You can customize these strategies through QueryOptions:
setPrepareOnAllHosts controls whether statements are initially re-prepared on other hosts (step 1 above);
setReprepareOnUp controls whether statements are re-prepared on a node that comes back up (step 2 above).
Changing the driver’s defaults should be done with care and only in specific situations; read each method’s Javadoc for detailed explanations.
Both the driver and Cassandra maintain a mapping of PreparedStatement queries to their
metadata. When a change is made to a table, such as a column being added or dropped, there
is currently no mechanism for Cassandra to invalidate the existing metadata. Because of this,
the driver is not able to properly react to these changes and will improperly read rows after
a schema change is made.
Therefore it is currently recommended to list all columns of interest in
your prepared statements (i.e. SELECT a, b, c FROM table), instead of
relying on SELECT *.
This will be addressed in a future release of both Cassandra and the driver. Follow CASSANDRA-10786 and JAVA-1196 for more information.
Was this page helpful?
ScyllaDB Java Driver is available under the Apache v2 License. ScyllaDB Java Driver is a fork of DataStax Java Driver. See Copyright here.