Skip to content

Multiple Connections

Oxide can manage any number of database connections side-by-side. Declare them once under [connections.*] in config/database.toml, then resolve them with the DB facade — no passing handles through every function signature.

Declare the connections

config/database.toml
default = "${DB_CONNECTION:-mysql}"
[connections.mysql]
driver = "mysql"
host = "${DB_HOST:-127.0.0.1}"
port = "${DB_PORT:-3306}"
database = "${DB_DATABASE:-app}"
username = "${DB_USERNAME:-root}"
password = "${DB_PASSWORD:-}"
[connections.mysql2]
driver = "mysql"
host = "${DB2_HOST:-10.0.0.12}"
port = "${DB2_PORT:-3306}"
database = "${DB2_DATABASE:-reports}"
username = "${DB2_USERNAME:-reporter}"
password = "${DB2_PASSWORD:-}"
[connections.sqlite]
driver = "sqlite"
database = "storage/cache.sqlite"

default picks which one the rest of the framework uses by default — Model::get(), conn(), the migrator, oxide migrate, and so on.

Resolve a connection

use oxide::db::DB;
// Default connection (same as `oxide::db::conn()`).
let default = DB::conn();
// Named connection — built on first access, cached thereafter.
let reports = DB::connection("mysql2");
// Raw SQL against the named connection
reports
.conn()
.execute_unprepared("INSERT INTO events (kind) VALUES ('sync')")
.await?;
// SeaORM entity query against the named connection
use sea_orm::EntityTrait;
let rows = user::Entity::find().all(reports.conn()).await?;

DB::connection(name) returns a Database by value. The handle is cheaply cloneable — the underlying sqlx pool is already Arc-shared — so pass it around freely.

Laziness

Connections are built on first access, not at boot. That way a broken staging-only connection won’t kill local development. The sqlx pool itself is also lazy: no TCP handshake happens until the first query runs.

Consequence: a typo in the connection name won’t surface until something actually calls DB::connection("tpyo"). The panic message names the missing block so it’s easy to spot.

Use inside a Model query

The Model facade works against any connection via on(name):

use crate::app::models::Event;
use oxide::db::Model;
// Default connection — unchanged
let all = Event::get().await?;
let one = Event::find_id(1).await?;
let first = Event::first().await?;
// Named connection — same three methods, scoped to mysql2
let all = Event::on("mysql2").get().await?;
let one = Event::on("mysql2").find_id(1).await?;
let first = Event::on("mysql2").first().await?;

on(name) returns a Scope<Entity> holding the resolved Database. For queries beyond get/find_id/first, grab the connection and drop into SeaORM’s full builder:

use oxide::db::Model;
use sea_orm::{EntityTrait, QueryFilter, ColumnTrait};
let scope = Event::on("mysql2");
let open = Event::find()
.filter(event::Column::Status.eq("open"))
.all(scope.conn())
.await?;

Transactions

Transactions live on the connection, not the facade — same pattern:

use sea_orm::TransactionTrait;
let txn = DB::connection("mysql2").conn().begin().await?;
// ... writes using `&txn` ...
txn.commit().await?;

Introspection

let names = DB::connection_names(); // Vec<String>
let default = DB::default_name(); // &str
let backend = DB::connection("mysql2").backend(); // DbBackend::MySql

Useful for diagnostic commands or admin tooling.

What’s the default used for?

Unless a caller picks a specific connection, these all talk to the default:

  • oxide::db::conn()
  • DB::conn() / DB::default_connection()
  • Model::get(), Model::find_id(n), Model::first()
  • Every oxide migrate / oxide migrate:fresh invocation
  • Every entity/command that doesn’t explicitly pass a different db.conn()

Change database.default (usually via DB_CONNECTION) to redirect all of the above at once.