📚

NoSQL sembast を試してみた

2024/05/09に公開

📕Overview

ローカルDBのIsarが気に入ってたのですが、最近メンテナンスがされていないようです😱
Realm, ObjectBoxを使ったことがあるのですが、⭐️が多いsembastなるものを使ってみようと思いました。5年前からあるみたい???

いや9年前か?

解説

単一プロセス IO アプリケーション向けのさらに別の NoSQL 永続ストア データベース ソリューション。ドキュメントベースのデータベース全体が 1 つのファイル内に存在し、開くとメモリにロードされます。変更はすぐにファイルに追加され、必要に応じてファイルは自動的に圧縮されます。

Dart VM および Flutter で動作します (プラグインは必要ありません。100% Dart なので、すべてのプラットフォーム (MacOS/Android/iOS/Linux/Windows) で動作します)。 IndexedDB、DataStore、WebSql、NeDB、Lawndart からインスピレーションを受けています...

ユーザー定義のコーデックを使用した暗号化をサポートします。

Pure Dart シングルファイル IO VM/Flutter ストレージをサポート。
sembast_web を介した Web サポート (Flutter Web を含む)。
sembast_sqflite を通じて sqflite 上で動作できます。
使用例: notepad_sembast: すべてのプラットフォーム (Web/モバイル/Mac) で動作するシンプルなフラッター メモ帳 (オンライン デモ)

[ブラウザで動くサンプル]
https://alextekartik.github.io/flutter_app_example/notepad_sembast/#/

🎁使用するpackageの説明

  • sembast
    • sembast db は、Simple Embedded Application Store データベースの略です
    • NoSQLのデータベース

https://pub.dev/packages/sembast

  • path_provider
    • ファイルシステム上でよく使用される場所を検索するための Flutter プラグイン.
    • Local DBと組み合わせて使われることが多い.

https://pub.dev/packages/path_provider/install

🔧プロジェクトを作って使ってみよう

  1. create flutter project
flutter create sembast_example --org com.jboy
  1. add package
flutter pub add sembast
flutter pub add path_provider

🧷summary

モデルクラス、ビジネスロジックのクラス、UIのクラスに分けて作り方を解説していきます。

JSONのデータをEncode, Decodeできるモデルクラスを作成する。

Entityを作成
// data model of sembast
class Book {
  int? id;
  final String title;
  final String author;
  
  Book({
    this.id,
    required this.title,
    required this.author,
  });

  // toMap method to convert the object to a map
  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'title': title,
      'author': author,
    };
  }

  // fromMap method to convert a map to an object
  factory Book.fromMap(Map<String, dynamic> map) {
    return Book(
      id: map['id'],
      title: map['title'],
      author: map['author'],
    );
  }
}

データの状態の保持とロジックはシングルトンで作成します。昔誰かが作ったサンプルを参考にしてます。今回は、追加・表示・削除のメソッドしか使わないです。

DTOとは?

DTO(Data Transfer Object)とは、著名なデザインパターンの一つで、関連するデータを一つにまとめ、データの格納・読み出しのためのメソッドを定義したオブジェクトのこと。 異なるプログラム間やコンピュータ間でひとまとまりのデータを受け渡す際に用いられる。

DBを操作する dto を作成
// book dto class sembast
import 'package:sembast/sembast.dart';

import 'package:sembast_example/entity/book.dart';

// Singleton class for Sembast database
class BookDB {
  // Singleton instance
  static final BookDB _singleton = BookDB._();

  // Singleton accessor
  static BookDB get instance => _singleton;

  // Database instance
  late Database _database;

  // Store reference
  final _store = intMapStoreFactory.store('book_store');

  // Private constructor
  BookDB._();

  // Initialize database
  Future<void> init(Database database) async {
    _database = database;
  }

  // Insert a new book into the database
  Future<int> insert(Book book) async {
    return await _store.add(_database, book.toMap());
  }

  // Update an existing book in the database
  Future<int> update(Book book) async {
    final finder = Finder(filter: Filter.byKey(book.id));
    return await _store.update(_database, book.toMap(), finder: finder);
  }

  // Delete a book from the database
  Future<int> delete(int id) async {
    final finder = Finder(filter: Filter.byKey(id));
    return await _store.delete(_database, finder: finder);
  }

  // Get all books from the database
  Future<List<Book>> getAllSortedByName() async {
    final finder = Finder(sortOrders: [SortOrder('title')]);
    final recordSnapshots = await _store.find(_database, finder: finder);

    return recordSnapshots.map((snapshot) {
      final book = Book.fromMap(snapshot.value);
      book.id = snapshot.key;
      return book;
    }).toList();
  }
}

アプリの画面となるUIのクラスは、非同期でローカルDBとやりとりをするので、FutureBuilderを使います。でもこれだと、一度しかデータを習得できないので、保存ボタンと削除ボタンが押されたらsetStateで、Widgetをリビルドして画面を更新します。

riverpod使った方がいいのでしょうけど今回は入門レベルなので使いません🙇

アプリのUIを作成
import 'package:flutter/material.dart';
import 'package:sembast_example/dto/book_dto.dart';
import 'package:sembast_example/entity/book.dart';

class BookList extends StatefulWidget {
  const BookList({super.key});

  
  State<BookList> createState() => _BookListState();
}

class _BookListState extends State<BookList> {
  final titleController = TextEditingController();
  final authorController = TextEditingController();

  
  void dispose() {
    titleController.dispose();
    authorController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Book List'),
      ),
      body: FutureBuilder<List<Book>>(
        future: BookDB.instance.getAllSortedByName(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.done) {
            final List<Book>? books = snapshot.data;
            return ListView.builder(
              itemCount: books?.length,
              itemBuilder: (_, index) {
                final book = books![index];
                return ListTile(
                  title: Text(book.title),
                  subtitle: Text(book.author),
                  trailing: IconButton(
                    icon: const Icon(Icons.delete),
                    onPressed: () async {
                      await BookDB.instance.delete(book.id!);
                      setState(() {
                      ScaffoldMessenger.of(context).showSnackBar(
                        SnackBar(
                          content: Text('Deleted ${book.title}'
                          ),
                          backgroundColor: Colors.redAccent,
                          ),
                      );
                      });
                    },
                  ),
                );
              },
            );
          } else {
            return const Center(child: CircularProgressIndicator());
          }
        },
      ),
      floatingActionButton: FloatingActionButton(
  child: const Icon(Icons.add),
  onPressed: () async {
    await showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: const Text('Add a new book'),
          content: Column(
            mainAxisSize: MainAxisSize.min, // Add this line
            children: <Widget>[
              TextField(
                controller: titleController,
                decoration: const InputDecoration(hintText: 'Title'),
              ),
              TextField(
                controller: authorController,
                decoration: const InputDecoration(hintText: 'Author'),
              ),
            ],
          ),
          actions: <Widget>[
            TextButton(
              child: const Text('Cancel'),
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),
            TextButton(
              child: const Text('Add'),
              onPressed: () async {
                String title = titleController.text;
                String author = authorController.text;

                // Add your book to the database here
                Book book = Book(title: title, author: author);
                await BookDB.instance.insert(book);
                setState(() {
                  titleController.clear();
                  authorController.clear();
                });
                if(context.mounted) {
                Navigator.of(context).pop();
                }
              },
            ),
          ],
        );
      },
    );
  },
),
    );
  }
}

main.dartには、Local DBと接続する処理をmain関数の中に記述する必要があります。ここでは、ファイルシステムにサクセスするコードを書かないとエラーが発生するので、path_providerのコードを書きます。これで設定は完了です。簡単な本の名前と作者を保存するアプリができました🙌

main.dartにDBの設定を追加
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sembast/sembast.dart';
import 'package:sembast_example/dto/book_dto.dart';
import 'package:sembast_example/view/book_list.dart';

import 'package:sembast/sembast_io.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Get the application documents directory
  final appDocumentDir = await getApplicationDocumentsDirectory();

  // Specify the location of the database file
  final dbPath = appDocumentDir.path + 'book.db';

  // Initialize your database
  DatabaseFactory dbFactory = databaseFactoryIo;
  Database db = await dbFactory.openDatabase(dbPath);

  // Initialize your singleton class with the database
  BookDB.instance.init(db);

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const BookList(),
    );
  }
}

[動作はこんな感じです]


🧑‍🎓thoughts

Local DBは個人開発でしか使ったことないのですが、最近副業で必要そうな場面が出てきたのでプロダクションで考えるとなると、LIKE数も大事だけどメンテナンスが継続されているDartのpackageが良いだろうと考えて、sembastを選択しました。
この記事がどなたかの参考になると嬉しいです💙💚

こちらが完成品です

Discussion