Skip to content

Using Database Kit

Database Kit is a framework for configuring and working with database connections. It helps you do things like manage and pool connections, create keyed caches, and log queries.

Many of Vapor's packages such as the Fluent drivers, Redis, and Vapor core are built on top of Database Kit. This guide will walk you through some of the common APIs you might encounter when using Database Kit.

Config

Your first interaction with Database Kit will most likely be with the DatabasesConfig struct. This type helps you configure one or more databases to your application and will ultimately yield a Databases struct. This usually takes place in configure.swift.

// Create a SQLite database.
let sqliteDB = SQLiteDatabase(...)

// Create a new, empty DatabasesConfig.
var dbsConfig = DatabasesConfig()

// Register the SQLite database using '.sqlite' as an identifier.
dbsConfig.add(sqliteDB, as: .sqlite)

// Register more DBs here if you want

// Register the DatabaseConfig to services.
services.register(dbsConfig)

Using the add(...) methods, you can register Databases to the config. You can register instances of a database, a database type, or a closure that creates a database. The latter two methods will be resolved when your container boots.

You can also configure options on your databases, such as enabling logging.

// Enable logging on the SQLite database
dbsConfig.enableLogging(for: .sqlite)

See the section on logging for more information.

Identifier

Most database integrations will provide a default DatabaseIdentifier to use. However, you can always create your own. This is usually done by creating a static extension.

extension DatabaseIdentifier {
    /// Test database.
    static var testing: DatabaseIdentifier<MySQLDatabase> {
        return "testing"
    }
}

DatabaseIdentifier is ExpressibleByStringLiteral which allows you to create one with just a String.

Databases

Once you have registered a DatabasesConfig to your services and booted a container, you can take advantage of the convenience extensions on Container to start creating connections.

// Creates a new connection to `.sqlite` db
app.withNewConnection(to: .sqlite) { conn in
    return conn.query(...) // do some db query
}

Read more about creating and managing connections in the next section.

Connections

Database Kit's main focus is on creating, managing, and pooling connections. Creating new connections takes a non-trivial amount of time for your application and many cloud services limit the total number of connections to a service that can be open. Because of this, it is important for high-concurrency web applications to manage their connections carefully.

Pools

A common solution to connection management is the use of connection pools. These pools usually have a set maximum number of connections that are allowed to be open at once. Each time the pool is asked for a connection, it will first check if one is available before creating a new connection. If none are available, it will create a new one. If no connections are available and the pool is already at its maximum, the request for a new connection will wait for a connection to be returned.

The easiest way to request and release a pooled connection is the method withPooledConnection(...).

// Requests a pooled connection to `.psql` db
req.withPooledConnection(to: .psql) { conn in
    return conn.query(...) // do some db query
}

This method will request a pooled connection to the identified database and call the provided closure when the connection is available. When the Future returned by the closure has completed, the connection will automatically be returned to the pool.

If you need access to a connection outside of a closure, you can use the related request / release methods instead.

// Request a connection from the pool and wait for it to be ready.
let conn = try app.requestPooledConnection(to: .psql).wait()

// Ensure the connection is released when we exit this scope.
defer { app.releasePooledConnection(conn, to: .psql) }

You can configure your connection pools using the DatabaseConnectionPoolConfig struct.

// Create a new, empty pool config.
var poolConfig = DatabaseConnectionPoolConfig()

// Set max connections per pool to 8.
poolConfig.maxConnections = 8

// Register the pool config.
services.register(poolConfig)

To prevent race conditions, pools are never shared between event loops. There is usually one pool per database per event loop. This means that the amount of connections your application can potentially open to a given database is equal to numThreads * maxConns.

New

You can always create a new connection to your databases if you need to. This will not affect your pooled connections. Creating new connections is especially useful during testing and app boot. But try not to do it in route closures since heavy traffic to your app could end up creating a lot of connections!

Similar to pooled connections, opening and closing new connections can be done using withNewConnection(...).

// Creates a new connection to `.sqlite` db
app.withNewConnection(to: .sqlite) { conn in
    return conn.query(...) // do some db query
}

This method will create a new connection, calling the supplied closure when the connection is open. When the Future returned in the closure completes, the connection will be closed automatically.

You can also simply open a new connection with newConnection(...).

// Creates a new connection to `.sqlite` db
let conn = try app.newConnection(to: .sqlite).wait()

// Ensure the connection is closed when we exit this scope.
defer { conn.close() }

Logging

Databases can opt into supporting query logging via the LogSupporting protocol. Databases that conform to this protocol can have loggers configured via DatabasesConfig.

// Enable logging on the SQLite database
dbsConfig.enableLogging(for: .sqlite)

By default, a simple print logger will be used, but you can pass a custom DatabaseLogHandler.

// Create a custom log handler.
let myLogger: DatabaseLogHandler = ...

// Enable logging on SQLite w/ custom logger.
dbsConfig.enableLogging(for: .sqlite, logger: myLogger)

Log handlers will receive an instance of DatabaseLog for each logged query. This contains information such as the query, parameterized values, database id, and time.

Keyed Cache

Databases can opt into supporting keyed-caching via the KeyedCacheSupporting protocol. Databases that conform to this protocol can be used to create instances of DatabaseKeyedCache.

Keyed caches are capable of getting, setting, and removing Codable values at keys. They are sometimes called "key value stores".

To create a keyed cache, you can use the extensions on Container.

// Creates a DatabaseKeyedCache with .redis connection pool
let cache = try app.keyedCache(for: .redis)

// Sets "hello" = "world"
try cache.set("hello", to: "world").wait()

// Gets "hello"
let world = try cache.get("hello", as: String.self).wait()
print(world) // "world"

// Removes "hello"
try cache.remove("hello").wait()

See the KeyedCache protocol for more information.

API Docs

Check out the API docs for more in-depth information about DatabaseKit's APIs.

Comments