MySQL
Finch gebruikt het mysql_client_plus-pakket voor MySQL. De verbinding wordt automatisch tot stand gebracht door FinchApp bij het opstarten, op basis van je FinchMysqlConfig-instellingen, en wordt blootgesteld als een DatabaseDriver<MySQLConnectionPool> via app.mysqlDriver.
De SQL-integratie van Finch (gedeeld tussen MySQL en SQLite) bestaat uit drie lagen die samenwerken, allemaal afkomstig uit het sqler-pakket:
MTable/MField*— Dart-klassen die je databaseschema beschrijven. Ze genererenCREATE TABLE-SQL voor migraties en dienen daarnaast als formuliervalidators per veld.Sqler— Een fluent querybouwer die op een veilige manier geparametriseerde SQL samenstelt.DatabaseDriver— Eén enkele connection-wrapperklasse die een opgebouwde query uitvoert tegen MySQL of SQLite (het onderzoekt het onderliggende verbindingstype tijdens runtime) en eenSqlDatabaseResultretourneert.
Configuratie
Voeg mysqlConfig toe aan FinchConfigs in je app.dart. Waarden moeten afkomstig zijn uit omgevingsvariabelen:
FinchConfigs configs = FinchConfigs(
mysqlConfig: FinchMysqlConfig(
enable: true,
host: env.get('MYSQL_HOST', 'localhost'),
port: env.getInt('MYSQL_PORT', 3306),
user: env.get('MYSQL_USER', 'db_user'),
pass: env.get('MYSQL_PASS', 'db_password'),
databaseName: env.get('MYSQL_DATABASE', 'my_db'),
maxConnections: 10, // grootte van de MySQLConnectionPool
),
);
Let op:
FinchMysqlConfig.portis eenint(in tegenstelling totFinchDBConfig.portvoor MongoDB, dat eenStringis).
Toegang tot de driver
Zodra de app draait, kun je overal waar je toegang hebt tot de app-instantie bij de database driver:
var driver = app.mysqlDriver; // DatabaseDriver<MySQLConnectionPool>
// You can also check whether the connection is active
bool ok = app.mysqlDb.connected;
Geef driver door aan je datalaag-klassen in plaats van app.mysqlDriver rechtstreeks in controllers aan te roepen. Dit houdt controllers dun en je datalaag testbaar.
Een tabel definiëren (MTable)
MTable vertegenwoordigt een databasetabel in Dart. Je definieert hem één keer en hergebruikt hem voor:
- Migraties —
finch migrateleest jeMTable-definities omCREATE TABLE- enALTER TABLE-instructies te genereren. - Validatie — de
validatorsop elk veld worden gedeeld metAdvancedForm. - Query's opbouwen —
table.allSelectFields()en de hieronder beschreven hulpmethoden besparen je het steeds opnieuw opsommen van kolomlijsten.
Elke kolom wordt vertegenwoordigd door een MField*-klasse:
import 'package:finch/finch_mysql.dart';
import 'package:finch/finch_ui.dart'; // voor 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),
MFieldBoolean(name: 'is_published', defaultValue: 'FALSE'),
],
foreignKeys: [
ForeignKey(
name: 'category_id',
refTable: 'categories',
refColumn: 'id',
onDelete: 'SET NULL',
),
],
);
Available Field Types
MField* beschrijft het volledige scala aan MySQL-kolomtypen. De typen die je het vaakst zult gebruiken:
| Klasse | SQL-type | Opmerkingen |
|---|---|---|
MFieldInt |
INT | Primaire sleutel + auto-increment ondersteund |
MBigInt / MMediumInt / MSmallInt / MTinyInt |
BIGINT / MEDIUMINT / SMALLINT / TINYINT | Smallere/bredere integer-bereiken |
MFieldVarchar |
VARCHAR(n) | Standaard length is 255 |
MFieldChar |
CHAR(n) | String met vaste lengte |
MFieldText / MFieldTinyText / MFieldMediumText / MFieldLongText |
TEXT-varianten | Voor lange strings, ingedeeld op groottelimiet |
MFieldDate |
DATE | Opgeslagen als YYYY-MM-DD |
MFieldDateTime / MFieldTimestamp |
DATETIME / TIMESTAMP | Volledige timestamp; MFieldTimestamp wordt doorgaans gebruikt voor created_at/updated_at |
MFieldBoolean |
TINYINT(1) | Opgeslagen als 0/1 — niet MFieldBool |
MFieldFloat (m, optioneel d) |
FLOAT(m,d) | Benaderende drijvende-kommagetallen |
MFieldDecimal (m: 10, d: 2) |
DECIMAL(m,d) | Exact vast-kommagetal — gebruik dit voor geldbedragen in plaats van MFieldFloat |
MFieldEnum (values: [...]) |
ENUM(...) | Vaste verzameling stringwaarden |
MFieldJson |
JSON | Native JSON-kolom (MySQL 5.7+) |
MFieldBlob-familie, MFieldBinary/MFieldVarBinary |
BLOB / BINARY | Binaire gegevens |
MFieldBit, MFieldTime, MFieldYear, MFieldPoint, MFieldPolygon |
— | Minder gangbare typen, hetzelfde constructorpatroon |
Er bestaat geen
MFieldDouble— gebruikMFieldFloatvoor benaderende getallen ofMFieldDecimalvoor exacte getallen.
Foreign Keys
Geef ForeignKey-instanties door aan MTable.foreignKeys. Elke instantie genereert een ALTER TABLE ... ADD CONSTRAINT-instructie wanneer de tabel wordt gemigreerd:
ForeignKey(
name: 'category_id', // kolom in deze tabel
refTable: 'categories', // tabel waarnaar wordt verwezen
refColumn: 'id', // kolom waarnaar wordt verwezen (standaard: 'id')
onDelete: 'CASCADE', // 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION'
onUpdate: 'RESTRICT',
)
Queries met Sqler
Sqler genereert altijd geparametriseerde queries via QVar, dat waarden escapet en SQL-injectie voorkomt — je voegt nooit gebruikersinvoer rechtstreeks samen in een querystring.
Stel een query samen met de fluent API en geef deze vervolgens door aan driver.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', desc: true))
..limit(20);
return db.execute(query);
}
Invoegen
Sqler.insert() neemt de doeltabel en een lijst van rij-maps (zodat één enkele multi-row INSERT ... VALUES (...), (...) in één aanroep kan worden opgebouwd):
Future<void> insertBook(DatabaseDriver db, Map<String, QVar> data) async {
var query = Sqler().insert(QField(table.name), [data]);
await db.execute(query);
}
Bijwerken
Gebruik .update(table) om een tabel te targeten, roep vervolgens .updateSet(field, value) één keer per veld aan, en gebruik .where() om de rijen af te bakenen:
Future<void> updateBook(DatabaseDriver db, int id, Map<String, QVar> data) async {
var query = Sqler()..update(QField(table.name));
data.forEach((field, value) => query.updateSet(field, value));
query.where(WhereOne(QField('id'), QO.EQ, QVar(id)));
await db.execute(query);
}
Verwijderen
Combineer .delete() met .from(). Neem altijd een .where()-clausule op om te voorkomen dat alle rijen worden verwijderd:
Future<void> deleteBook(DatabaseDriver db, int id) async {
var query = Sqler()
..delete()
..from(QField(table.name))
..where(WhereOne(QField('id'), QO.EQ, QVar(id)));
await db.execute(query);
}
Table Convenience Methods
Elke MTable krijgt daarnaast een set kant-en-klare methoden (via een extension) waarmee je voor de meestvoorkomende gevallen geen handmatige Sqler-aanroepen hoeft te schrijven:
await table.existsTable(driver); // bool — bestaat de tabel?
await table.createTable(driver); // CREATE TABLE op basis van de MTable-definitie
await table.createForeignKeys(driver); // ALTER TABLE ... ADD CONSTRAINT voor elke ForeignKey
await table.dropTable(driver); // DROP TABLE IF EXISTS
await table.insert(driver, {'title': QVar('Dart in Action'), 'author': QVar('Alice')});
await table.insertMany(driver, [
{'title': QVar('Book A'), 'author': QVar('Alice')},
{'title': QVar('Book B'), 'author': QVar('Bob')},
]);
await table.select(driver, Sqler()..from(table.qName)..selects(table.allSelectFields()));
await table.delete(driver, Sqler()..delete()..from(table.qName)..where(WhereOne(QField('id'), QO.EQ, QVar(1))));
// Valideer een formulierinzending tegen de veldvalidators van deze tabel
var formResult = await table.formValidateUI({'title': 'x', 'author': 'Alice'});
table.qName is een snelkoppeling voor QField(table.name), en table.allSelectFields() retourneert QSelect-items voor elk gedefinieerd veld, zodat je kolommen niet handmatig hoeft op te sommen.
Repository-basisklasse (MysqlTable)
Voor een klein repositorypatroon extend je de abstracte klasse MysqlTable in plaats van in elke methode ruwe queries te schrijven. Deze implementeert al deleteBy, deleteById, findById en countBy; je hoeft alleen de abstracte findAll/updateFilters te implementeren:
class BooksRepository extends MysqlTable {
@override
DatabaseDriver get db => app.mysqlDriver;
@override
String get tableName => 'books';
@override
Sqler updateFilters(Sqler query, Map<String, dynamic> filter) {
if (filter['author'] != null) {
query.where(WhereOne(QField('author'), QO.EQ, QVar(filter['author'])));
}
return query;
}
@override
Future<({int count, SqlDatabaseResult rows})> findAll({
String orderBy = 'id',
bool orderReverse = true,
Map<String, dynamic> filters = const {},
int? pageSize,
int? offset,
}) async {
var query = Sqler()..from(qName)..selects(table.allSelectFields());
query = updateFilters(query, filters);
query.orderBy(QOrder(orderBy, desc: orderReverse));
if (pageSize != null) query.limit(pageSize, offset);
var count = await countBy(filters.isEmpty ? WhereOne(QField('id'), QO.GT, QVar(0)) : Where());
var rows = await db.execute(query);
return (count: count, rows: rows);
}
}
// Gebruik:
var books = BooksRepository();
await books.deleteById(3);
var found = await books.findById(1);
Result Handling
db.execute() retourneert een SqlDatabaseResult. Specifiek voor MySQL is .rows een lijst van ResultSetRow (uit mysql_client_plus), die .colByName(name) ondersteunt — let op: dit retourneert de ruwe waarde direct, en gooit een fout als de kolomnaam niet bestaat, dus gebruik het alleen voor kolommen waarvan je weet dat ze in de query voorkomen:
var result = await getAllBooks(app.mysqlDriver);
for (var row in result.rows) {
var title = row.colByName('title') ?? '';
var id = row.colByName('id') ?? 0;
}
De database-onafhankelijke oplossing — en de enige optie voor SQLite, zie SQLite — is .assoc/.assocFirst, die een gewone Map<String, dynamic> retourneren:
for (var row in result.assoc) {
print(row['title']);
}
var firstRow = result.assocFirst; // Map<String, dynamic>? — eerste rij of null
var numRows = result.numRows; // Aantal rijen in dit resultaat
var newId = result.insertId; // Auto-increment ID van de laatste INSERT
var affected = result.affectedRows; // Rijen geraakt door de laatste INSERT/UPDATE/DELETE
Voor een pagineringsquery in de stijl van een totaal-rijenaantal alias je je COUNT(*) als count_records en lees je deze terug met .countRecords:
var countQuery = Sqler()..from(table.qName)..addSelect(SQL.count(QField('id', as: 'count_records')));
var total = (await table.execute(driver, countQuery)).countRecords;
Migraties
Zie Database Migration voor het aanmaken en uitvoeren van migratiebestanden.