Txn
provides a high level interface to Jena transactions. It is a
library over the core functionality - applications do not have to use Txn
to use transactions.
Features:
- Java8 idioms
- Application exceptions cause transaction aborts.
- “Transaction continuation” - use any existing active transaction.
- Autocommit - ensure actions are inside a transaction even if none is active.
Transactions
The basic transactions API provides operations
begin
, commit
, abort
and end
.
A write transaction looks like:
dataset.begin(ReadWrite.write) ;
try {
... write operations ...
dataset.commit() ; // Or abort
} finally {
dataset.end() ;
}
This can be simplified by wrapping application code, contained in a Java lambda expression or a Java Runnable object, and calling a method of the dataset or other transactional object. This wil apply the correct entry and exit code for a transaction, eliminating coding errors.
This is also available via transactional objects such as Dataset
.
The pattern is:
Dataset dataset = ...
dataset.executeRead(()-> {
. . .
}) ;
and
dataset.executeWrite(()-> {
. . .
}) ;
The form is:
Txn.executeRead(ds, ()-> {
. . .
}) ;
and
Txn.executeWrite(ds, ()-> {
. . .
}) ;
is also available (Txn
is the implementation of this machinery). Using Txn is
this way is necessary for Jena3.
Usage
This first example shows how to write a SPARQL query .
Dataset dataset = ... ;
Query query = ... ;
dataset.executeRead(()-> {
try(QueryExecution qExec = QueryExecutionFactory.create(query, dataset)) {
ResultSetFormatter.out(qExec.execSelect()) ;
}
}) ;
Here, a try-with-resources
ensures correct handling of the
QueryExecution
inside a read transaction.
Writing to a file is a read-action (it does not update the RDF data, the writer needs to read the dataset or model):
Dataset dataset = ... ;
dataset.executeRead(()-> {
RDFDataMgr.write(System.out, dataset, Lang.TRIG) ;
}) ;
whereas reading data into an RDF dataset needs to be a write transaction (the dataset or model is changed).
Dataset dataset = ... ;
dataset.executeWrite(()-> {
RDFDataMgr.read("data.ttl") ;
}) ;
Applications are not limited to a single operation inside a transaction. It can involve multiple applications read operations, such as making several queries:
Dataset dataset = ... ;
Query query1 = ... ;
Query query2 = ... ;
dataset.executeRead(()-> {
try(QueryExecution qExec1 = QueryExecutionFactory.create(query1, dataset)) {
...
}
try(QueryExecution qExec2 = QueryExecutionFactory.create(query2, dataset)) {
...
}
}) ;
A calculateRead
block can return a result but only with the condition
that what is returned does not touch the data again unless it uses a new
transaction.
This includes returning a result set or returning a model from a dataset.
ResultSets
by default stream - each time hasNext
or next
is
called, new data might be read from the RDF dataset. A copy of the
results needs to be taken:
Dataset dataset = ... ;
Query query = ... ;
List<String> results = dataset.calculateRead(()-> {
List<String> accumulator = new ArrayList<>() ;
try(QueryExecution qExec = QueryExecutionFactory.create(query, dataset)) {
qExec.execSelect().forEachRemaining((row)->{
String strThisRow = row.getLiteral("variable").getLexicalForm() ;
accumulator.add(strThisRow) ;
}) ;
}
return accumulator ;
}) ;
// use "results"
Dataset dataset = ... ;
Query query = ... ;
ResultSet List<String> resultSet = dataset.calculateRead(()-> {
List<String> accumulator = new ArrayList<>() ;
try(QueryExecution qExec = QueryExecutionFactory.create(query, dataset)) {
return ResultSetFactory.copyResults(qExec.execSelect()) ;
}
}) ;
// use "resultSet"
The functions execute
and calculate
start READ_PROMOTE
transactions which start in “read” mode but convert to “write” mode if
needed. For details of transaction promotion see the
section in the transaction API documentation.
Working with RDF Models
The unit of transaction is the dataset. Model in datasets are just views of that dataset. Model should not be passed out of a transaction because they are still attached to the dataset.
Autocommit and Transaction continuation
If there is a transaction already started for the thread, then
execute...
or calculate...
will be performed as part of
the transaction and that transaction is not terminated. If there is not transaction already started,
a transaction is wrapped around the execute...
or calculate...
action.
Dataset dataset = ...
// Main transaction.
dataset.begin(ReadWrite.WRITE) ;
try {
...
// Part of the transaction above.
dataset.executeRead(() -> ...) ;
...
// Part of the transaction above - no commit/abort
dataset.executeWrite(() -> ...) ;
// Outer transaction
dataset.commit() ;
} finally { dataset.end() ; }
Design
Txn
uses Java Runnable
for the application code, passed into control code
that wraps the transaction operations around the application code. This results
in application code automatically applied transaction begin/commit/end as needed.
A bare read transaction requires the following code structure (no exception handling):
txn.begin(ReadWrite.READ) ;
try {
... application code ...
} finally { txn.end() ; }
while a write transaction requires either a commit
or an abort
at the end of the application code as well.
Without the transaction continuation code (simplified, the Txn
code
for a read transaction takes the form:
public static <T extends Transactional> void executeRead(T txn, Runnable r) {
txn.begin(ReadWrite.READ) ;
try { r.run() ; }
catch (Throwable th) { txn.end() ; throw th ; }
txn.end() ;
}
See Txn.java
for full details.