【Flutter】高速データベースIsarでStreamのアプリ開発!

対象者

  • ITエンジニアであり、特にアプリケーション開発に従事している人
  • データベース技術の理解を深め、自身の技術力を向上させたいと考えている人
  • 新たなデータベース技術であるIsarを習得し、アプリのパフォーマンス向上を目指している人

はじめに

データベースはアプリケーションの性能に大きな影響を与えます。データベースを適切に選び、最適化することで、アプリケーションのパフォーマンスは飛躍的に向上します。しかし、新しいデータベース技術が次々と生まれ、その中から最適なものを選ぶのはなかなか難しいですよね。そんなあなたにピッタリな技術がここにあります。それが「Isar」です。

Isarは、アプリケーション開発で高いパフォーマンスを発揮するデータベース技術です。しかし、未だにIsarについて詳しく解説した日本語の情報は少ないのが現状です。本記事では、そんなIsarの特徴や設定方法、応用例などを詳しく解説します。Isarの使い方を理解し、あなたのアプリ開発スキルを更にレベルアップさせましょう。全てのエンジニアが持つべき知識であり、あなたが次のステップに進むための一助になれば幸いです。

本記事を読むことで、あなたはIsarの効率的な使用法を理解し、これを自身のプロジェクトに適用できるようになります。データベースの選択と最適化が、あなたのアプリのパフォーマンスを大きく左右することを理解し、Isarを効果的に使用することで、あなたのアプリは新たな次元のパフォーマンスを発揮します。

Isarとは何か

Isarは、Flutterに特化した高速で特徴豊かなNoSQLデータベースです。非常に便利な機能と簡単な使用方法を提供し、同時にスケーラビリティを確保し、大量のデータの効率的な非同期クエリを可能にします。また、iOS、Android、そしてデスクトップといった複数のプラットフォームをサポートしています。さらに、オープンソースとして提供されており、開発者が活発にコミュニティに参加し、独自の改良を加えることも可能です。

Isarの基本的な特徴

Isarは非同期操作、フルテキスト検索、およびマルチプラットフォーム開発をサポートしています。また、フィルタリング、ソート、クエリの各種機能を提供します。さらに、リアルタイムの監視と編集を可能にするIsar Inspectorも特徴的な機能の一つです。コレクションやクエリの変更を監視することも可能で、これによりリアルタイムのデータ更新を反映するアプリケーションを簡単に開発できます。

Isarを選ぶ理由

Isarが選ばれる理由は、その高速性と豊富な機能、そして使いやすさにあります。Isarは、複合インデックスやマルチエントリインデックス、クエリ修飾子、JSONのサポートなど、幅広いデータ管理機能を提供します。さらに、トランザクションに対するACID(原子性、一貫性、分離性、持続性)セマンティクスを確保し、コンパイル時にクエリをチェックする静的型付けもサポートしています。これにより、開発者はデータの一貫性と信頼性を確保しながら、効率的なデータ操作を行うことができます。また、非同期クエリのサポートにより、ユーザーインターフェースのパフォーマンスを低下させることなく、大量のデータを効率的に操作できます。

Isarの使いやすさは、データベースの操作をシンプルにするためのフレンドリーなAPIと組み合わされています。データの追加、更新、削除などの基本的なCRUD操作は、一貫したメソッドを通じて行うことができます。

簡単な操作の例を見てみましょう。以下は、新しいレコードを追加するための基本的なコードです:

 final todo = Todo()
  ..title = _textEditingController.text;
isar.writeTxn(() => isar.todos.put(todo));

このコードは、新しいユーザーレコードを作成し、データベースに追加しています。Isarのメソッドは直感的であり、開発者が素早くデータベース操作を行うことを可能にします。

以上の要素が組み合わさることにより、Isarは多くの開発者にとって信頼性と効率性を兼ね備えた選択肢となっています。

最初はObjectBoxを使おうと思ったのですが、Windows 上の Android で動作しなかったので、挫折しました(解決する気力もなく)。hiveも有力ですが、後継らしいのでIsarを選択しました。

Isarのセットアップ

FlutterプロジェクトにIsarを導入するには、まずIsarパッケージとその依存関係を追加し、アノテーションを使用してデータモデルを定義します。次に、コード生成を行い、Isarインスタンスを開設します。このステップは、データベースの初期設定とその後の使用のための基礎を作る重要なステップです。

依存関係の追加とアノテーションの使用

FlutterプロジェクトにIsarを導入するには、まずpubspec.yamlファイルにIsarパッケージとその関連パッケージを追加する必要があります。

dependencies:
  flutter:
    sdk: flutter
  isar: ^3.1.0+1
  isar_flutter_libs: ^3.1.0+1
  path_provider: ^2.0.15

dev_dependencies:
  isar_generator: ^3.1.0+1
  build_runner: ^2.4.6

次に、データモデルを作成します。データモデルは、データベースの構造とそれがどのように動作するかを定義します。このために、Isarのアノテーションを使用します。ここでは、Todoモデルを作成し、その属性に対してアノテーションを使用します:

import 'package:isar/isar.dart';
part 'main.g.dart';

@Collection()
class Todo {
  Id id = Isar.autoIncrement;

  String? title;

  @Index()
  bool? isCompleted;
}

ここでは、@Collection()アノテーションを用いてUserモデルがデータベースのコレクションであることを示し、@Id()アノテーションを使って主キーを指定し、@Index()アノテーションを使ってインデックスを作成しています。
finalを付けたり や?を消したり、コンストラクタを作成すると、ライブラリが上手く動作しないので、つけない方が良さそうです。(どこまで大丈夫か未検証。ただイミュータブルにしようとして、IDが自動生成されず、削除できなかった)

コード生成とIsarインスタンスの開設

Isarはコード生成により、モデルクラスに対するIsarの操作を提供します。Isarのコード生成を行うには、次のコマンドを実行します:

flutter pub run build_runner build

このコマンドは、@Collection()アノテーションが付けられた各モデルに対するIsarの操作を生成します。

次に、Isarインスタンスを開設します。これは、アプリケーションの起動時に一度だけ行われます:

final isar = await Isar.openSync([TodoSchema], directory: directory.path);

これで、Isarのインスタンスを開設し、データベースに対する操作を行う準備が整いました。このIsarインスタンスを使用して、以降のデータベースの操作(データの追加、更新、削除など)を行うことができます。

以上が、IsarをFlutterプロジェクトにセットアップするための基本的な手順です。このセットアップを行うことで、Isarの提供する高速な操作と豊富な機能をFlutterアプリケーションで利用することができます。

IsarでのCRUD操作

Isarを用いたデータベースの操作は、一般的なCRUD(Create、Read、Update、Delete)操作を網羅します。直感的なAPIを備えたIsarは、データベース操作のための信頼性と速度を確保します。

レコードの追加や更新

Isarによるデータの追加や更新は非常にシンプルです。これは、Isarが提供する直感的なAPIを使用することで実現されます。次の例では、新たにUserモデルのインスタンスを作成し、それをデータベースに追加しています。

 final todo = Todo()
..title = _textEditingController.text;

isar.todos.put(todo);

また、既存のレコードを更新するには、同じ.put()メソッドを使用します。Isarは自動的に主キーを検索し、一致するレコードが存在すればそのデータを新しいものに更新します。

レコードの削除

Isarでは、データベースからレコードを削除するための簡潔なAPIが提供されています。具体的には、レコードの主キーを指定して削除を行うことができます。

isar.todos.delete(1);

上記のコードは、主キーが1のUserレコードを削除します。これにより、不要なデータを効率的に管理することが可能となります。

これらの操作は、Isarが提供する直感的でパフォーマントなAPIにより、開発者が直面する日常的な問題を解決します。データの追加、更新、削除といった基本的なCRUD操作は、アプリケーションの主要な機能であり、Isarはこれらの操作をシンプルに、そして効率的に行う手段を提供します。

Isarの詳細な機能

Isarはただデータを保存するだけではなく、高度なクエリ機能、フルテキスト検索、そしてトランザクションのACID準拠といった詳細な機能を持っています。これにより、データベースの操作が非常に柔軟でパワフルになります。

フルテキスト検索とクエリの修飾

Isarは、非常に強力なクエリ機能とフルテキスト検索機能を提供しています。これにより、データベース内の任意のテキストを簡単に検索することが可能となります。例えば、特定の単語を含む全てのユーザーレコードを検索することができます。

final users = await isar.todos.where()
  .filter()
  .titleContains('test')
  .findAll();

このクエリは、名前に’Alice’という文字列を含む全てのユーザーを返します。また、クエリは修飾可能で、複雑な条件を設定することも可能です。これにより、必要なデータだけを効率的に取り出すことが可能になります。

トランザクションのACID準拠

Isarは、ACID(Atomicity、Consistency、Isolation、Durability)を満たすトランザクションをサポートしています。これにより、データの一貫性と整合性を保つことが可能となります。トランザクションは以下のように開始することができます。

 isar.writeTxn(() => isar.todos.put(
    list[index]..isCompleted = value));

このトランザクションでは、新しいユーザーを作成し、それをデータベースに保存します。もし途中で何らかのエラーが発生した場合、変更は全てロールバックされ、データベースの状態はトランザクション開始前の状態に戻ります。

動的なクエリと集計操作

さらに、Isarは動的なクエリと集計操作をサポートしています。これにより、アプリケーション内でデータを効率的に操作することができます。例えば、ユーザーの平均年齢を計算することができます。

final avgAge = await isar.users.where().avgProperty((u) => u.age);

このコードは、データベース内の全てのユーザーの平均年齢を返します。Isarのこのような動的なクエリと集計操作は、大量のデータを扱うアプリケーションで非常に有用です。

Isarの詳細な機能は、アプリケーションのパフォーマンスと柔軟性を向上させます。フルテキスト検索、クエリの修飾、ACIDトランザクション、そして動的なクエリと集計操作は、Isarを効率的で強力なデータストレージソリューションにしています。

Isarのパフォーマンスと最適化

Isarはパフォーマンスと最適化に関しても優れた機能を提供します。インデックスの使用、タイプコンバーター、そしてオブジェクト間のリレーションシップを最適化することで、データベースのパフォーマンスを飛躍的に向上させることが可能です。

インデックスの使用とパフォーマンス評価

Isarでは、任意のプロパティにインデックスを設定することができます。これにより、特定のプロパティでの検索やソートが高速化します。

@Collection()
class Todo {
  @Index()
  bool? isCompleted;
}

このコードでは、TodoクラスのisCompletedプロパティにインデックスを設定しています。この結果、isCompletedプロパティでのクエリは速くなります、多分。

Isarのパフォーマンスと最適化機能は、アプリケーションのレスポンス性と効率性を向上させます。インデックスの使用とタイプコンバーター、そしてオブジェクト間のリレーションシップの最適化は、大量のデータを高速に扱うことを可能にします。これらの機能はIsarを強力なデータストレージソリューションにしています。

Isarの応用例

Isarの豊富な機能と優れたパフォーマンスは、様々なアプリケーションでの利用に適しています。今回は、具体的にTodoアプリケーションの構築とIsar Database Inspectorの利用について考察していきます。

Todoアプリケーションの構築

Isarは、TodoアプリケーションのようなCRUD操作が中心となるアプリケーションの構築に非常に適しています。その理由は、Isarが提供する効率的なデータ管理と、容易なCRUD操作の実装が可能な設計にあります。

以下はIsarを使用してTodoアプリケーションを構築する一例です。

@Collection()
class Todo {
  Id id = Isar.autoIncrement;

  String? title;

  @Index()
  bool? isCompleted;
}

final todosBox = await isar.todos.open();

final todo = Todo('Buy groceries');
await todosBox.put(todo);

このコードでは、Todoリストの項目を表すTodoクラスを定義し、それをIsarのBoxに保存しています。このように、Isarを使用すれば、シンプルな操作でデータの保存と取得が可能となります。

Isar Inspectorの利用

Isar Inspectorは、Isarデータベースの内容を視覚的に確認するための強力なツールです。これにより、データベースの状態を直接視覚化して確認でき、デバッグやテストの作業を大幅に容易にします。
デバッグコンソール内にURLが書かれていますので、クリックしてください。Isar Inspectorが起動し、接続されたIsarデータベースの内容を視覚的に確認できます。

Isarの豊富な機能と、その応用例は、効率的で安全なデータ管理を可能にします。Todoアプリケーションの構築からデータベースの視覚化まで、Isarはあらゆるシチュエーションでのデータベース操作をシンプルかつ効率的に行うことを可能にします。これらの特徴は、Isarが高性能なアプリケーションを作るための強力なツールとなることを示しています。
Isar Database Inspector

Q&A

Q1: Isarとは何ですか?

IsarはNoSQLの形式を採用したフラットバファイアウト型のデータベースです。多くの開発者に支持されており、ACID準拠のトランザクション、フルテキスト検索、動的なクエリといった多機能性を誇っています。さらに、そのセットアップは依存関係の追加とアノテーションの使用を通じて行われ、CRUD操作が手軽に行えます。

Q2: Isarのパフォーマンスと最適化について教えてください。

Isarは高いパフォーマンスを発揮します。特にインデックスの使用は、クエリのパフォーマンスを大幅に改善します。さらに、タイプコンバーターとオブジェクト間のリレーションシップの取り扱いも柔軟で、これにより複雑なデータ構造を効率的に扱うことが可能となります。

Q3: Isarの具体的な応用例を教えてください。

Isarは、例えばTodoアプリケーションの構築などに利用することが可能です。加えて、Isar Database Inspectorを用いることで、データベースの状態をリアルタイムに確認することも可能です。これにより、データ管理を一層効率化することができます。

まとめ

Isarは、フラットバファイアウト型のNoSQLデータベースであり、そのパフォーマンスと効率性から、多くの開発者たちに選ばれています。便利な特徴として、ACID準拠のトランザクション、フルテキスト検索、動的クエリなどがあり、アプリケーションの必要性に合わせてカスタマイズできます。そのセットアップは、依存関係の追加とアノテーションの使用によって行われ、CRUD操作も手軽に行えます。

また、Isarは高いパフォーマンスを発揮します。特にインデックスの使用は、クエリのパフォーマンスを劇的に改善します。また、タイプコンバーターとオブジェクト間のリレーションシップの扱いも柔軟で、これにより複雑なデータ構造を効率的に扱うことができます。

具体的な応用例としては、Todoアプリケーションの構築などが考えられます。さらに、Isar Database Inspectorを利用することで、データベースの状態をリアルタイムで確認することも可能です。

これらを踏まえると、Isarは機能性と効率性を兼ね備えた優れたデータベースツールと言えます。総じて、Isarは、現代のアプリケーション開発において必要な多くの要素を提供してくれます。

  • Isarは、フラットバファイアウト型のNoSQLデータベースであり、ACID準拠のトランザクション、フルテキスト検索、動的クエリなどの便利な特徴を備えています。
  • セットアップは、依存関係の追加とアノテーションの使用によって簡単に行うことができます。
  • Isarは、インデックスの使用と、タイプコンバーターとオブジェクト間のリレーションシップを利用することで、高いパフォーマンスを発揮します。
  • Todoアプリケーションの構築やIsar Database Inspectorの利用など、具体的な応用例も多数存在します。

参考

ソース(main.dartにコピペして動作確認用)

まず、pubspec.yamlに追加するため、以下のコマンドを実行します。

flutter pub add   isar  isar_flutter_libs  path_provider
flutter pub add  isar_generator   build_runner --dev
flutter pub get

main.dartのプログラムは以下の通りです。
Todoを追加、更新、削除が行えます。

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:isar/isar.dart';
import 'package:path_provider/path_provider.dart' as path;
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'main.g.dart';

@Collection()
class Todo {
  Id id = Isar.autoIncrement;

  String? title;

  @Index()
  bool? isCompleted;
}

Future<Isar> openIsar() async {
  final directory = await path.getApplicationDocumentsDirectory();
  final isar = await Isar.openSync([TodoSchema], directory: directory.path);
  if (isar == null) {
    throw 'Could not open Isar instance.';
  }
  return isar;
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final futureIsar = openIsar();
  final _textEditingController = TextEditingController(text: 'Todo Sample 1');

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Isar Demo',
      theme: ThemeData(
        useMaterial3: true,
      ),
      home: Scaffold(
        body: FutureBuilder(
            future: futureIsar,
            builder: (context, snapshot) {
              if (snapshot.hasError) {
                return Text('error');
              } else if (!snapshot.hasData) {
                return Text('Loading');
              }
              final isar = snapshot.data!;
              final stream =
                  isar.todos.buildQuery<Todo>().watch(fireImmediately: true);

              return SafeArea(
                child: Column(
                  children: [
                    Row(
                      children: [
                        Expanded(
                            child:
                                TextField(controller: _textEditingController)),
                        FilledButton(
                          onPressed: () {
                            final todo = Todo()
                              ..title = _textEditingController.text;
                            isar.writeTxn(() => isar.todos.put(todo));
                          },
                          child: Icon(Icons.add),
                        ),
                      ],
                    ),
                    Expanded(
                      child: StreamBuilder<List<Todo>>(
                          stream: stream,
                          builder: (context, snapshot) {
                            if (snapshot.hasError) {
                              return Text('error');
                            } else if (!snapshot.hasData) {
                              return Text('Loading');
                            }

                            final list = snapshot.data!;
                            return ListView.builder(
                                itemCount: list.length,
                                itemBuilder: (context, index) {
                                  return ListTile(
                                    leading: Checkbox(
                                      value: list[index].isCompleted ?? false,
                                      onChanged: (value) {
                                        isar.writeTxn(() => isar.todos.put(
                                            list[index]..isCompleted = value));
                                      },
                                    ),
                                    title:
                                        Text(list[index].title ?? 'NO TITLE'),
                                    trailing: IconButton(
                                        onPressed: () => isar.writeTxn(() =>
                                            isar.todos.delete(list[index].id)),
                                        icon: Icon(Icons.delete)),
                                  );
                                });
                          }),
                    ),
                  ],
                ),
              );
            }),
      ),
    );
  }
}