- name
- sembast-crud
- description
- Use when writing Dart or Flutter code that stores, reads, updates or deletes records with package:sembast: StoreRef, intMapStoreFactory, stringMapStoreFactory, RecordRef, RecordsRef, add/put/update/get/delete, merge and FieldValue.delete, dotted field paths, db.transaction and the Transaction/DatabaseClient parameter, supported value types (Timestamp, Blob, immutable read values, cloneMap) and store change triggers (addOnChangesListener). Opening databases and queries have their own skills.
# sembast: stores, records, writes and transactions
`package:sembast` is a NoSQL document database (pure Dart, VM, Flutter and,
with `sembast_web`, the web). Data lives in typed stores of records. A
`StoreRef<K, V>` and a `RecordRef<K, V>` are immutable pointers, they hold no
data; every data method takes a `DatabaseClient` (the `Database` or a
`Transaction`) as first argument.
```dart
import 'package:sembast/sembast.dart';
// Declare stores once, globally. Keys are int, values are maps.
final productStore = intMapStoreFactory.store('product');
Future<void> demo(Database db) async {
var key = await productStore.add(db, {'name': 'Lamp', 'price': 10});
await productStore.record(key).update(db, {'price': 12});
var value = await productStore.record(key).get(db);
print(value); // {name: Lamp, price: 12}
await productStore.record(key).delete(db);
}
```
## Guidelines
### Stores and references
* Import `package:sembast/sembast.dart`. It exports `StoreRef`, `RecordRef`,
`RecordsRef`, `Database`, `Transaction`, `DatabaseClient`, `Field`,
`FieldValue`, `FieldKey` and the extension methods (`add`, `put`, `get`...)
on the refs. `sembast_io.dart` and `sembast_memory.dart` re-export it.
* Use `intMapStoreFactory.store('name')` for
`StoreRef<int, Map<String, Object?>>` (auto-increment int keys) and
`stringMapStoreFactory.store('name')` for
`StoreRef<String, Map<String, Object?>>` (generated unique string keys).
Omit the name (`intMapStoreFactory.store()`) for the main store.
* For other value types use `StoreRef<K, V>('name')` or
`StoreRef<K, V>.main()`, for example `StoreRef<String, String>.main()` for
key/value settings. `K` must be `int` or `String`, nothing else.
* Store names must not start with `_`. A store exists only while it has
records; there is no create/delete store call. `store.drop(db)` deletes all
its records, `db.dropAll()` (extension on `DatabaseClient`) clears the whole
database.
* Declare stores and fixed records as top-level `final` variables and reuse
them; `store.record(key)` and `store.records(keys)` are cheap.
* `RecordsRef` (from `store.records([k1, k2])` or
`store.recordsFromRefs(refs)`) batches operations on known keys: `get`,
`getSnapshots`, `add`, `put`, `update`, `delete`, `onSnapshots`, plus
`length`, `refs` and `[index]`.
### Writing
* `store.add(db, value)` inserts with a generated key and returns it.
`store.addAll(db, values)` returns the list of keys in one transaction.
`record.add(db, value)` inserts with the record's key and returns `null`
when the record already exists (nothing is written).
* `record.put(db, value)` creates or replaces (upsert) and returns the stored
value. `put(..., merge: true)` merges a map into the existing map.
`put(..., ifNotExists: true)` writes only when absent and returns the
existing value otherwise. `merge` and `ifNotExists` cannot both be true.
* `record.update(db, value)` merges into an existing record and returns the
new value, or `null` when the record does not exist (nothing is created).
`store.update(db, value, finder: finder)` merges the same value into every
matching record and returns the count (all records when `finder` is null).
* In `update` (and `put(merge: true)`) map keys are paths: `'address.city'`
targets the nested `city` field. Set a field to `FieldValue.delete` to
remove it. Use `FieldKey.escape('with.dots')` for a literal key containing
a dot. `add` and plain `put` store keys verbatim, no path interpretation.
* `record.delete(db)` returns the key or `null` if absent.
`store.delete(db, finder: finder)` deletes matching records and returns the
count; without a finder it clears the store. `records.delete(db)` deletes a
batch.
* Need a key before inserting? `store.generateKey(db)` (typed `K`) or
`store.generateIntKey(db)`.
### Values
* Supported value types: `String`, `int`, `double`, `bool`, `null` (inside
maps and lists only), `Map<String, Object?>`, `List<Object?>`, `Timestamp`
(`package:sembast/timestamp.dart`) and `Blob`
(`package:sembast/blob.dart`). Anything else (`DateTime`, `Uint8List`,
`Iterable`, enums, custom classes) throws `ArgumentError` on write. Convert
with `Timestamp.fromDateTime(dt)` / `timestamp.toDateTime()`,
`Blob(bytes)` / `blob.bytes`, `iterable.toList()`, `myEnum.name`.
* A record's root value cannot be `null`; `put(db, null)` throws.
* Maps are cast to `Map<String, Object?>` and lists to `List<Object?>` on
write. Typed maps such as `Map<String, int>` work; `Map<int, ...>` does not.
* Values returned by `get`, `find`, snapshots and streams are read-only:
writing to them throws `StateError('read only')`. Clone before mutating:
`cloneMap(value)`, `cloneList(value)`, `cloneValue(value)` from
`package:sembast/utils/value_utils.dart`.
* Keep records small (a few KB). Store big binaries in files and keep a
reference in the record.
### Transactions
* Group more than one write in `db.transaction((txn) async { ... })`. It is
atomic (an exception rolls back every change and is rethrown) and much
faster than separate writes: each standalone write is its own transaction.
* Inside the callback use only `txn`. Calling any method with `db` inside
a transaction deadlocks (the call waits for the transaction that waits for
it). Set `debugSembastWarnDatabaseCallInTransaction = true` during
development to get a printed diagnosis after 10 s.
* The callback may return a value: `var key = await db.transaction((txn)
=> store.add(txn, value));`.
* Write helpers that take a `DatabaseClient client` parameter so they work
with both `db` and `txn`.
* Make transaction bodies idempotent and side-effect free: on `sembast_web`
and `sembast_sqflite` a transaction is re-run when another tab or process
wrote concurrently.
* Reads inside a transaction see the transaction's pending changes; other
readers see them after commit. Reads never need a transaction.
* Batch size: around 100-1000 writes per transaction; split larger imports.
* Do not open a nested `db.transaction` from inside a transaction (also a
deadlock). The only exception is `onVersionChanged` during
`openDatabase`, where `db` calls join the open transaction.
### Change triggers (in-transaction listeners)
* `store.addOnChangesListener(db, (txn, changes) async { ... })` runs inside
the transaction that modified the store, before it commits. Use it for
cascading deletes or derived data; use the `txn` argument for writes.
* Each `RecordChange` has `oldSnapshot`/`newSnapshot` (null for add/delete),
`oldValue`/`newValue`, `ref`, and `isAdd`, `isUpdate`, `isDelete`.
* Writing to the same store from the listener triggers it again; guard
against infinite loops. Remove with `store.removeOnChangesListener(db,
sameCallback)`.
* `db.addAllStoresOnChangesListener(onChanges, excludedStoreNames: [...],
storePredicate: (name) => ...)` is the untyped variant for every store.
* Register triggers right after opening, before the app uses the database.
For reactive UI updates after commit use `onSnapshot`/`onSnapshots`
streams instead (see the `sembast-query` skill).
## Examples
### Typed store with add, put, update, delete
```dart
import 'package:sembast/sembast.dart';
final animals = intMapStoreFactory.store('animals');
Future<void> crud(Database db) async {
// Insert, generated int key.
var key = await animals.add(db, {'name': 'cat', 'age': 4});
// Upsert with an explicit key.
await animals.record(100).put(db, {'name': 'dog'});
// Merge fields into an existing record.
await animals.record(key).update(db, {'age': 5, 'color': 'black'});
// Read the value.
var cat = await animals.record(key).get(db);
print(cat); // {name: cat, age: 5, color: black}
// Delete.
await animals.record(100).delete(db);
print(await animals.record(100).exists(db)); // false
}
```
### Key/value settings in the main store
```dart
import 'package:sembast/sembast.dart';
final settings = StoreRef<String, String>.main();
final themeRecord = settings.record('theme');
Future<String> getTheme(Database db) async =>
await themeRecord.get(db) ?? 'light';
Future<void> setTheme(Database db, String theme) =>
themeRecord.put(db, theme);
```
### Nested fields, dotted paths and FieldValue.delete
```dart
import 'package:sembast/sembast.dart';
final store = intMapStoreFactory.store('people');
Future<void> paths(Database db) async {
var key = await store.add(db, {
'name': 'Felix',
'age': 4,
'address': {'city': 'Ledignan'},
'with.dots': 'kept as is by add',
});
var record = store.record(key);
await record.update(db, {
'address.city': 'San Francisco', // nested path
'age': FieldValue.delete, // remove a field
FieldKey.escape('with.dots'): 'literal key', // key containing a dot
});
print(await record.get(db));
// {name: Felix, address: {city: San Francisco}, with.dots: literal key}
}
```
### Transaction with a returned value and a batch delete
```dart
import 'package:sembast/sembast.dart';
final shop = intMapStoreFactory.store('shop');
Future<List<int>> replaceAll(
Database db,
List<Map<String, Object?>> products,
) {
return db.transaction((txn) async {
// Only txn is used inside the transaction, never db.
await shop.delete(txn);
return shop.addAll(txn, products);
});
}
Future<void> deleteSome(Database db, List<int> keys) =>
db.transaction((txn) => shop.records(keys).delete(txn));
```
### Add or update by a unique field, race free
```dart
import 'package:sembast/sembast.dart';
final products = intMapStoreFactory.store('product');
/// Insert or update the product whose 'code' matches.
Future<void> addOrUpdateProduct(Database db, Map<String, Object?> map) {
return db.transaction((txn) async {
var existing = await products
.query(finder: Finder(filter: Filter.equals('code', map['code'])))
.getSnapshot(txn);
if (existing == null) {
await products.add(txn, map);
} else {
await existing.ref.update(txn, map);
}
});
}
```
### Helpers accepting a DatabaseClient
```dart
import 'package:sembast/sembast.dart';
final counters = StoreRef<String, int>('counters');
/// Works with a Database or a Transaction.
Future<int> increment(DatabaseClient client, String name) async {
var record = counters.record(name);
var value = (await record.get(client) ?? 0) + 1;
await record.put(client, value);
return value;
}
Future<void> incrementTwo(Database db) => db.transaction((txn) async {
await increment(txn, 'a');
await increment(txn, 'b');
});
```
### Timestamp and Blob values
```dart
import 'dart:typed_data';
import 'package:sembast/blob.dart';
import 'package:sembast/sembast.dart';
import 'package:sembast/timestamp.dart';
final files = stringMapStoreFactory.store('files');
Future<void> saveFile(Database db, String id, Uint8List bytes) async {
await files.record(id).put(db, {
'createdAt': Timestamp.now(), // never a DateTime
'thumbnail': Blob(bytes), // never a raw Uint8List
});
var value = await files.record(id).get(db);
var createdAt = (value!['createdAt'] as Timestamp).toDateTime();
var thumbnail = (value['thumbnail'] as Blob).bytes;
print('$createdAt ${thumbnail.length} bytes');
}
```
### Clone a read value before modifying it
```dart
import 'package:sembast/sembast.dart';
import 'package:sembast/utils/value_utils.dart';
final store = intMapStoreFactory.store('notes');
Future<void> rename(Database db, int key) async {
var value = await store.record(key).get(db);
if (value == null) return;
// value is read-only; value['title'] = 'x' would throw StateError.
var map = cloneMap(value);
map['title'] = 'Renamed';
await store.record(key).put(db, map);
}
```
### Cascade delete with a store trigger
```dart
GitHubで見る