Sembast
Sembastは「Simple Embedded Application Store」の略称で、DartおよびFlutter向けの軽量でパワフルなNoSQLデータベースライブラリです。単一プロセスアプリケーション向けに設計された永続的なNoSQLストアで、ドキュメントベースのデータベース全体が単一ファイルに格納され、オープン時にメモリに読み込まれます。プラグイン不要でDart純正実装のため、すべてのプラットフォーム(Android/iOS/macOS/Linux/Windows/Web)で動作し、暗号化サポート、高性能な読み書き、直感的なAPIを提供する現代的なモバイル・デスクトップアプリ開発に最適なデータベースソリューションです。
GitHub概要
トピックス
スター履歴
ライブラリ
Sembast
概要
Sembastは「Simple Embedded Application Store」の略称で、DartおよびFlutter向けの軽量でパワフルなNoSQLデータベースライブラリです。単一プロセスアプリケーション向けに設計された永続的なNoSQLストアで、ドキュメントベースのデータベース全体が単一ファイルに格納され、オープン時にメモリに読み込まれます。プラグイン不要でDart純正実装のため、すべてのプラットフォーム(Android/iOS/macOS/Linux/Windows/Web)で動作し、暗号化サポート、高性能な読み書き、直感的なAPIを提供する現代的なモバイル・デスクトップアプリ開発に最適なデータベースソリューションです。
詳細
Sembast 2025年版は、Flutterエコシステムで最も信頼されるローカルデータベースソリューションの一つとして確固たる地位を築いています。JSONライクなドキュメント指向データベースとして、非正規化データを効率的に処理し、複雑なクエリ、インデックス、トランザクション機能を提供。BLoCパターンとの統合により、状態管理との連携も容易です。ファイルベースの単純な構造でありながら、ACID準拠のトランザクション、リアクティブAPI、大容量データ処理をサポートし、小中規模アプリから本格的なプロダクションアプリまで幅広く対応します。
主な特徴
- 完全クロスプラットフォーム: すべてのFlutter対応プラットフォームで動作
- 暗号化サポート: ユーザー定義コーデックによる強力な暗号化
- 高性能: メモリベース処理と自動ファイル最適化
- シンプルAPI: 直感的で学習コストの低いインターフェース
- 非正規化データ対応: JSONベースの柔軟なデータ構造
- リアクティブプログラミング: ストリームベースのリアルタイム更新
メリット・デメリット
メリット
- プラグイン不要で100% Dartコード、全プラットフォーム対応による開発効率向上
- セットアップが極めて簡単で、ボイラープレートコードが不要
- 軽量設計で小中規模アプリに最適、高速な読み書き性能
- 強力な暗号化機能により機密データを安全に保存可能
- ファイルベースのシンプル構造でデバッグとデータ確認が容易
- BLoCなどの状態管理ライブラリとの優れた統合性
デメリット
- 単一プロセス向け設計で、大規模なマルチユーザーアプリには不向き
- SQLベースのRDBMSと比較して複雑なリレーショナルクエリが困難
- メモリベース処理のため、超大容量データには制約がある
- トランザクション機能は提供されるが、分散データベースほど堅牢ではない
- 他のデータベースシステムとの相互運用性が限定的
- エンタープライズレベルの高度な機能(レプリケーション等)は未対応
参考ページ
書き方の例
セットアップとインストール
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
sembast: ^3.4.9
path_provider: ^2.1.1 # アプリディレクトリ取得用
path: ^1.8.3
dev_dependencies:
flutter_test:
sdk: flutter
// パッケージのインポート
import 'package:sembast/sembast.dart';
import 'package:sembast/sembast_io.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart';
基本的なデータベース操作
import 'package:sembast/sembast.dart';
import 'package:sembast/sembast_io.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart';
class DatabaseService {
static Database? _database;
static final _userStore = intMapStoreFactory.store('users');
static final _postStore = intMapStoreFactory.store('posts');
// データベースインスタンス取得
static Future<Database> get database async {
if (_database != null) return _database!;
// アプリディレクトリの取得
final appDocumentDir = await getApplicationDocumentsDirectory();
final dbPath = join(appDocumentDir.path, 'my_app.db');
// データベースを開く
_database = await databaseFactoryIo.openDatabase(dbPath);
return _database!;
}
// データベースクローズ
static Future<void> close() async {
if (_database != null) {
await _database!.close();
_database = null;
}
}
// レコード追加
static Future<int> addUser(Map<String, dynamic> userData) async {
final db = await database;
return await _userStore.add(db, userData);
}
// 全ユーザー取得
static Future<List<Map<String, dynamic>>> getAllUsers() async {
final db = await database;
final records = await _userStore.find(db);
return records.map((record) => {
'id': record.key,
...record.value,
}).toList();
}
// ユーザー検索
static Future<Map<String, dynamic>?> getUserById(int id) async {
final db = await database;
final record = await _userStore.record(id).get(db);
if (record != null) {
return {'id': id, ...record};
}
return null;
}
// ユーザー更新
static Future<void> updateUser(int id, Map<String, dynamic> userData) async {
final db = await database;
await _userStore.record(id).update(db, userData);
}
// ユーザー削除
static Future<void> deleteUser(int id) async {
final db = await database;
await _userStore.record(id).delete(db);
}
}
// 使用例
void main() async {
// ユーザー追加
final userId = await DatabaseService.addUser({
'name': '田中太郎',
'email': '[email protected]',
'age': 30,
'createdAt': DateTime.now().toIso8601String(),
});
print('追加されたユーザーID: $userId');
// 全ユーザー取得
final users = await DatabaseService.getAllUsers();
print('全ユーザー: $users');
// ユーザー更新
await DatabaseService.updateUser(userId, {
'name': '田中太郎(更新済み)',
'age': 31,
'updatedAt': DateTime.now().toIso8601String(),
});
// データベースクローズ
await DatabaseService.close();
}
フィルタリングとクエリ操作
import 'package:sembast/sembast.dart';
class AdvancedQueryService {
static final _userStore = intMapStoreFactory.store('users');
static final _productStore = intMapStoreFactory.store('products');
// 条件付き検索
static Future<List<Map<String, dynamic>>> getUsersByAge(int minAge, int maxAge) async {
final db = await DatabaseService.database;
// 年齢による範囲フィルタ
final finder = Finder(
filter: Filter.and([
Filter.greaterThanOrEquals('age', minAge),
Filter.lessThanOrEquals('age', maxAge),
]),
sortOrders: [SortOrder('age')],
);
final records = await _userStore.find(db, finder: finder);
return records.map((record) => {
'id': record.key,
...record.value,
}).toList();
}
// 複雑なフィルタリング
static Future<List<Map<String, dynamic>>> searchUsers({
String? namePattern,
String? emailDomain,
bool? isActive,
int? limit,
int? offset,
}) async {
final db = await DatabaseService.database;
List<Filter> filters = [];
// 名前の部分一致
if (namePattern != null) {
filters.add(Filter.matches('name', namePattern, anyInList: true));
}
// メールドメインフィルタ
if (emailDomain != null) {
filters.add(Filter.matches('email', '.*@$emailDomain'));
}
// アクティブユーザーフィルタ
if (isActive != null) {
filters.add(Filter.equals('isActive', isActive));
}
final finder = Finder(
filter: filters.isNotEmpty ? Filter.and(filters) : null,
sortOrders: [SortOrder('createdAt', false)], // 新しい順
limit: limit,
offset: offset,
);
final records = await _userStore.find(db, finder: finder);
return records.map((record) => {
'id': record.key,
...record.value,
}).toList();
}
// カスタムクエリ
static Future<Map<String, dynamic>> getUserStats() async {
final db = await DatabaseService.database;
// 全ユーザー数
final totalUsers = await _userStore.count(db);
// アクティブユーザー数
final activeUsers = await _userStore.count(
db,
filter: Filter.equals('isActive', true),
);
// 年齢層統計
final youngUsers = await _userStore.count(
db,
filter: Filter.lessThan('age', 30),
);
final middleAgedUsers = await _userStore.count(
db,
filter: Filter.and([
Filter.greaterThanOrEquals('age', 30),
Filter.lessThan('age', 50),
]),
);
final seniorUsers = await _userStore.count(
db,
filter: Filter.greaterThanOrEquals('age', 50),
);
return {
'totalUsers': totalUsers,
'activeUsers': activeUsers,
'inactiveUsers': totalUsers - activeUsers,
'ageDistribution': {
'young': youngUsers, // 30歳未満
'middleAged': middleAgedUsers, // 30-49歳
'senior': seniorUsers, // 50歳以上
},
};
}
// ページネーション
static Future<Map<String, dynamic>> getUsersPage({
int page = 1,
int itemsPerPage = 20,
String? sortBy = 'createdAt',
bool ascending = false,
}) async {
final db = await DatabaseService.database;
final offset = (page - 1) * itemsPerPage;
final finder = Finder(
sortOrders: [SortOrder(sortBy!, ascending)],
limit: itemsPerPage,
offset: offset,
);
final records = await _userStore.find(db, finder: finder);
final totalCount = await _userStore.count(db);
return {
'data': records.map((record) => {
'id': record.key,
...record.value,
}).toList(),
'pagination': {
'currentPage': page,
'itemsPerPage': itemsPerPage,
'totalItems': totalCount,
'totalPages': (totalCount / itemsPerPage).ceil(),
'hasNextPage': offset + itemsPerPage < totalCount,
'hasPreviousPage': page > 1,
},
};
}
}
トランザクションとバッチ操作
import 'package:sembast/sembast.dart';
class TransactionService {
static final _userStore = intMapStoreFactory.store('users');
static final _orderStore = intMapStoreFactory.store('orders');
static final _productStore = intMapStoreFactory.store('products');
// 基本的なトランザクション
static Future<void> transferUserData(
int fromUserId,
int toUserId,
Map<String, dynamic> transferData,
) async {
final db = await DatabaseService.database;
await db.transaction((txn) async {
// fromUserからデータを読み取り
final fromUser = await _userStore.record(fromUserId).get(txn);
if (fromUser == null) {
throw Exception('転送元ユーザーが見つかりません');
}
// toUserからデータを読み取り
final toUser = await _userStore.record(toUserId).get(txn);
if (toUser == null) {
throw Exception('転送先ユーザーが見つかりません');
}
// fromUserからデータを削除/更新
final updatedFromUser = Map<String, dynamic>.from(fromUser);
transferData.forEach((key, value) {
if (updatedFromUser.containsKey(key)) {
updatedFromUser.remove(key);
}
});
// toUserにデータを追加/更新
final updatedToUser = Map<String, dynamic>.from(toUser);
updatedToUser.addAll(transferData);
updatedToUser['updatedAt'] = DateTime.now().toIso8601String();
// 両方のユーザーを更新
await _userStore.record(fromUserId).put(txn, updatedFromUser);
await _userStore.record(toUserId).put(txn, updatedToUser);
print('データ転送が正常に完了しました');
});
}
// 注文処理トランザクション
static Future<int> createOrderWithItems(
int userId,
List<Map<String, dynamic>> orderItems,
) async {
final db = await DatabaseService.database;
return await db.transaction<int>((txn) async {
// ユーザーの存在確認
final user = await _userStore.record(userId).get(txn);
if (user == null) {
throw Exception('ユーザーが見つかりません');
}
// 商品在庫チェック
for (final item in orderItems) {
final productId = item['productId'] as int;
final quantity = item['quantity'] as int;
final product = await _productStore.record(productId).get(txn);
if (product == null) {
throw Exception('商品ID $productId が見つかりません');
}
final currentStock = product['stock'] as int;
if (currentStock < quantity) {
throw Exception('商品ID $productId の在庫が不足しています');
}
}
// 注文作成
final orderId = await _orderStore.add(txn, {
'userId': userId,
'items': orderItems,
'totalAmount': _calculateTotal(orderItems),
'status': 'pending',
'createdAt': DateTime.now().toIso8601String(),
});
// 在庫減算
for (final item in orderItems) {
final productId = item['productId'] as int;
final quantity = item['quantity'] as int;
final product = await _productStore.record(productId).get(txn);
final updatedProduct = Map<String, dynamic>.from(product!);
updatedProduct['stock'] = (updatedProduct['stock'] as int) - quantity;
updatedProduct['updatedAt'] = DateTime.now().toIso8601String();
await _productStore.record(productId).put(txn, updatedProduct);
}
print('注文 $orderId が正常に作成されました');
return orderId;
});
}
// バッチ操作
static Future<void> batchUpdateUsers(
List<Map<String, dynamic>> updates,
) async {
final db = await DatabaseService.database;
await db.transaction((txn) async {
for (final update in updates) {
final userId = update['id'] as int;
final userData = Map<String, dynamic>.from(update);
userData.remove('id'); // IDは除外
userData['updatedAt'] = DateTime.now().toIso8601String();
await _userStore.record(userId).update(txn, userData);
}
print('${updates.length}件のユーザーが一括更新されました');
});
}
// データ削除とクリーンアップ
static Future<void> cleanupOldData(Duration maxAge) async {
final db = await DatabaseService.database;
await db.transaction((txn) async {
final cutoffDate = DateTime.now().subtract(maxAge);
final cutoffString = cutoffDate.toIso8601String();
// 古いユーザーを削除
final oldUserFinder = Finder(
filter: Filter.lessThan('createdAt', cutoffString),
);
final deletedUserCount = await _userStore.delete(txn, finder: oldUserFinder);
// 古い注文を削除
final oldOrderFinder = Finder(
filter: Filter.and([
Filter.lessThan('createdAt', cutoffString),
Filter.equals('status', 'completed'),
]),
);
final deletedOrderCount = await _orderStore.delete(txn, finder: oldOrderFinder);
print('クリーンアップ完了: ユーザー $deletedUserCount 件、注文 $deletedOrderCount 件を削除');
});
}
static double _calculateTotal(List<Map<String, dynamic>> items) {
return items.fold(0.0, (total, item) {
final price = item['price'] as double;
final quantity = item['quantity'] as int;
return total + (price * quantity);
});
}
}
暗号化とセキュリティ
import 'package:sembast/sembast.dart';
import 'package:sembast/sembast_io.dart';
import 'package:crypto/crypto.dart';
import 'dart:convert';
import 'dart:typed_data';
// カスタム暗号化コーデック
class EncryptionCodec extends SembastCodec {
final String _password;
EncryptionCodec(this._password);
@override
String get signature => 'encrypt_v1';
@override
Uint8List encode(Object? input) {
if (input == null) return Uint8List(0);
// データをJSONエンコード
final jsonString = jsonEncode(input);
final bytes = utf8.encode(jsonString);
// 簡単な暗号化(本番では適切な暗号化ライブラリを使用)
final key = sha256.convert(utf8.encode(_password)).bytes;
final encrypted = _xorEncrypt(bytes, key);
return Uint8List.fromList(encrypted);
}
@override
Object? decode(Uint8List encoded) {
if (encoded.isEmpty) return null;
// 復号化
final key = sha256.convert(utf8.encode(_password)).bytes;
final decrypted = _xorEncrypt(encoded, key);
// JSONデコード
final jsonString = utf8.decode(decrypted);
return jsonDecode(jsonString);
}
List<int> _xorEncrypt(List<int> data, List<int> key) {
final result = <int>[];
for (int i = 0; i < data.length; i++) {
result.add(data[i] ^ key[i % key.length]);
}
return result;
}
}
class SecureDatabaseService {
static Database? _database;
static final _userStore = intMapStoreFactory.store('users');
static final _sensitiveStore = intMapStoreFactory.store('sensitive_data');
// 暗号化データベースの初期化
static Future<Database> initializeSecureDatabase(String password) async {
if (_database != null) return _database!;
final appDocumentDir = await getApplicationDocumentsDirectory();
final dbPath = join(appDocumentDir.path, 'secure_app.db');
// 暗号化コーデックを作成
final codec = EncryptionCodec(password);
// 暗号化データベースを開く
_database = await databaseFactoryIo.openDatabase(
dbPath,
codec: codec,
);
return _database!;
}
// 機密データの保存
static Future<int> storeSensitiveData(Map<String, dynamic> data) async {
final db = await _database!;
// タイムスタンプ付きでデータを保存
final secureData = {
...data,
'encryptedAt': DateTime.now().toIso8601String(),
'hash': _generateDataHash(data),
};
return await _sensitiveStore.add(db, secureData);
}
// 機密データの取得と検証
static Future<Map<String, dynamic>?> getSensitiveData(int id) async {
final db = await _database!;
final record = await _sensitiveStore.record(id).get(db);
if (record == null) return null;
// データ整合性の検証
final storedHash = record['hash'] as String;
final dataWithoutHash = Map<String, dynamic>.from(record);
dataWithoutHash.remove('hash');
dataWithoutHash.remove('encryptedAt');
final calculatedHash = _generateDataHash(dataWithoutHash);
if (storedHash != calculatedHash) {
throw Exception('データが改ざんされている可能性があります');
}
return {
'id': id,
...record,
};
}
static String _generateDataHash(Map<String, dynamic> data) {
final jsonString = jsonEncode(data);
final bytes = utf8.encode(jsonString);
final digest = sha256.convert(bytes);
return digest.toString();
}
// セキュアなデータバックアップ
static Future<void> createSecureBackup(String backupPassword) async {
final db = await _database!;
final appDocumentDir = await getApplicationDocumentsDirectory();
final backupPath = join(appDocumentDir.path, 'backup_${DateTime.now().millisecondsSinceEpoch}.db');
// バックアップ用の新しいコーデック
final backupCodec = EncryptionCodec(backupPassword);
// バックアップデータベースを作成
final backupDb = await databaseFactoryIo.openDatabase(
backupPath,
codec: backupCodec,
);
try {
// 全データのエクスポート
final users = await _userStore.find(db);
final sensitiveData = await _sensitiveStore.find(db);
final backupUserStore = intMapStoreFactory.store('users');
final backupSensitiveStore = intMapStoreFactory.store('sensitive_data');
// バックアップデータベースにデータを復元
await backupDb.transaction((txn) async {
for (final record in users) {
await backupUserStore.record(record.key).put(txn, record.value);
}
for (final record in sensitiveData) {
await backupSensitiveStore.record(record.key).put(txn, record.value);
}
});
print('セキュアバックアップが作成されました: $backupPath');
} finally {
await backupDb.close();
}
}
}
// 使用例
void main() async {
try {
// セキュアデータベースの初期化
await SecureDatabaseService.initializeSecureDatabase('my_secret_password_123');
// 機密データの保存
final sensitiveId = await SecureDatabaseService.storeSensitiveData({
'creditCardNumber': '1234-5678-9012-3456',
'expiryDate': '12/25',
'holderName': '田中太郎',
'notes': '機密情報',
});
// 機密データの取得
final retrievedData = await SecureDatabaseService.getSensitiveData(sensitiveId);
print('取得データ: $retrievedData');
// セキュアバックアップの作成
await SecureDatabaseService.createSecureBackup('backup_password_456');
} catch (e) {
print('エラー: $e');
}
}
リアクティブプログラミングとストリーム
import 'package:sembast/sembast.dart';
import 'dart:async';
class ReactiveDataService {
static final _userStore = intMapStoreFactory.store('users');
static final _chatStore = intMapStoreFactory.store('chat_messages');
// ユーザー変更の監視
static Stream<List<Map<String, dynamic>>> watchUsers() async* {
final db = await DatabaseService.database;
await for (final snapshot in _userStore.query().onSnapshots(db)) {
final users = snapshot.map((record) => {
'id': record.key,
...record.value,
}).toList();
yield users;
}
}
// 特定条件のユーザー監視
static Stream<List<Map<String, dynamic>>> watchActiveUsers() async* {
final db = await DatabaseService.database;
final query = _userStore.query(
finder: Finder(
filter: Filter.equals('isActive', true),
sortOrders: [SortOrder('lastLoginAt', false)],
),
);
await for (final snapshot in query.onSnapshots(db)) {
final activeUsers = snapshot.map((record) => {
'id': record.key,
...record.value,
}).toList();
yield activeUsers;
}
}
// チャットメッセージのリアルタイム監視
static Stream<List<Map<String, dynamic>>> watchChatMessages(int chatRoomId) async* {
final db = await DatabaseService.database;
final query = _chatStore.query(
finder: Finder(
filter: Filter.equals('chatRoomId', chatRoomId),
sortOrders: [SortOrder('timestamp')],
),
);
await for (final snapshot in query.onSnapshots(db)) {
final messages = snapshot.map((record) => {
'id': record.key,
...record.value,
}).toList();
yield messages;
}
}
// カウント変更の監視
static Stream<int> watchUserCount() async* {
final db = await DatabaseService.database;
await for (final _ in _userStore.query().onSnapshots(db)) {
final count = await _userStore.count(db);
yield count;
}
}
// 複合条件での監視
static Stream<Map<String, dynamic>> watchUserStatistics() async* {
final db = await DatabaseService.database;
await for (final _ in _userStore.query().onSnapshots(db)) {
final totalCount = await _userStore.count(db);
final activeCount = await _userStore.count(
db,
filter: Filter.equals('isActive', true),
);
final newUsersToday = await _userStore.count(
db,
filter: Filter.greaterThan(
'createdAt',
DateTime.now().subtract(Duration(days: 1)).toIso8601String(),
),
);
yield {
'totalUsers': totalCount,
'activeUsers': activeCount,
'inactiveUsers': totalCount - activeCount,
'newUsersToday': newUsersToday,
'lastUpdated': DateTime.now().toIso8601String(),
};
}
}
// メッセージ送信とリアルタイム更新
static Future<void> sendChatMessage(
int chatRoomId,
int senderId,
String message,
) async {
final db = await DatabaseService.database;
await _chatStore.add(db, {
'chatRoomId': chatRoomId,
'senderId': senderId,
'message': message,
'timestamp': DateTime.now().toIso8601String(),
'isRead': false,
});
// この追加により、watchChatMessages()を監視している
// 全てのリスナーに自動的に新しいメッセージが通知される
}
// 一括操作とリアルタイム更新
static Future<void> markMessagesAsRead(
int chatRoomId,
int userId,
) async {
final db = await DatabaseService.database;
await db.transaction((txn) async {
final finder = Finder(
filter: Filter.and([
Filter.equals('chatRoomId', chatRoomId),
Filter.notEquals('senderId', userId),
Filter.equals('isRead', false),
]),
);
await _chatStore.update(
txn,
{'isRead': true, 'readAt': DateTime.now().toIso8601String()},
finder: finder,
);
});
// この更新により、リアルタイム監視が自動的にトリガーされる
}
}
// Flutter Widget での使用例
class UserListWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StreamBuilder<List<Map<String, dynamic>>>(
stream: ReactiveDataService.watchUsers(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('エラー: ${snapshot.error}');
}
final users = snapshot.data ?? [];
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
title: Text(user['name'] ?? ''),
subtitle: Text(user['email'] ?? ''),
trailing: user['isActive'] == true
? Icon(Icons.circle, color: Colors.green, size: 12)
: Icon(Icons.circle, color: Colors.grey, size: 12),
);
},
);
},
);
}
}
// チャットUI例
class ChatWidget extends StatelessWidget {
final int chatRoomId;
const ChatWidget({Key? key, required this.chatRoomId}) : super(key: key);
@override
Widget build(BuildContext context) {
return StreamBuilder<List<Map<String, dynamic>>>(
stream: ReactiveDataService.watchChatMessages(chatRoomId),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
}
final messages = snapshot.data!;
return ListView.builder(
itemCount: messages.length,
itemBuilder: (context, index) {
final message = messages[index];
return ListTile(
title: Text(message['message'] ?? ''),
subtitle: Text(message['timestamp'] ?? ''),
leading: CircleAvatar(
child: Text('${message['senderId']}'),
),
trailing: message['isRead'] == true
? Icon(Icons.done_all, color: Colors.blue)
: Icon(Icons.done, color: Colors.grey),
);
},
);
},
);
}
}