Database
DB_CONNECTION selects SQLite (sqlite, the default), PostgreSQL (pgsql),
MySQL (mysql) or MariaDB (mariadb). MySQL and MariaDB come from
package:daravel_mysql, which new projects register in bootstrap/app.dart:
Application.configure(basePath: '.')
.withDatabaseDriver('mysql', MySqlDriver.fromConfig)
.withDatabaseDriver('mariadb', MySqlDriver.fromConfig)
SQLite and PostgreSQL are built in through Daravel 1.x. See Drivers and connections for the driver packages and for using several databases at once.
On MySQL, results read as in Laravel (TINYINT(1) as 0/1, DECIMAL and
JSON as strings, dates as Y-m-d H:i:s), and the session time zone is the
server's unless DB_TIMEZONE is set, so TIMESTAMP columns read the same
from both frameworks. MySQL 8.4 and MariaDB 11 run in CI on every commit.
Every query is built by a grammar with bound values; identifiers are quoted.
Drivers and connections#
Each database client is a package. MySQL and MariaDB already work this way.
For SQLite (daravel_sqlite) and PostgreSQL (daravel_postgres), the
packages re-export the drivers that are still built into 1.x, so adding one
changes nothing today. In 2.0, package:daravel stops depending on any
database client, and the drivers live only in these packages. A project
that already depends on its driver package then upgrades without changes,
and no longer builds or ships the clients it does not use.
dartisan db:install adds a driver package, registers it and documents its
settings:
dart run bin/dartisan.dart db:install pgsql # or sqlite, mysql, mariadb
| It changes | With |
|---|---|
pubspec.yaml |
daravel_postgres: ^<version> (then dart pub get; --no-get skips it) |
bootstrap/app.dart |
the import and .withPostgres() (.withSqlite(), or the MySQL withDatabaseDriver lines) after Application.configure(...) |
.env.example |
a commented block with the driver's DB_* keys |
Running it again changes nothing.
Switching the default database#
The models, queries and migrations do not depend on the driver. To move an application from SQLite to PostgreSQL:
dartisan db:install pgsql.- In
.env:DB_CONNECTION=pgsqlandDB_HOST,DB_PORT,DB_DATABASE,DB_USERNAME,DB_PASSWORD. dartisan migrate, or copy the existing data across withdb:transfer(below), which migrates the new database first.
Several databases at once#
The DB_* keys configure one database. Add others with their own settings
in bootstrap/app.dart, for example from a URL in Laravel's DB_URL
form:
Application.configure(basePath: basePath ?? Directory.current.path)
.withDatabaseConnection(
'legacy',
(env) => ConnectionConfig.fromUrl(env.string('LEGACY_DB_URL')),
)
LEGACY_DB_URL=mysql://shop:secret@10.0.0.5:3306/shop
ConnectionConfig.fromUrl reads pgsql://, mysql://, mariadb:// and
sqlite: URLs, and the query string sets sslmode, pool, charset and
timezone. A connection whose driver is MySQL or MariaDB still needs that
driver registered (db:install mysql).
Queries and models pick a connection by name:
final orders = await DB.table('orders', connection: 'legacy').get();
final class LegacyOrder extends Model {
static final ModelDefinition<LegacyOrder> definition =
ModelDefinition<LegacyOrder>(
table: 'orders',
connection: 'legacy',
factory: LegacyOrder.new,
);
@override
ModelDefinition<LegacyOrder> get modelDefinition => LegacyOrder.definition;
}
Transactions are per connection, as in Laravel. Queries on another
connection inside DB.transaction(...) do not join it, so they commit even
when it rolls back. DB.transaction(..., connection: 'legacy') starts a
transaction of its own on that connection.
Copying data between databases#
dartisan db:transfer copies an application's data to another database,
for example from the SQLite file used in development to PostgreSQL:
dart run bin/dartisan.dart db:transfer --from=sqlite --to=pgsql://app:secret@db.internal:5432/app
dart run bin/dartisan.dart db:transfer --from=sqlite --to=pgsql --dry-run # counts, changes nothing
--from and --to take a connection name (sqlite, pgsql, a
withDatabaseConnection name) or a URL. The DB_* keys configure every
built-in connection at once, so a URL is how one .env reaches two
databases.
- Schema. The target is migrated with the application's migrations
first (
--no-migratewhen it already is). Each database gets its own column types rather than a translation of the source's DDL. Tables that no migration creates are listed and skipped, and themigrationsledger is never copied. - Order and atomicity. Tables are copied parents first, following the target's foreign keys, in one transaction on the target. A failure leaves the target as it was. On PostgreSQL, foreign keys are deferred during the copy and checked once at the end, so rows that reference later rows (a category whose parent comes after it) or tables that reference each other copy too. Their definitions are restored before the commit.
- Values.
0/1become booleans on PostgreSQL, JSON documents from PostgreSQL become text, and dates become Laravel'sY-m-d H:i:son SQLite and MySQL. - Ids. Rows keep their primary keys. PostgreSQL sequences continue after the highest copied id, so the next insert does not collide.
- Safety. A target table that already has rows stops the transfer
unless
--truncatereplaces them. WithAPP_ENV=productionit needs--force. Rows are read in chunks of--chunk(500), paged by primary key.--tables=users,postslimits the copy.
Migrations#
final class CreatePostsTable extends Migration {
const CreatePostsTable();
@override
String get name => '2026_01_01_000000_create_posts_table';
@override
Future<void> up(SchemaBuilder schema) => schema.create('posts', (table) {
table.id();
table.string('title');
table.text('body').nullable();
table.boolean('published').defaultTo(false);
table.timestamps();
});
@override
Future<void> down(SchemaBuilder schema) => schema.dropIfExists('posts');
}
Migrations are listed in bootstrap/migrations.dart and recorded in
Laravel's migrations table (id, migration, batch), so a Laravel app
and a Daravel app can share one ledger. Commands: migrate,
migrate:status, migrate:rollback, migrate:reset, migrate:fresh,
db:seed. On SQLite, column changes rebuild the table safely.
Models#
Models are "hybrid active records": an attribute bag plus a static
ModelDefinition that replaces Laravel's magic properties.
final class Post extends Model {
static final ModelDefinition<Post> definition = ModelDefinition<Post>(
table: 'posts',
factory: Post.new,
fillable: ['title', 'body', 'published'],
casts: {'published': Cast.boolean},
scopes: {'published': (query, args) => query.where('published', true)},
);
@override
ModelDefinition<Post> get modelDefinition => Post.definition;
static ModelQuery<Post> query() => definition.query();
int get id => getAttr('id');
String get title => getAttr('title');
bool get published => getAttr('published');
}
dartisan make:model writes this for you and dartisan model:sync
regenerates the typed accessors from the table. Relations (HasOne,
HasMany, BelongsTo, BelongsToMany, HasManyThrough and the
polymorphic ones) are declared in
relations: and eager loaded with with_([...]) (with is a reserved
word in Dart). Soft deletes, observers, model events, global scopes,
withCount and (nested) whereHas work as in Eloquent.
ModelDefinition.morphClass is the Laravel class name of the model
(App\Models\Post by default). It is stored in polymorphic columns such as
personal_access_tokens.tokenable_type, unless Relation.morphMap gives
the model an alias, and used in
No query results for model [App\Models\Post] 1.
Querying#
await Post.query().create({'title': 'Hello', 'published': true});
await DB.table('posts').insert({'title': 'Draft'});
final published = await Post.query()
.where('published', true)
.orderByDesc('id')
.get();
final titles = await DB.table('posts').orderBy('id').pluck('title');
final count = await Post.query().scope('published').count();
await DB.transaction(() async {
final post = await Post.query().findOrFail(1);
await post.update({'title': 'Hello, Daravel'});
});
Transactions follow the zone: nested calls become savepoints, and
after-commit callbacks (App.make<DatabaseManager>().afterCommit(...), used
by queued jobs and Event.dispatchAfterCommit) run only when the outer
transaction commits.
Dart can run several things at once inside one transaction, which PHP cannot, so two rules keep it safe:
- Nested transactions started together (
Future.wait([DB.transaction(...), DB.transaction(...)])) run one after the other, because savepoints on a connection form a stack. One failing rolls back only its own work. - A query that runs after its transaction has finished (a missing
await) throws aStateErrorinstead of running outside the transaction.awaitevery query insideDB.transaction;dartisan lint(unawaited_in_db_transaction) points out the ones that are not, includingunawaited(...)and.ignore().
Listening to queries#
DB.listen((QueryExecuted query) { ... }) is called after every query of
the builder, the ORM, raw SQL and migrations, with its SQL, bindings,
duration and connection, for example to log slow queries:
DB.listen((query) {
if (query.time > const Duration(milliseconds: 100)) {
Log.warning('Slow query', {'sql': query.sql, 'ms': query.time.inMilliseconds});
}
});
Without listeners, queries are not wrapped and cost nothing extra.
For a time budget rather than single slow queries, Laravel's
whenQueryingForLongerThan adds up the queries of each request, job or
command and calls the handler once when the total passes the threshold:
DB.whenQueryingForLongerThan(const Duration(milliseconds: 500), (query) {
Log.warning('Request spent over 500 ms in queries',
{'total_ms': DB.totalQueryDuration.inMilliseconds});
});
DB.withQueryCount runs a callback and returns its result with the queries
it ran, which turns an N+1 into a failing test:
final (users, count, queries) = await DB.withQueryCount(
() => User.query().with_('posts').get(),
);
expect(count, 2); // users, then every post in one query
Dates in queries#
whereDate, whereYear, whereMonth and whereDay compare a part of a
date column. Laravel 11's shortcuts compare with the current time, read from
Clock (so freezeTime() applies), and take a column or a list:
| Method | Condition |
|---|---|
wherePast / whereNowOrPast |
before / at or before now |
whereFuture / whereNowOrFuture |
after / at or after now |
whereToday |
the date is today |
whereBeforeToday / whereTodayOrBefore |
the date is before / at most today |
whereAfterToday / whereTodayOrAfter |
the date is after / at least today |
Each has an orWhere… form, on DB.table(...) and on model queries.
Pagination#
paginate(), simplePaginate() and cursorPaginate() read the page (or
cursor) and the absolute path from the current request, like Laravel, and
serialize to Laravel's exact JSON: the same keys in the same order, links
with Laravel's sliding window and page numbers, current_page_url for
simple pagination, and cursors encoded like Laravel's Cursor::encode() so
a cursor issued by either framework works in the other. Use
withQueryString() or appends({...}) to keep extra query parameters.