Migrating from 4.x to 5.0
5.0 changes most call sites. Each ODM method now maps to one cloud_firestore call, and DateTime fields are stored as Firestore Timestamp values. There is no compatibility layer; update the code and, if you stored dates with 4.x, convert the stored data.
Steps
- Set
firestore_odmandfirestore_odm_builderto^5.0.0. - Update the schema declaration (below).
- Convert stored
DateTimestrings toTimestampvalues (below). - Replace removed APIs using the tables below.
- Regenerate:
dart run build_runner build --delete-conflicting-outputs. - Run your tests against the emulator or a test database.
Schema declaration
4.x generated the schema class. In 5.0 you declare it:
// 4.x
part 'schema.g.dart';
@Schema()
@Collection<User>('users')
const appSchema = _$AppSchema;// 5.0
part 'schema.g.dart';
class AppSchema extends FirestoreSchema {
const AppSchema();
}
@Schema()
@Collection<User>('users')
const appSchema = AppSchema();Stored dates
4.x wrote DateTime fields as ISO-8601 strings. 5.0 writes and reads them as Timestamp values, and reading a document that still holds a string in a DateTime field throws. Convert existing documents once, before the new app version reads them. A sketch with cloud_firestore, for the users collection:
import 'package:cloud_firestore/cloud_firestore.dart';
Future<void> convertDates(
FirebaseFirestore db,
String collection,
List<String> dateFields,
) async {
final docs = (await db.collection(collection).get()).docs;
for (var i = 0; i < docs.length; i += 500) {
final batch = db.batch();
for (final doc in docs.skip(i).take(500)) {
final updates = <String, Object?>{};
for (final field in dateFields) {
final value = doc.data()[field];
if (value is String) {
updates[field] = Timestamp.fromDate(DateTime.parse(value));
}
}
if (updates.isNotEmpty) batch.update(doc.reference, updates);
}
await batch.commit();
}
}
await convertDates(FirebaseFirestore.instance, 'users', [
'lastLogin',
'createdAt',
'updatedAt',
]);Nested date fields use dotted paths, such as 'profile.lastActive'. Run the conversion for every collection and subcollection with DateTime fields. Queries that filter or sort on those fields only match converted documents.
Duration is stored as integer microseconds in both versions; no change is needed.
Writes
| 4.x | 5.0 |
|---|---|
insert(model) | set(model) to write under the model's ID, or create(model) for a generated ID |
insert(User(id: FirestoreODM.autoGeneratedId, ...)) | final id = await create(model) |
update(model) | set(model) (replaces; does not require the document to exist) |
upsert(model) | set(model) |
modify((u) => u.copyWith(...)) | patch(($) => [...]), or a transaction when the new value depends on the current one |
incrementalModify(...) | patch(($) => [...]) with increment, arrayUnion, arrayRemove |
exists() | await get() != null |
Patch operations
| 4.x | 5.0 |
|---|---|
$.name('x') | $.name.set('x') |
$.tags.add(v) / $.tags.addAll([...]) | $.tags.arrayUnion([...]) |
$.tags.remove(v) / $.tags.removeAll([...]) | $.tags.arrayRemove([...]) |
$.age.increment(1) | $.age.increment(1) (unchanged) |
$.updatedAt.serverTimestamp() | $.updatedAt.serverTimestamp() (unchanged) |
FirestoreODM.serverTimestamp in a model | $.field.serverTimestamp() in a patch (see Server Timestamps) |
$.profile.followers.increment(1) (nested field) | $.profile.set(updatedProfile) (nested models are patched as a whole value) |
$.settings.setKey('k', v) | $.settings.set({...current, 'k': v}) |
Other changes:
patchsends exactly the listed operations. 4.x merged operations on the same field by precedence rules; 5.0 does not. If you list two operations on one field, Firestore applies the last one.incrementis available on non-nullable numeric fields, andarrayUnion/arrayRemoveon non-nullableListfields.
Queries
| 4.x | 5.0 |
|---|---|
where(($) => $.and(a, b)) | where(($) => a & b) or a.and(b) |
where(($) => $.or(a, b)) | where(($) => a | b) or a.or(b) |
orderBy(($) => $.age()) | orderBy(($) => ($.age(),)) (always a record) |
count().get() | count() returns Future<int> |
count().stream, aggregate(...).stream | removed; call count() or aggregate(...).get() again |
query.patch(($) => [...]) | query.patchAll([...]) with operations from <Model>PatchBuilder() (see Bulk Operations) |
query.modify(...), query.incrementalModify(...) | query.patchAll([...]) |
query.delete() | query.deleteAll() |
Subcollections, transactions and batches
| 4.x | 5.0 |
|---|---|
odm.users('u1').posts | odm.usersPosts('u1') |
odm.users('u1').posts('p1').comments | odm.usersPostsComments('u1', 'p1') |
tx.users('u1').get() | odm.users.inTransaction(tx)('u1').get() |
tx.users('u1').modify(...) | odm.users.inTransaction(tx)('u1').patch(($) => [...]) |
batch.users.insert(model) | odm.users.inBatch(batch).set(model) or .create(model) |
batch.users('u1').patch(...) | odm.users.inBatch(batch).patch('u1', ...) |
batch.users('u1').delete() | odm.users.inBatch(batch).delete('u1') |
Models
fast_immutable_collectionstypes (IList,IMap,ISet) are not supported. UseList,MapandSet.json_serializableis no longer needed for storage. The ODM generates its own converters; keeptoJson/fromJsononly if you use them elsewhere.
New since 4.x
createreturns the generated document ID, also in batches and transactions.getacceptsGetOptionsto read from the cache or the server only.patchAllanddeleteAllsplit their writes into batches of at most 500.GeoPoint,DocumentReference,BlobandTimestampfields are stored as they are.
See Benchmarks for performance measurements.