Driver -> Transaction

These two classes are the main points of interactions with a Neo4j database.

A Driver is basically the connection with the Database, and provides a context for performing operations (Transactions) over the database. Usually, you would only need one instance per application.
A Transaction is a logical container for an atomic unit of work. A single Driver can start multiple concurrent Transactions.

Note: Like its Java counterpart, neotypes.Driver is thread safe but neotypes.Transaction is not.

Transaction management

Each Transaction has to be started, used, and finally either committed or rolled back.

neotypes provides 3 ways of interacting with Transactions, designed for different use cases.

Single query + automatic commit / rollback.

If you only need to perform one query, and want it to be automatically committed in case of success, or rolled back in case of failure. You can use the Driver directly.

import neotypes.AsyncDriver
import neotypes.mappers.ResultMapper
import neotypes.syntax.all._ // Provides the query[T] extension method. // Provides the query[T] extension method.

def result(driver: AsyncDriver[F]): F[String] =
  "MATCH (p: Person { name: 'Charlize Theron' }) RETURN p.name"
    .query(ResultMapper.string)
    .single(driver)

Multiple queries + automatic commit / rollback.

Like the previous one, but with the possibility of executing multiple queries in the same Transaction. You can use Driver.transact method.

import neotypes.AsyncDriver
import neotypes.mappers.ResultMapper
import neotypes.syntax.all._ // Provides the query[T] extension method. // Provides the query[T] extension method.

def result(driver: AsyncDriver[F]): F[(String, String)] =
  driver.transact { tx =>
    for {
      r1 <-"MATCH (p: Person { name: 'Charlize Theron' }) RETURN p.name"
        .query(ResultMapper.string)
        .single(tx)
      r2 <-"MATCH (p: Person { name: 'Tom Hanks' }) RETURN p.name"
        .query(ResultMapper.string)
        .single(tx)
    } yield (r1, r2)
  }

Note: under the hood, the previous method uses this one. Thus, they are equivalent for single-query operations.

Multiple queries + explicit commit / rollback.

If you want to control when to commit or rollback a Transaction. You can use the Driver.transaction method, to create an F[Transaction[F]].

import neotypes.AsyncDriver
import neotypes.syntax.all._ // Provides the query[T] extension method. // Provides the query[T] extension method.

def result(driver: AsyncDriver[F]): F[Unit] =
  driver.transaction.flatMap { tx =>
    for {
      _ <-"CREATE (p: Person { name: 'Charlize Theron' })".execute.void(tx)
      _ <-"CREATE (p: Person { name: 'Tom Hanks' })".execute.void(tx)
      _ <- tx.rollback // Nothing will be done.
    } yield ()
  }

Note: It is mandatory to only call either commit or rollback once. Calling both or one of them but more than once will leave the system in an undefined state. (probably an error or a deadlock)

Transaction configuration.

You can configure the timeout, access mode, database and metadata of a Transaction Using a custom TransactionConfig

import neotypes.TransactionConfig
import neotypes.model.query.QueryParam
import scala.concurrent.duration._

val config =
  TransactionConfig
    .default
    .withTimeout(2.seconds)
    .withMetadata(Map("foo" -> QueryParam("bar"), "baz" -> QueryParam(10)))
// config: TransactionConfig = neotypes.TransactionConfig@3a97c329

Which you can use in operations that explicitly or implicitly create Transactions.

import neotypes.{AsyncDriver, AsyncTransaction}
import neotypes.mappers.ResultMapper
import neotypes.syntax.all._ // Provides the query[T] extension method. // Provides the query[T] extension method.

def customTransaction(driver: AsyncDriver[F]): F[AsyncTransaction[F]] =
  driver.transaction(config)

def result1(driver: AsyncDriver[F]): F[(String, String)] =
  driver.transact(config) { tx =>
    for {
      r1 <-"MATCH (p:Person {name: 'Charlize Theron'}) RETURN p.name"
        .query(ResultMapper.string)
        .single(tx)
      r2 <-"MATCH (p:Person {name: 'Tom Hanks'}) RETURN p.name"
        .query(ResultMapper.string)
        .single(tx)
    } yield (r1, r2)
  }

def result2(driver: AsyncDriver[F]): F[String] =
  "MATCH (p:Person {name: 'Charlize Theron'}) RETURN p.name"
    .query(ResultMapper.string)
    .single(driver, config)