Skip to content

Database

Dustin Catap edited this page Nov 12, 2023 · 2 revisions

Database is one of the (local) data sources of our application. It is responsible for persisting data on the device. This is important if you plan to support offline functionality in your application.

The project is using Isar as the database.

Creating the Database

The database file is saved in the application's cache directory. IsarDatabaseFactory is responsible for opening the database when we are going read or write data.

Creating a Data Object

A DataObject is a class that represents a table in the database.

See Data Objects for more details.

Creating a Local Data Source

LocalDataSource is an interface that defines the operations that can be done in the database. We use this interface to hide the implementation details of the database to the consumers of the data source.

abstract interface class LocalDataSource<T extends DataObject> {
  Future<T?> get(int id);

  Future<Iterable<T>> getAll({required int offset, required int limit});

  Future<void> addOrUpdate(T object);

  Future<void> addOrUpdateAll(Iterable<T> objects);

  Future<void> delete(int id);

  Future<void> deleteAll(Iterable<int> ids);
}

💡 IMPORTANT

  • By default, we expose getAll and deleteAll database operations with filters. This is to avoid loading all the data in the database at once. This is important if you have a large amount of data in the database. Loading all the data without any filters may be applicable for tables that have a small or fixed amount of data.
  • Update the implementing interfaces of LocalDataSource if you wish to add additional filters. See the next sections below for more details.

Once you have created your data object, you can now create your local data source by extending IsarLocalDataSource.

import 'package:injectable/injectable.dart';
import 'package:isar/isar.dart';

abstract interface class PostLocalDataSource implements LocalDataSource<PostDataObject> {}

@LazySingleton(as: PostLocalDataSource)
class PostLocalDataSourceImpl extends IsarLocalDataSource<PostDataObject> implements PostLocalDataSource {
  const PostLocalDataSourceImpl(super._isarDatabaseFactory);

  @protected
  @override
  @visibleForTesting
  IsarGeneratedSchema get schema => PostDataObjectSchema;
}
  • @LazySingleton is used to register the data source as a lazy singleton. We don't need to worry about sharing the same instance since all transactions will be using a different database instance from IsarDatabaseFactory.
  • IsarLocalDataSource is a base class that implements LocalDataSource. It is responsible for opening the database and executing transactions.
  • PostDataObjectSchema is generated after running the build_runner. It is used to point the schema to use when opening the database.
  • It is important that we only implement LocalDataSource<PostDataObject> to PostLocalDataSource. This will hide the unecessary implementation details of IsarLocalDataSource to the consumers of PostLocalDataSource.

💡 TIP

  • Use the snippet shortcut locd to create a local data source.

Extending the Local Data Source

If you need to support other queries to your data source, you can add update your own interface that implements LocalDataSource.

import 'package:injectable/injectable.dart';
import 'package:isar/isar.dart';

abstract interface class PostLocalDataSource implements LocalDataSource<PostDataObject> {
  Future<PostDataObject?> getPost(int postId);
}

@LazySingleton(as: PostLocalDataSource)
class PostLocalDataSourceImpl extends IsarLocalDataSource<PostDataObject> implements PostLocalDataSource {
  PostLocalDataSourceImpl(super._isarDatabaseFactory);

  @protected
  @override
  @visibleForTesting
  IsarGeneratedSchema get schema => PostDataObjectSchema;

  @override
  Future<PostDataObject?> getPost(int postId) async {
    final PostDataObject? object = await readWithIsar((Isar i) {
      return i.posts.where().postIdEqualTo(postId).findFirst();
    });

    return object;
  }
}s
  • In the example above, we added getPost which uses Isar's generated collection extension on an instance of Isar for our PostDataObject called posts. It provides additional filter options for each of the properties of the data object, for this example we used postIdEqualTo to filter the posts by postId.

💡 TIP

  • As stated in Isar's documentation about Indexes, using @Index() improves the performance of your queries if you are to use the annotated property for filtering.