Jena Transactions API

API for Transactions

This page describes the basic transaction API in Jena (3.1.0 and later).

There is also a higher-level API useful in many situations but sometimes it is necessary to use the basic transaction API described here.

Read transactions

These are used for SPARQL queries and code using the Jena API actions that do not change the data. The general pattern is:

dataset.begin(ReadWrite.READ) ;
try {
    ...
} finally { dataset.end() ; }

The dataset.end() declares the end of the read transaction. Applications may also call dataset.commit() or dataset.abort() which all have the same effect for a read transaction.

This example has two queries - no updates between or during the queries will be seen by this code even if another thread commits changes in the lifetime of this transaction.

Dataset dataset = ... ;
dataset.begin(ReadWrite.READ) ;
try {
    String qs1 = "SELECT * {?s ?p ?o} LIMIT 10" ;        
    try(QueryExecution qExec = QueryExecution.create(qs1, dataset)) {
        ResultSet rs = qExec.execSelect() ;
        ResultSetFormatter.out(rs) ;
    }

    String qs2 = "SELECT * {?s ?p ?o} OFFSET 10 LIMIT 10" ;  
    try(QueryExecution qExec = QueryExecution.create(qs2, dataset)) {
        rs = qExec.execSelect() ;
        ResultSetFormatter.out(rs) ;
    }
} finally { dataset.end() ; }

Write transactions

These are used for SPARQL queries, SPARQL updates and any Jena API actions that modify the data. Beware that large model.read operations to change a dataset may consume large amounts of temporary space.

The general pattern is:

dataset.begin(ReadWrite.WRITE) ;
try {
    ...
    dataset.commit() ;
} finally {
    dataset.end() ;
}

The dataset.end() will abort the transaction is there was no call to dataset.commit() or dataset.abort() inside the write transaction.

Once dataset.commit() or dataset.abort() is called, the application needs to start a new transaction to perform further operations on the dataset.

Dataset dataset = ... ;
dataset.begin(TxnType.WRITE) ;

try {
    Model model = dataset.getDefaultModel() ;
    // API calls to a model in the dataset

    // Make some changes via the model ...
    model.add( ... )

    // A SPARQL query will see the new statement added.
    try (QueryExecution qExec = QueryExecution.create(
         "SELECT (count(?s) AS ?count) { ?s ?p ?o} LIMIT 10",
         dataset)) {
        ResultSet rs = qExec.execSelect() ;
        ResultSetFormatter.out(rs) ;
    }

    // ... perform a SPARQL Update
    String sparqlUpdateString = StrUtils.strjoinNL(
        "PREFIX . <http://example/>",
        "INSERT { :s :p ?now } WHERE { BIND(now() AS ?now) }"
    ) ;

    UpdateRequest request = UpdateFactory.create(sparqlUpdateString) ;
    UpdateExecution.dataset(dataset).update(request).execute() ;

    // Finally, commit the transaction.
    dataset.commit() ;
    // Or call .abort()
} finally {
    dataset.end() ;
}

Transaction Types, Modes and Promotion.

Transaction have a type (enum TxnType) and a mode (enum ReadWrite). TxnType.READ and TxnType.Write start the transaction in that mode and the mode is fixed for the transaction’s lifetime. A READ transaction can never update the data of the transactional object it is acting on.

Transactions can have type TxnType.READ_PROMOTE or TxnType.READ_COMMITTED_PROMOTE. These start in mode READ but can become mode WRITE, either implicitly by attempting an update, or explicitly by calling promote.

READ_PROMOTE only succeeds if no writer has made any changes since this transaction started. It gives full isolation.

READ_COMMITTED_PROMOTE always succeeds because it changes the view of the data to include any changes made up to that point (it is “read committed”). Applications should be aware that data they have read up until the point of promotion (the first call or .promote or first update made) may now be invalid. For this reason, READ_PROMOTE is preferred.

begin(), the method with no arguments, is equivalent to begin(TxnType.READ_PROMOTE).

Multi-threaded use

Each dataset object has one transaction active at a time per thread. A dataset object can be used by different threads, with independent transactions.

The usual idiom within multi-threaded applications is to have one dataset, and so there is one transaction per thread.

Either:

// Create a dataset and keep it globally.
static Dataset dataset = TDBFactory.createDataset(location) ;

Thread 1:

dataset.begin(TxnType.WRITE) ;
try {
    ...
    dataset.commit() ;
} finally { dataset.end() ; }

Thread 2:

dataset.begin(TxnType.READ) ;
try {
    ...
} finally { dataset.end() ; }

It is possible (in TDB) to create different dataset objects to the same location.

Thread 1:

Dataset dataset = TDBFactory.createDataset(location) ;
dataset.begin(TxnType.WRITE) ;
try {
    ...
    dataset.commit() ;
} finally { dataset.end() ; }

Thread 2:

Dataset dataset = TDBFactory.createDataset(location) ;
dataset.begin(TxnType.READ) ;
try {
    ...
} finally { dataset.end() ; }

Each thread has a separate dataset object; these safely share the same storage and have independent transactions.

Multi JVM

Multiple applications, running in multiple JVMs, using the same file databases is not supported and has a high risk of data corruption. Once corrupted a database cannot be repaired and must be rebuilt from the original source data. Therefore there must be a single JVM controlling the database directory and files. From 1.1.0 onwards TDB includes automatic prevention against multi-JVM which prevents this under most circumstances.

Use our Fuseki component to provide a database server for multiple applications. Fuseki supports SPARQL Query, SPARQL Update and the SPARQL Graph Store protocol.