Skip to content

Instantly share code, notes, and snippets.

@blaugold
Created June 19, 2023 14:50
Show Gist options
  • Save blaugold/ab3cd21a6777612d67b2b2b5e066a12f to your computer and use it in GitHub Desktop.
Save blaugold/ab3cd21a6777612d67b2b2b5e066a12f to your computer and use it in GitHub Desktop.

Installation

Packages:

  • cbl: Contains the Couchbase Lite API.
  • cbl_flutter: Initializes Couchbase Lite for Flutter.
  • cbl_dart: Initializes Couchbase Lite for Standalone Dart or Flutter unit tests.
  • cbl_flutter_ce: Selects the Community Edition for use with Flutter.
  • cbl_flutter_ee: Selects the Enterprise Edition for use with Flutter.

Note: The Enterprise Edition is always free to use in development.

Set up a Flutter app for using the Couchbase Lite Enterprise Edition (including unit tests):

  1. flutter pub add cbl cbl_flutter cbl_flutter_ee dev:cbl_dart

  2. Initialize Couchbase Lite in main:

    import 'package:flutter/widgets.dart';
    import 'package:cbl_flutter/cbl_flutter.dart';
    
    Future<void> main() async {
      WidgetsFlutterBinding.ensureInitialized();
      await CouchbaseLiteFlutter.init();
      runApp(MyApp());
    }
  3. Initialize Couchbase Lite in unit tests:

     import 'package:flutter_test/flutter_test.dart';
     import 'package:cbl_dart/cbl_dart.dart';
    
     Future<void> main() async {
       setUpAll(() => CouchbaseLiteDart.init(edition: Edition.enterprise));
     }

Opening a Database

final directory = ...
final database = await Database.openAsync(
  'my_db',
  DatabaseConfiguration(directory: directory),
);

It is recommended to always specify a directory. When no directory is specified, the database is opened in the default directory for the platform.

Concurrency Control

Couchbase Lite supports optimistic locking. When a document is read, it includes a revision ID. Write operations can specify how to handle conflicts between the current revision and the revision that was read. The default is to ignore these conflicts and let the last write win, but it is also possible to fail the write operation or to merge the changes in a callback.

Scenarios where conflicts can occur:

  • In the same Dart isolate, when a document is read multiple times and independently modified.
  • In different Dart isolates, when a document is modified in parallel.
  • When a document is modified on different devices and the changes are replicated.

Creating a Document

// Use a randomly generated ID.
final document = MutableDocument({'message': 'Hello World!'});

// Or specify an ID.
final document = MutableDocument.withId('my_doc', {'message': 'Hello World!'});

// Save the document.
await database.saveDocument(document);

Updating a Document

// Read the document. This returns an immutable copy.
final document = await database.document('my_doc');

// Create a mutable copy.
final mutableDocument = document!.toMutable();

// Modify the document through a setter method.
mutableDocument.setString('message', 'Hello Dart!');

// Or through the setter of a fragment.
mutableDocument['message'].string = 'Hello Dart!';

// Save the document.
await database.saveDocument(mutableDocument);

Deleting a Document

// Read the document. This returns an immutable copy.
final document = await database.document('my_doc');

// Delete the document.
await database.deleteDocument(document!);

Using Documents With JSON Data

Setting JSON data:

final jsonMap = jsonDecode(...) as Map<String, Object?>;

// Create a document from JSON data.
final document = MutableDocument(jsonMap);

// Update an existing document with JSON data.
document.setData(jsonMap);

Getting JSON data:

// Get a JSON string.
final jsonString = document.toJson();

// Get a JSON map.
final jsonMap = document.toPlainMap();

Document Fragments

Document data is represented through Arrays and Dictionarys. Document extends Dictionary. These classes don't implement List or Map. Instead, they provide typed getter and setter methods. To make them more convenient to use, they also provide a subscript operator ([]), which returns a fragment.

Fragments make it easy to access and modify nested properties because they are easy to chain. They have getters and setters for all supported data types (e.g. string, float, dictionary).

// Get a nested property through container getters.
final avatarUrl = document.array('friends')!.dictionary(0)!.string('avatarUrl');

// Get the same nested property through chained fragments.
final avatarUrl = document['friends'][0]['avatarUrl'].string;

Creating a Query

// Create a query using the builder API.
final query = QueryBuilder()
    .select(
      SelectResult.expression(Meta.id),
      SelectResult.property('name'),
    )
    .from(DataSource.database(database))
    .where(Expression.property('age').greaterThan(Expression.number(21)))
    .limit(Expression.number(10))

// Create the same query using SQL++.
final query = await Query.fromN1ql(
    database,
    '''
    SELECT META().id, name
    FROM _
    WHERE age > 21
    LIMIT 10
    ''',
);

Executing a Query

final resultSet = await query.execute();
await for (final result in resultSet.asStream()) {
  print(row.toPlainMap());
}

Watching a Query

The change stream immediately emits the current results and then emits changes as they occur.

await for (final change in query.changes()) {
  print(change.results.toPlainMap());
}

Explaining a Query

A query explanation gives insights into how a query is executed. It can be used to optimize a query, for example, by adding or changing indexes.

print(await query.explain());

Creating an Value Index

A value index indexes documents by one or more expressions that extract a value from the document.

// Create a value index with a single expression using the builder API.
await database.createIndex('name', ValueIndex([ValueIndexItem.property('name')]));

// Create the same index using SQL++.
await database.createIndex('name', ValueIndexConfiguration(['name']))

Creating a Full-Text Index

A full-text index indexes documents by an expression that extracts a string from the document and makes this string searchable.

Full-text indexes can be made more effective by specifying a language.

// Create a full-text index using the builder API.
await database.createIndex('name_fts', FullTextIndex([FullTextIndexItem.property('name')]));

// Create the same index using SQL++.
await database.createIndex('name_fts', FullTextIndexConfiguration(['name']))

Using a Full-Text Index

The full-text query syntax supports prefix queries (*), phrases and more. For details see the documentation.

// Create a query that uses a full-text index using the builder API.
final query = QueryBuilder()
    .select(SelectResult.all())
    .from(DataSource.database(database))
    .where(FullTextFunction.match(indexName: 'name_fts', query: 'ali*'));

// Create the same query using SQL++.
final query = await Query.fromN1ql(
    database,
    '''
    SELECT *
    FROM _
    WHERE MATCH(name_fts, 'ali*')
    ''',
);

Creating a Replicator

A replicator can be used to replicate a local database with a remote server or with another local database (Enterprise Edition only).

Replication between two local databases is useful, for example, for incremental backups.

// Configuration for replication with a remote server.
final configuration = ReplicatorConfiguration(
  database: database,
  target: UrlEndpoint(Uri.parse('ws://localhost:4984/example')),
);

// Configuration for for replication with another local database.
final configuration = ReplicatorConfiguration(
  database: database,
  target: DatabaseEndpoint(otherDatabase),
);

// Create the replicator.
final replicator = await Replicator.create(configuration);

Starting and Observing a Replication

// Start listening for changes. It's important to listen to the stream
// before starting the replicator to avoid missing any events.
final replicatorStopped = replicator
  .changes()
  .singleWhere((change) => change.status.activity == ReplicatorActivityLevel.stopped);

// Start the replicator.
await replicator.start();

// Wait until the replicator has stopped.
final lastChange = await replicatorStopped;

// Check if an error occurred during replication.
final error = lastChange.status.error;
if (error != null) {
  print('Replication finished with error: $error');
} else {
  print('Replication completed successfully');
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment