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 Database
s 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.