MySQL
Finch uses the mysql_client package for MySQL. The connection is established automatically by FinchApp at startup based on your FinchMysqlConfig settings, and is exposed as a DatabaseDriver<MySQLConnection> through app.mysqlDriver.
Finch's MySQL integration has three layers that work together:
MTable/MField*— Dart classes that describe your database schema. They serve double duty: they generate migration SQL and they define per-field validators for forms.Sqler— A fluent query builder that constructs parameterized SQL safely. See Sqler for the full API reference.DatabaseDriver— The connection wrapper that executes a built query and returns aSqlDatabaseResult.
Configuration
Add mysqlConfig to FinchConfigs in your app.dart. Values should come from environment variables:
FinchConfigs configs = FinchConfigs(
mysqlConfig: FinchMysqlConfig(
enable: true,
host: env['MYSQL_HOST'] ?? 'localhost',
port: (env['MYSQL_PORT'] ?? '3306').toInt(def: 3306),
user: env['MYSQL_USER'] ?? 'db_user',
pass: env['MYSQL_PASS'] ?? 'db_password',
databaseName: env['MYSQL_DATABASE'] ?? 'my_db',
),
);
Note:
FinchMysqlConfig.portis anint(unlikeFinchDBConfig.portfor MongoDB which is aString).
Accessing the Driver
Once the app is running you can access the database driver anywhere that has access to the app instance:
var driver = app.mysqlDriver; // DatabaseDriver<MySQLConnection>
// You can also check whether the connection is active
bool ok = app.mysqlDb.connected;
Pass driver into your data-layer classes rather than calling app.mysqlDriver in controllers. This keeps controllers thin and your data layer testable.
Defining a Table (MTable)
MTable represents a database table in Dart. You define it once and reuse it for:
- Migrations —
finch migratereads yourMTabledefinitions to generateCREATE TABLEandALTER TABLEstatements. - Validation — the
validatorson each field are shared withAdvancedForm.
Each column is represented by an MField* class. Example:
import 'package:finch/finch_mysql.dart';
import 'package:finch/finch_ui.dart'; // for FieldValidator
final table = MTable(
name: 'books',
fields: [
MFieldInt(
name: 'id',
isPrimaryKey: true,
isAutoIncrement: true,
isNullable: false,
),
MFieldVarchar(
name: 'title',
length: 255,
isNullable: false,
comment: 'Title of the book',
validators: [
FieldValidator.requiredField().toSimple(),
FieldValidator.fieldLength(min: 3, max: 255).toSimple(),
],
),
MFieldVarchar(
name: 'author',
length: 255,
isNullable: false,
),
MFieldDate(
name: 'published_date',
isNullable: false,
),
MFieldInt(
name: 'category_id',
isNullable: true,
),
],
);
Available Field Types
| Class | SQL Type | Notes |
|---|---|---|
MFieldInt |
INT | Primary key + auto-increment supported |
MFieldVarchar |
VARCHAR(n) | Default length is 255 |
MFieldText |
TEXT | For long strings |
MFieldDate |
DATE | Stored as YYYY-MM-DD |
MFieldDateTime |
DATETIME | Full timestamp |
MFieldBool |
TINYINT(1) | Stored as 0/1 |
MFieldDouble |
DOUBLE | Floating-point numbers |
Querying with Sqler
Finch uses sqler for building SQL queries. Sqler always generates parameterized queries, which prevents SQL injection. You never concatenate user input into a query string.
Construct a query using the fluent API, then pass it to db.execute(query):
import 'package:finch/finch_mysql.dart';
Future<SqlDatabaseResult> getAllBooks(DatabaseDriver db) async {
var query = Sqler()
..from(QField(table.name, as: 'b'))
..selects([
QSelect('b.id'),
QSelect('b.title'),
QSelect('b.author'),
QSelect('b.published_date'),
])
..orderBy(QOrder('b.id', descending: true))
..limit(20);
return db.execute(query);
}
Insert
Insert uses insertInto with QField column names and QVar values. QVar ensures values are properly escaped:
Future<void> insertBook(DatabaseDriver db, Map<String, dynamic> data) async {
var query = Sqler().insertInto(
QField(table.name),
data.map((k, v) => MapEntry(QField(k), QVar(v))),
);
await db.execute(query);
}
Update
Use .update() with .sets() and .where() to update specific rows:
Future<void> updateBook(DatabaseDriver db, int id, Map<String, dynamic> data) async {
var query = Sqler()
..update(QField(table.name))
..sets(data.map((k, v) => MapEntry(QField(k), QVar(v))))
..where(WhereOne(QField('id'), QO.EQ, QVar(id)));
await db.execute(query);
}
Delete
Always include a .where() clause on delete to avoid deleting all rows:
Future<void> deleteBook(DatabaseDriver db, int id) async {
var query = Sqler()
..deleteFrom(QField(table.name))
..where(WhereOne(QField('id'), QO.EQ, QVar(id)));
await db.execute(query);
}
Result Handling
db.execute() returns a SqlDatabaseResult. Iterate over rows with .rows, and access columns by name using .colByName():
var result = await getAllBooks(app.mysqlDriver);
for (var row in result.rows) {
var title = row.colByName('title')?.value ?? '';
var id = row.colByName('id')?.value ?? 0;
}
// Total number of matching rows (useful for pagination)
var total = result.count;
Migrations
See Database Migration for creating and running migration files.
Configuration
FinchConfigs configs = FinchConfigs(
mysqlConfig: FinchMysqlConfig(
enable: true,
host: env['MYSQL_HOST'] ?? 'localhost',
port: (env['MYSQL_PORT'] ?? '3306').toInt(def: 3306),
user: env['MYSQL_USER'] ?? 'db_user',
pass: env['MYSQL_PASS'] ?? 'db_password',
databaseName: env['MYSQL_DATABASE'] ?? 'my_db',
),
);
Accessing the Driver
var driver = app.mysqlDriver; // DatabaseDriver<MySQLConnection>
// Check if connected
bool ok = app.mysqlDb.connected;
Defining a Table (MTable)
Use MTable and MField* classes to describe your schema. This same definition is used for migrations and query building:
import 'package:finch/finch_mysql.dart';
import 'package:finch/finch_ui.dart'; // for FieldValidator
final table = MTable(
name: 'books',
fields: [
MFieldInt(
name: 'id',
isPrimaryKey: true,
isAutoIncrement: true,
isNullable: false,
),
MFieldVarchar(
name: 'title',
length: 255,
isNullable: false,
comment: 'Title of the book',
validators: [
FieldValidator.requiredField().toSimple(),
FieldValidator.fieldLength(min: 3, max: 255).toSimple(),
],
),
MFieldVarchar(
name: 'author',
length: 255,
isNullable: false,
),
MFieldDate(
name: 'published_date',
isNullable: false,
),
MFieldInt(
name: 'category_id',
isNullable: true,
),
],
);
Available Field Types
| Class | SQL Type |
|---|---|
MFieldInt |
INT |
MFieldVarchar |
VARCHAR(n) |
MFieldText |
TEXT |
MFieldDate |
DATE |
MFieldDateTime |
DATETIME |
MFieldBool |
TINYINT(1) |
MFieldDouble |
DOUBLE |
Querying with Sqler
Finch uses sqler for building SQL queries. Use Sqler() to construct a query then execute it with driver.execute():
import 'package:finch/finch_mysql.dart';
Future<SqlDatabaseResult> getAllBooks(DatabaseDriver db) async {
var query = Sqler()
..from(QField(table.name, as: 'b'))
..selects([
QSelect('b.id'),
QSelect('b.title'),
QSelect('b.author'),
QSelect('b.published_date'),
])
..orderBy(QOrder('b.id', descending: true))
..limit(20);
return db.execute(query);
}
Insert
Future<void> insertBook(DatabaseDriver db, Map<String, dynamic> data) async {
var query = Sqler().insertInto(
QField(table.name),
data.map((k, v) => MapEntry(QField(k), QVar(v))),
);
await db.execute(query);
}
Update
Future<void> updateBook(DatabaseDriver db, int id, Map<String, dynamic> data) async {
var query = Sqler()
..update(QField(table.name))
..sets(data.map((k, v) => MapEntry(QField(k), QVar(v))))
..where(WhereOne(QField('id'), QO.EQ, QVar(id)));
await db.execute(query);
}
Delete
Future<void> deleteBook(DatabaseDriver db, int id) async {
var query = Sqler()
..deleteFrom(QField(table.name))
..where(WhereOne(QField('id'), QO.EQ, QVar(id)));
await db.execute(query);
}
Result Handling
SqlDatabaseResult provides typed row access:
var result = await getAllBooks(app.mysqlDriver);
for (var row in result.rows) {
var title = row.colByName('title')?.value ?? '';
var id = row.colByName('id')?.value ?? 0;
}
// Total count (for pagination)
var total = result.count;
Migrations
See Database Migration for creating and running migration files.