[Local RDB] Fast! An Overview and How to Use Drift!!

In app design, it is necessary to permanently store specific data.
In such cases, a local DB is designed within the user’s smartphone.

Previously, I introduced Isar, but my personal evaluation is that Isar has a low update frequency, which is concerning.
Therefore, this time, I will introduce Drift, a high-speed RDB that can compete with Isar.

When Do You Need a Local DB in the First Place?

Normally, variables and states used in an app are lost when the app is closed,
but some apps have data like memos or chat histories that you want to use the next time the app is launched.

To save such data, a local DB is used.

What is Drift?

Drift is a powerful local database library for Flutter and Dart.
It is based on SQLite and offers a type-safe query builder, reactive streams,
and intuitive data manipulation. Formerly known as Moor.

Key Features of Drift

By saving data in a local DB, you can access it the next time the app is launched.

Drift has the following features:

  1. Type-Safe Queries
    Drift leverages Dart’s type system to detect query errors at compile time.
    It’s great to catch errors during coding!
  2. Reactive Streams
    It can detect changes in the database in real-time and automatically update the UI.
  3. Scalability and Customizability
    Drift is highly scalable and allows you to easily add custom functions and triggers.
    Handling complex queries and transactions is straightforward, providing high flexibility!
  4. Comprehensive Documentation
    Drift is supported by detailed documentation and an active community,
    so you can likely do most things by referring to the documentation!?
    Official Documentation: https://drift.simonbinder.eu/setup/
  5. Active Update Frequency
    Drift is frequently updated, with version upgrades every two weeks to a month.
    This might be because Stream Inc. sponsors Drift, keeping it active.

How to Use Drift

Here, I will introduce how to use Drift.

Setting Up the Environment

Add the necessary packages to the “pubspec.yaml”.
Please use the versions that suit your environment.

dependencies:
  ~ Other existing packages ~
  drift: ^2.23.1
  sqlite3_flutter_libs: ^0.5.28

dev_dependencies:
  build_runner: ^2.4.14
  drift_dev: ^2.23.1
  • drift: Core library for Drift
  • sqlite3_flutter_libs: Additional library to use SQLite3 in Flutter
  • build_runner: Tool for Dart code generation
  • drift_dev: Drift’s code generation tool

Defining the Database and Tables

First of all, since Drift is an RDB, you need to define the database and tables.
This time, the files are separated as follows, but you can also define tables and the database in a single file.

  • Table Definition File: todo_tbl.dart
  • Database Definition File: app_db.dart
  • Auto-Generated File: app_db.g.dart

Table Definition

Define the tables and set the data to be managed.
For example, let’s define a table to manage ToDo information: ToDos.

part of 'app_db.dart';

class Todos extends Table {
  IntColumn get id => integer().autoIncrement()(); // autoIncrement() for automatic numbering
  TextColumn get title => text().withLength(min: 1, max: 50)();
  BoolColumn get completed => boolean().withDefault(Constant(false))();
}

By adding an “s” at the end of the table name, the record name automatically excludes the “s”.
In the above example, the table name is ToDos, and the record name is ToDo.

Defining the Database

import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
import 'dart:io';

part 'todo_tbl.dart'; // File where the table is defined above
part 'app_db.g.dart'; // File automatically generated by the generator

@DriftDatabase(tables: [Todos]) // If there are multiple tables, specify them separated by commas like [ToDos, Users]
class AppDatabase extends _$AppDatabase {
  AppDatabase() : super(_openConnection());

  @override
  int get schemaVersion => 1; // Schema version

  // Operations
  Future<List<ToDo>> getAllTodos() => select(todos).get(); // Retrieve all records
  Future<List<ToDo>> fetchTodos(String word) async {
    return await (select(todos)..where((tbl) => tbl.title.equals(word))).get();
  }
  Future insertTodo(TodosCompanion todo) => into(todos).insert(todo); // Insert a record
  Future updateTodo(Todo todo) => update(todos).replace(todo); // Update a record
  Future deleteTodo(int id) =>
      (delete(todos)..where((tbl) => tbl.id.equals(id))).go(); // Delete a record
  Stream<List<ToDo>> watchAllTodos() => select(todos).watch(); // (Reference) Watch records
}

// Location to store the actual data of the local DB
LazyDatabase _openConnection() {
  return LazyDatabase(() async {
    final dbFolder = await getApplicationDocumentsDirectory();
    final file = File(p.join(dbFolder.path, 'db.sqlite'));
    return NativeDatabase(file);
  });
}

The imported “app_db.g.dart” does not exist at this point, so an error will occur.
By running the following command in the terminal, “app_db.g.dart” will be created.

flutter pub run build_runner build --delete-conflicting-outputs

Basic Usage of Drift

Here, I will introduce client-side processing for performing database operations using Drift.
Basically, you just call the operations set in the database above.

Fetching Data

import 'app_db.dart';
final db = AppDatabase();

Future<List<ToDo>> fetchAllTodos() async {
  return await db.getAllTodos();
}

Conditional Data Retrieval

import 'app_db.dart';
final db = AppDatabase();

Future<List<ToDo>> fetchTodos(String word) async {
  return await db.fetchTodos(word);
}

Inserting Data

import 'app_db.dart';
final db = AppDatabase();

Future<void> addTodo(String title) async {
  await db.insertTodo(TodosCompanion(
    title: Value(title),
  ));
}

Updating Data

import 'app_db.dart';
final db = AppDatabase();

Future<void> toggleTodoStatus(Todo todo) async {
  final updatedTodo = todo.copyWith(completed: !todo.completed);
  await db.updateTodo(updatedTodo);
}

Deleting Data

import 'app_db.dart';
final db = AppDatabase();

Future<void> deleteTodoItem(int id) async {
  await db.deleteTodo(id);
}

Domain Models and Mapping for Data

Usually, the data handled within an app is not simple and is defined as a domain model.
In such cases, data conversion between the domain model and the database model is necessary.

Defining the Domain Model

Assume that the ToDo class in the domain model is as follows.

/// Domain Model: ToDo
class ToDo {
  final int? id;
  final String title;
  final bool isCompleted;

  ToDo({
    this.id,
    required this.title,
    this.isCompleted = false,
  });
}

Conversion Between Domain and Database Models

Using extension methods, implement the conversion process between the domain model and the database model.

/// Conversion from Domain Model to Drift's Database Model
extension ToDoToDatabase on ToDo {
  TodosCompanion toInsertCompanion() {
    return TodosCompanion.insert(
      // id is automatically set during insert
      title: title,
      completed: completed,
    );
  }

  TodosCompanion toUpdateCompanion() {
    return TodosCompanion(
      id: Value(id),
      title: Value(title),
      completed: Value(isCompleted),
    );
  }
}

/// Conversion from Drift's Database Model to Domain Model
extension DatabaseToDo on Todo {
  ToDo toDomain() {
    return ToDo(
      id: id,
      title: title,
      isCompleted: completed,
    );
  }
}

Data Operations Utilizing Mapping

As an example, I will introduce data operations using the above conversion process.

import 'app_db.dart';
final db = AppDatabase();

// Insert ToDo
Future<void> addToDoItem(ToDo toDo) async {
  await db.insertTodo(toDo.toInsertCompanion());
}

// Retrieve All ToDos
Future<List<ToDo>> getToDoItems() async {
  final dbTodos = await db.getAllTodos();
  return dbTodos.map((dbTodo) => dbTodo.toDomain()).toList();
}

Cautions When Using Drift

Since Drift is an RDB, version management is necessary when changing table definitions or schemas.
Additionally, when adding data items, migration processes must be written for each version.

For more details, please refer to the official documentation.
Official Documentation: https://drift.simonbinder.eu/Migrations/step_by_step/?h=migration

Conclusion

The processing speed of Drift was not much different from Isar in actual device experience.
Due to the high update frequency of the package, it is easy to adjust dependencies with other packages, making it user-friendly.
I believe Drift is a good alternative to Isar.

I encourage everyone to try developing with Drift!

Copied title and URL