Skip to content
On this page

Writing & Updating Documents

This guide covers all the ways to create, update, and delete single documents. For information on updating multiple documents at once, see the Bulk Operations guide.

Creating Documents

  • insert(T value): Adds a new document to a collection. The document ID is taken from the @DocumentIdField() in your model. This method will fail if a document with that ID already exists.
  • upsert(T value): Adds a new document or completely overwrites an existing one with the same ID. This is useful when you don't need to worry about whether the document exists.
dart
// Assumes 'id' is the @DocumentIdField in the User model.

// Fails if 'john-doe' already exists
await db.users.insert(User(id: 'john-doe', name: 'John', ...));

// Creates a new document or overwrites the existing one
await db.users.upsert(User(id: 'jane-doe', name: 'Jane', ...));

Auto-Generated Document IDs

When you want Firestore to automatically generate unique document IDs, use the special constant:

dart
// Use the auto-generated ID constant
await db.users.insert(User(
  id: FirestoreODM.autoGeneratedId, // Server generates unique ID
  name: 'Auto User',
  email: 'auto@example.com',
));

// Note: Auto-generated IDs cannot be used with upsert operations
// because upsert needs a specific ID to check for existing documents.
// Use insert() for auto-generated IDs:
await db.users.insert(User(
  id: FirestoreODM.autoGeneratedId,
  name: 'Auto Insert User',
  email: 'autoinsert@example.com',
));

// For upsert, provide a specific ID:
await db.users.upsert(User(
  id: 'specific-user-id',
  name: 'Upsert User',
  email: 'upsert@example.com',
));

The generated document ID will be available when you read the document back:

dart
// Query to find the user since we don't know the generated ID
final users = await db.users
  .where(($) => $.email(isEqualTo: 'auto@example.com'))
  .get();

if (users.isNotEmpty) {
  final user = users.first;
  print('Generated ID: ${user.id}'); // Shows the auto-generated ID
}

Updating a Document

The ODM provides two powerful and flexible methods for updating a single document.

The patch() method is for making specific, atomic updates. This is the most efficient method for operations like incrementing a number, adding/removing elements from an array, or setting a server timestamp. It gives you a builder with methods for each field.

dart
final userDoc = db.users('jane-doe');

await userDoc.patch(($) => [
  $.profile.followers.increment(1),
  $.tags.add('active'), // Atomically adds 'active' to the 'tags' array
  $.tags.remove('inactive'), // Atomically removes 'inactive' from the 'tags' array
  $.lastLogin.serverTimestamp(),
  $.name('Jane Smith'), // Also supports simple field sets
]);

Array Bulk Operations

For adding or removing multiple elements at once, use addAll() and removeAll():

dart
await userDoc.patch(($) => [
  // Add multiple tags at once - accepts any Iterable
  $.tags.addAll(['premium', 'verified', 'active']),  // List
  $.tags.addAll({'new', 'unique'}),                  // Set
  $.tags.addAll(someIterable),                       // Any Iterable
  
  // Remove multiple old tags at once - accepts any Iterable
  $.scores.removeAll([0, -1, -5]),                   // List
  $.scores.removeAll({-10, -20}),                    // Set
  
  // Mix with other operations
  $.profile.followers.increment(10),
]);

Important Notes:

  • You cannot use both addAll() and removeAll() (or add() and remove()) on the same field in a single patch() operation due to Firestore limitations
  • For mixed operations on the same field, use separate patch() calls:
dart
// First remove unwanted elements
await userDoc.patch(($) => [
  $.tags.removeAll(['old', 'deprecated']),
]);

// Then add new elements
await userDoc.patch(($) => [
  $.tags.addAll(['new', 'updated']),
]);

Operation Precedence and Behavior

When using patch() with multiple operations on the same field, it's important to understand the precedence rules:

Set Operations Override Array Operations:

dart
await userDoc.patch(($) => [
  $.tags.add('will-be-ignored'),        // Array operation
  $.tags.addAll(['also-ignored']),      // Array operation
  $.tags(['final', 'result']),          // Set operation - WINS!
]);
// Result: tags = ['final', 'result']
// The set operation overrides all array operations on the same field

Multiple Set Operations - Last One Wins:

dart
await userDoc.patch(($) => [
  $.tags(['first']),                    // Set operation
  $.tags(['second', 'wins']),           // Set operation - WINS!
]);
// Result: tags = ['second', 'wins']

Array Operations Accumulate (when no set operations):

dart
await userDoc.patch(($) => [
  $.tags.add('first'),                  // Array operation
  $.tags.addAll(['second', 'third']),   // Array operation
]);
// Result: tags = [existing_tags..., 'first', 'second', 'third']
// All array operations are applied together

Mixed Add/Remove on Same Field - Not Allowed:

dart
// ❌ This will throw an error
await userDoc.patch(($) => [
  $.tags.add('new'),
  $.tags.remove('old'),  // Error: Cannot mix add/remove on same field
]);

// ✅ Use separate patch calls instead
await userDoc.patch(($) => [$.tags.remove('old')]);
await userDoc.patch(($) => [$.tags.add('new')]);

Different Operation Types Work Independently:

dart
await userDoc.patch(($) => [
  $.age.increment(1),                   // Increment operation
  $.tags.addAll(['new', 'tags']),       // Array operation
  $.name('Updated Name'),               // Set operation
  $.lastLogin.serverTimestamp(),        // Server timestamp
]);
// All operations are applied - they don't interfere with each other

modify() (Convenient but Slower)

This method compares the current state of your document with the new state you provide. It performs a read operation followed by an update operation, making it slightly slower than patch() due to the additional read. However, it's convenient when you need to read the current state before writing.

Important Notes:

  • Performance: This method has an additional read operation, making it slower than patch()
  • Concurrency: Firestore uses last-write-wins semantics. This read-modify-write operation is NOT transactional and may be subject to race conditions
  • Transactions: For transactional updates, use transactions instead

By default, modify() automatically detects and uses atomic operations where possible:

dart
await userDoc.modify((user) => user.copyWith(
  // This will be converted to a FieldValue.increment(1) operation
  age: user.age + 1,

  // This will be converted to a FieldValue.arrayUnion(['new-tag'])
  tags: [...user.tags, 'new-tag'],

  // This will just be a normal field update
  name: 'Jane Smith',
));

You can disable atomic operations by setting atomic: false:

dart
await userDoc.modify((user) => user.copyWith(
  name: 'Jane Smith',
  isPremium: true,
), atomic: false);

update()

The update() method performs a full overwrite of an existing document. The entire object you provide will replace the one in Firestore. This method will fail if the document does not already exist.

dart
// The user document MUST exist for this to succeed.
await userDoc.update(User(id: 'jane-doe', name: 'Jane Smith', ...));

Deleting a Document

To delete a single document, use the .delete() method on a document reference.

dart
await db.users('jane-doe').delete();