Skip to content

Working with a connection

Connecting to a database

To connect to a database you first need to get a hold on ConnectionFactory implementation provided by a rdbc driver. Consult the driver's documentation on how to instantiate and configure ConnectionFactory it provides.

To connect to a database means to obtain Connection instance. Once you have the factory, you have the following options on how to get the connection:

Manually opening and releasing a connection

Connection factory provides connection method that simply returns a Future holding a Connection. A connection obtained this way must be then released manually using release method. Here's the example usage:

import io.rdbc.sapi._
import io.rdbc.util.Futures._

val cf: ConnectionFactory = ???

val result = cf.connect().flatMap { conn =>
   /* use the connection */
   ???
}.andThenF { case _ =>
   conn.release()
}

andThenF future combinator provided by rdbc utilities package is like a standard andThen but partial function passed to it returns a Future and the chain can proceed only when this future completes. Relation between andThenF and andThen is analogous to the relation between flatMap and map.

Using the loan pattern

Even if you haven't heard about the loan pattern you probably know what it is. Connection factory provides withConnection method that executes a block of code in the context of a database connection and releases the connection afterwards. It looks like this:

import io.rdbc.sapi._

val cf: ConnectionFactory = ???

val result = cf.withConnection { conn =>
   /* use the connection */
   ???
}

The above example is equivalent to the one from the previous paragraph.

Transaction management

In rdbc, Connection provides facilities to manage database transactions. Using SQL to manage transaction state by issuing commands like BEGIN or ROLLBACK is not allowed. If you don't manage transaction at all, every SQL statement will be executed in its own transaction. Following sections describe ways of managing transactions.

Manual

Managing transaction manually means using the three methods provided by Connection trait: beginTx, commitTx and rollbackTx. Each of these methods return a Future of Unit — there is no dedicated trait or class representing a transaction. Similar to manual handling of connecting and disconnecting from the database, manual transaction management is kind of cumbersome. When you don't need the flexibility provided by the three methods, use the transactional loan pattern described below.

Using the transactional loan pattern

Connection trait provides withTransaction method which allows to execute a block of code in a context of a newly started transaction. If a Future returned by the block of code is successful, the transaction will be committed — if it fails, the transaction will be rolled back:

import io.rdbc.sapi._

val cf: ConnectionFactory = ???

val result = cf.withConnection { conn =>
   conn.withTransaction {
    /* use the connection in a context of the transaction */
    ???
   }
}

If any error occurs when rolling back the transaction, this error will be reported to the execution context and the original error causing the rollback will be returned.

It's a common use case to connect to the database and execute just a single transaction. This use case is covered by withTransaction method provided by a ConnectionFactory. Above snippet could be simplified as follows:

import io.rdbc.sapi._

val cf: ConnectionFactory = ???

val result = cf.withTransaction { conn =>
    /* use the connection in a context of the transaction */
    ???
}

Validation

Sometimes, it may be useful to verify whether already open connection is "valid", i.e. whether it can still execute queries. A connection may get broken because of a number of reasons including network-related problems. To check whether the connection is usable, use validate method.

Connection can also be validated by executing any query but using validate is a preferred way since it can leverage a vendor-specific way of validating the connection with a minimal overhead.

Concurrent operations

As every other rdbc class, Connection is thread-safe. However, it doesn't mean that multiple threads can execute queries using the same connection at the same time. Only one operation can be executed at any given time. When some thread tries to use a non-idle connection, resulting Future fails with IllegalSessionStateException. Exception to this rule is forceRelease method which can be used regardless of whether connection is idle or not.

Example below demonstrates an invalid code which can fail with IllegalSessionStateException:

1
2
3
4
5
6
7
8
import io.rdbc.sapi._

val cf: ConnectionFactory = ???

cf.withConnection { conn =>
    conn.validate().foreach(valid => println(s"1. valid = $valid"))
    conn.validate().foreach(valid => println(s"2. valid = $valid"))
}

Future returned by statement at line 7 may fail because connection may still be busy executing a request from line 6. You can be sure though that the connection will start executing the first request (line 6) before the second (line 7) — there is a happens-before relation there.

To make the above code safe, it could be rewritten like this:

import io.rdbc.sapi._

val cf: ConnectionFactory = ???

cf.withConnection { conn =>
    conn.validate().foreach(valid => println(s"1. valid = $valid"))
    .andThen { _ =>
       conn.validate().foreach(valid => println(s"2. valid = $valid"))
    }
}

Connection also provides watchForIdle method returning future which completes when connection becomes idle. Snippet below is safe from failing with IllegalSessionStateException.

import io.rdbc.sapi._

val cf: ConnectionFactory = ???

cf.withConnection { conn =>
    conn.validate().foreach(valid => println(s"1. valid = $valid"))
    conn.watchForIdle.andThen { _ =>
        conn.validate().foreach(valid => println(s"2. valid = $valid"))
    }
}

Connection pooling

Estabilishing a new connection every time an interaction with the database is needed is expensive performance-wise. There usually is an overhead of initializing new session. In order to avoid this problem you can use connection pooling.

Connection pooling mechanism isn't really part of the API — to use pooling you just need to use a special ConnectionFactory implementation.

Have a look at rdbc-pool project that provides a ConnectionFactory implementation capable of connection pooling.