Isar
Isarは「Flutter/Dart向けの超高速NoSQLデータベース」として開発された、ネイティブパフォーマンスを誇るクロスプラットフォーム対応データベースです。C++で書かれたコアエンジンにより、SQLiteを上回る圧倒的な読み書き性能を実現し、Dart FFI (Foreign Function Interface) を通じてネイティブコードと直接通信します。型安全で直感的なクエリAPI、強力なインデックス機能、自動スキーママイグレーション、リアクティブなストリーム監視により、現代的なFlutterアプリケーション開発における高性能ローカルストレージソリューションの新しい標準を確立しています。
GitHub概要
isar/isar
Extremely fast, easy to use, and fully async NoSQL database for Flutter
トピックス
スター履歴
ライブラリ
Isar
概要
Isarは「Flutter/Dart向けの超高速NoSQLデータベース」として開発された、ネイティブパフォーマンスを誇るクロスプラットフォーム対応データベースです。C++で書かれたコアエンジンにより、SQLiteを上回る圧倒的な読み書き性能を実現し、Dart FFI (Foreign Function Interface) を通じてネイティブコードと直接通信します。型安全で直感的なクエリAPI、強力なインデックス機能、自動スキーママイグレーション、リアクティブなストリーム監視により、現代的なFlutterアプリケーション開発における高性能ローカルストレージソリューションの新しい標準を確立しています。
詳細
Isar 2025年版は、Flutter 3.16以降とDart 3.0に完全対応し、null safetyとソリッドな型システムの恩恵を最大限活用した次世代データベースエンジンです。高度なインデックス戦略により、複雑なクエリも超高速で実行でき、複合インデックス、部分インデックス、全文検索インデックスを使い分けてパフォーマンスを最適化できます。暗号化機能、バックアップ・復元、スキーママイグレーション、リンクとバックリンクによる柔軟なリレーションシップ管理、そして強力なクエリビルダーにより、エンタープライズレベルのモバイルアプリケーション開発に必要な全機能を提供します。
主な特徴
- 超高速パフォーマンス: C++コアによりSQLiteより最大10倍高速
- 強力なインデックス: 複合、部分、全文検索インデックスサポート
- 型安全クエリ: コンパイル時型チェックによる安全なデータアクセス
- リアクティブストリーム: データ変更の自動監視とUI更新
- スキーママイグレーション: 自動・手動両方のマイグレーション対応
- 暗号化対応: AES-256による暗号化とセキュアなデータ保存
メリット・デメリット
メリット
- SQLiteを大幅に上回る圧倒的な読み書き性能
- Dart/Flutterエコシステムとの完璧な統合と型安全性
- 強力なインデックス機能による複雑なクエリの高速実行
- リアクティブプログラミングとの優れた親和性
- 自動スキーママイグレーションによる開発効率向上
- 豊富なクエリ機能とフルテキスト検索対応
デメリット
- 比較的新しいライブラリで実績がまだ限定的
- Rust/C++に依存し、デバッグが困難な場合がある
- ドキュメントとコミュニティがHiveより少ない
- 複雑なリレーションシップの表現にはコツが必要
- バイナリサイズがやや大きく、軽量アプリには不向き
- プラットフォーム固有のコンパイルが必要
参考ページ
書き方の例
セットアップ
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
isar: ^3.1.0+1
isar_flutter_libs: ^3.1.0+1
dev_dependencies:
isar_generator: ^3.1.0+1
build_runner: ^2.4.9
// main.dart - 初期化
import 'package:flutter/material.dart';
import 'package:isar/isar.dart';
import 'package:isar_flutter_libs/isar_flutter_libs.dart';
import 'package:path_provider/path_provider.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// Isarの初期化
final dir = await getApplicationDocumentsDirectory();
final isar = await Isar.open(
[UserSchema, PostSchema, CategorySchema],
directory: dir.path,
);
runApp(MyApp(isar: isar));
}
class MyApp extends StatelessWidget {
final Isar isar;
const MyApp({super.key, required this.isar});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Isar Demo',
home: HomePage(isar: isar),
);
}
}
基本的な使い方
// コレクションの定義
import 'package:isar/isar.dart';
part 'user.g.dart'; // 生成されるファイル
@collection
class User {
Id id = Isar.autoIncrement;
@Index()
late String name;
@Index(unique: true)
late String email;
late int age;
late DateTime createdAt;
@Index()
late bool isActive;
List<String> hobbies = [];
@ignore
String? temporaryData; // データベースに保存されない
// リンク(リレーションシップ)
final posts = IsarLinks<Post>();
final profile = IsarLink<UserProfile>();
// 計算プロパティ
@Index()
String get nameEmail => '$name $email';
// オブジェクトコンストラクタ
User({
required this.name,
required this.email,
required this.age,
DateTime? createdAt,
this.isActive = true,
this.hobbies = const [],
}) : createdAt = createdAt ?? DateTime.now();
}
@collection
class Post {
Id id = Isar.autoIncrement;
@Index()
late String title;
late String content;
@Index()
late DateTime publishedAt;
@Index()
late bool isPublished;
List<String> tags = [];
@Index()
late int categoryId;
// バックリンク
@Backlink(to: 'posts')
final author = IsarLink<User>();
}
@collection
class Category {
Id id = Isar.autoIncrement;
@Index(unique: true)
late String name;
late String description;
late DateTime createdAt;
}
@collection
class UserProfile {
Id id = Isar.autoIncrement;
late String bio;
String? website;
String? location;
DateTime? birthDate;
// バックリンク
@Backlink(to: 'profile')
final user = IsarLink<User>();
}
// アダプター生成コマンド:
// dart run build_runner build
データ操作
// データベース操作クラス
class UserService {
final Isar isar;
UserService(this.isar);
// ユーザー作成
Future<User> createUser({
required String name,
required String email,
required int age,
List<String> hobbies = const [],
}) async {
final user = User(
name: name,
email: email,
age: age,
hobbies: hobbies,
);
await isar.writeTxn(() async {
await isar.users.put(user);
});
return user;
}
// ユーザー更新
Future<void> updateUser(User user) async {
await isar.writeTxn(() async {
await isar.users.put(user);
});
}
// ユーザー削除
Future<bool> deleteUser(int id) async {
return await isar.writeTxn(() async {
return await isar.users.delete(id);
});
}
// 単一ユーザー取得
Future<User?> getUser(int id) async {
return await isar.users.get(id);
}
// 全ユーザー取得
Future<List<User>> getAllUsers() async {
return await isar.users.where().findAll();
}
// メールでユーザー検索
Future<User?> getUserByEmail(String email) async {
return await isar.users.filter().emailEqualTo(email).findFirst();
}
// 名前で検索(部分一致)
Future<List<User>> searchUsersByName(String nameQuery) async {
return await isar.users
.filter()
.nameContains(nameQuery, caseSensitive: false)
.findAll();
}
// 年齢範囲でフィルタ
Future<List<User>> getUsersByAgeRange(int minAge, int maxAge) async {
return await isar.users
.filter()
.ageBetween(minAge, maxAge)
.findAll();
}
// アクティブユーザーのみ取得
Future<List<User>> getActiveUsers() async {
return await isar.users
.filter()
.isActiveEqualTo(true)
.sortByCreatedAtDesc()
.findAll();
}
// 複雑なクエリ例
Future<List<User>> getActiveAdultUsersWithHobbies() async {
return await isar.users
.filter()
.isActiveEqualTo(true)
.and()
.ageGreaterThan(18)
.and()
.hobbiesIsNotEmpty()
.sortByName()
.findAll();
}
// ページネーション
Future<List<User>> getUsersPaged(int offset, int limit) async {
return await isar.users
.where()
.offset(offset)
.limit(limit)
.findAll();
}
// カウント取得
Future<int> getUserCount() async {
return await isar.users.count();
}
Future<int> getActiveUserCount() async {
return await isar.users.filter().isActiveEqualTo(true).count();
}
}
// 使用例
void demonstrateBasicOperations() async {
final userService = UserService(isar);
// ユーザー作成
final user1 = await userService.createUser(
name: '田中太郎',
email: '[email protected]',
age: 30,
hobbies: ['読書', 'プログラミング', 'ゲーム'],
);
print('Created user: ${user1.id}');
// 複数ユーザー作成
final users = [
('佐藤花子', '[email protected]', 25, ['料理', '旅行']),
('鈴木次郎', '[email protected]', 35, ['スポーツ', '音楽']),
('高橋美咲', '[email protected]', 28, ['写真', 'アート']),
];
for (final (name, email, age, hobbies) in users) {
await userService.createUser(
name: name,
email: email,
age: age,
hobbies: hobbies,
);
}
// データ取得
final allUsers = await userService.getAllUsers();
print('Total users: ${allUsers.length}');
// 検索
final searchResults = await userService.searchUsersByName('田中');
print('Search results: ${searchResults.length}');
// 年齢フィルタ
final youngAdults = await userService.getUsersByAgeRange(20, 30);
print('Young adults: ${youngAdults.length}');
}
リンクとリレーションシップ
// リレーションシップ操作サービス
class RelationshipService {
final Isar isar;
RelationshipService(this.isar);
// ユーザーにプロフィールを追加
Future<void> createUserProfile({
required User user,
required String bio,
String? website,
String? location,
DateTime? birthDate,
}) async {
final profile = UserProfile()
..bio = bio
..website = website
..location = location
..birthDate = birthDate;
await isar.writeTxn(() async {
await isar.userProfiles.put(profile);
user.profile.value = profile;
await user.profile.save();
});
}
// ユーザーの投稿を作成
Future<Post> createUserPost({
required User user,
required String title,
required String content,
List<String> tags = const [],
required int categoryId,
bool isPublished = false,
}) async {
final post = Post()
..title = title
..content = content
..tags = tags
..categoryId = categoryId
..publishedAt = DateTime.now()
..isPublished = isPublished;
await isar.writeTxn(() async {
await isar.posts.put(post);
user.posts.add(post);
await user.posts.save();
});
return post;
}
// ユーザーの投稿を取得(リレーション込み)
Future<User?> getUserWithPosts(int userId) async {
final user = await isar.users.get(userId);
if (user != null) {
await user.posts.load();
await user.profile.load();
}
return user;
}
// 投稿の作者を取得
Future<Post?> getPostWithAuthor(int postId) async {
final post = await isar.posts.get(postId);
if (post != null) {
await post.author.load();
}
return post;
}
// ユーザーの投稿をカテゴリ別に取得
Future<List<Post>> getUserPostsByCategory(int userId, int categoryId) async {
final user = await isar.users.get(userId);
if (user == null) return [];
await user.posts.load();
return user.posts.where((post) => post.categoryId == categoryId).toList();
}
// 公開投稿のみを取得
Future<List<Post>> getPublishedPosts({
int? limit,
int? offset,
}) async {
var query = isar.posts
.filter()
.isPublishedEqualTo(true)
.sortByPublishedAtDesc();
if (offset != null) {
query = query.offset(offset);
}
if (limit != null) {
query = query.limit(limit);
}
final posts = await query.findAll();
// 各投稿の作者情報を読み込み
for (final post in posts) {
await post.author.load();
}
return posts;
}
// 複雑なリレーションクエリ
Future<List<User>> getActiveUsersWithRecentPosts(int days) async {
final cutoffDate = DateTime.now().subtract(Duration(days: days));
return await isar.users
.filter()
.isActiveEqualTo(true)
.and()
.posts((q) => q
.isPublishedEqualTo(true)
.and()
.publishedAtGreaterThan(cutoffDate))
.findAll();
}
// ユーザー統計取得
Future<Map<String, dynamic>> getUserStats(int userId) async {
final user = await isar.users.get(userId);
if (user == null) return {};
await user.posts.load();
final totalPosts = user.posts.length;
final publishedPosts = user.posts.where((p) => p.isPublished).length;
final recentPosts = user.posts
.where((p) => p.publishedAt.isAfter(
DateTime.now().subtract(const Duration(days: 30))))
.length;
return {
'totalPosts': totalPosts,
'publishedPosts': publishedPosts,
'draftPosts': totalPosts - publishedPosts,
'recentPosts': recentPosts,
'joinDate': user.createdAt,
'hasProfile': user.profile.value != null,
};
}
}
リアクティブクエリとストリーム
// リアクティブUIの実装
class UserListWidget extends StatelessWidget {
final Isar isar;
const UserListWidget({super.key, required this.isar});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('User List')),
body: StreamBuilder<List<User>>(
stream: isar.users
.filter()
.isActiveEqualTo(true)
.watch(fireImmediately: true),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
final users = snapshot.data ?? [];
if (users.isEmpty) {
return const Center(child: Text('No active users found'));
}
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return UserTile(user: user, isar: isar);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddUserDialog(context),
child: const Icon(Icons.add),
),
);
}
void _showAddUserDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AddUserDialog(isar: isar),
);
}
}
// 個別ユーザータイル
class UserTile extends StatelessWidget {
final User user;
final Isar isar;
const UserTile({super.key, required this.user, required this.isar});
@override
Widget build(BuildContext context) {
return StreamBuilder<User?>(
stream: isar.users.watchObject(user.id, fireImmediately: true),
builder: (context, snapshot) {
final currentUser = snapshot.data ?? user;
return ListTile(
title: Text(currentUser.name),
subtitle: Text('${currentUser.age}歳 - ${currentUser.email}'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(
currentUser.isActive ? Icons.pause : Icons.play_arrow,
),
onPressed: () => _toggleActiveStatus(currentUser),
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _deleteUser(currentUser),
),
],
),
leading: CircleAvatar(
child: Text(currentUser.name.substring(0, 1)),
),
onTap: () => _showUserDetails(context, currentUser),
);
},
);
}
void _toggleActiveStatus(User user) async {
await isar.writeTxn(() async {
user.isActive = !user.isActive;
await isar.users.put(user);
});
}
void _deleteUser(User user) async {
await isar.writeTxn(() async {
await isar.users.delete(user.id);
});
}
void _showUserDetails(BuildContext context, User user) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => UserDetailPage(user: user, isar: isar),
),
);
}
}
// ユーザー詳細ページ
class UserDetailPage extends StatelessWidget {
final User user;
final Isar isar;
const UserDetailPage({super.key, required this.user, required this.isar});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(user.name)),
body: StreamBuilder<User?>(
stream: isar.users.watchObject(user.id, fireImmediately: true),
builder: (context, snapshot) {
final currentUser = snapshot.data;
if (currentUser == null) {
return const Center(child: Text('User not found'));
}
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_UserInfoCard(user: currentUser),
const SizedBox(height: 16),
_UserPostsSection(user: currentUser, isar: isar),
],
),
);
},
),
);
}
}
class _UserInfoCard extends StatelessWidget {
final User user;
const _UserInfoCard({required this.user});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user.name,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
Text('Email: ${user.email}'),
Text('Age: ${user.age}'),
Text('Status: ${user.isActive ? "Active" : "Inactive"}'),
Text('Joined: ${_formatDate(user.createdAt)}'),
if (user.hobbies.isNotEmpty) ...[
const SizedBox(height: 8),
Text('Hobbies: ${user.hobbies.join(", ")}'),
],
],
),
),
);
}
String _formatDate(DateTime date) {
return '${date.year}/${date.month}/${date.day}';
}
}
class _UserPostsSection extends StatelessWidget {
final User user;
final Isar isar;
const _UserPostsSection({required this.user, required this.isar});
@override
Widget build(BuildContext context) {
return StreamBuilder<List<Post>>(
stream: isar.posts
.filter()
.author((q) => q.idEqualTo(user.id))
.watch(fireImmediately: true),
builder: (context, snapshot) {
final posts = snapshot.data ?? [];
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Posts (${posts.length})',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
if (posts.isEmpty)
const Text('No posts yet')
else
...posts.map((post) => ListTile(
title: Text(post.title),
subtitle: Text(
'Published: ${_formatDate(post.publishedAt)}',
),
trailing: Icon(
post.isPublished
? Icons.visibility
: Icons.visibility_off,
),
)),
],
),
),
);
},
);
}
String _formatDate(DateTime date) {
return '${date.year}/${date.month}/${date.day}';
}
}
// 新規ユーザー追加ダイアログ
class AddUserDialog extends StatefulWidget {
final Isar isar;
const AddUserDialog({super.key, required this.isar});
@override
State<AddUserDialog> createState() => _AddUserDialogState();
}
class _AddUserDialogState extends State<AddUserDialog> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _ageController = TextEditingController();
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Add New User'),
content: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: _nameController,
decoration: const InputDecoration(labelText: 'Name'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a name';
}
return null;
},
),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Email'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter an email';
}
if (!value.contains('@')) {
return 'Please enter a valid email';
}
return null;
},
),
TextFormField(
controller: _ageController,
decoration: const InputDecoration(labelText: 'Age'),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter an age';
}
if (int.tryParse(value) == null) {
return 'Please enter a valid number';
}
return null;
},
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: _saveUser,
child: const Text('Save'),
),
],
);
}
void _saveUser() async {
if (_formKey.currentState!.validate()) {
final user = User(
name: _nameController.text,
email: _emailController.text,
age: int.parse(_ageController.text),
);
await widget.isar.writeTxn(() async {
await widget.isar.users.put(user);
});
if (mounted) {
Navigator.of(context).pop();
}
}
}
@override
void dispose() {
_nameController.dispose();
_emailController.dispose();
_ageController.dispose();
super.dispose();
}
}
インデックスとパフォーマンス最適化
// 高度なインデックス戦略
@collection
class OptimizedUser {
Id id = Isar.autoIncrement;
// 基本インデックス
@Index()
late String name;
// ユニークインデックス
@Index(unique: true)
late String email;
// 複合インデックス(高速範囲検索)
@Index(composite: [CompositeIndex('age')])
late bool isActive;
late int age;
// 大文字小文字を無視したインデックス
@Index(caseSensitive: false)
late String username;
// 部分インデックス(条件付きインデックス)
@Index(
type: IndexType.hash,
replace: true, // 重複キーを置き換え
)
String? phoneNumber;
// 全文検索用インデックス
@Index(type: IndexType.words)
late String biography;
late DateTime createdAt;
late DateTime lastLoginAt;
List<String> skills = [];
// 複合インデックス用計算プロパティ
@Index(composite: [CompositeIndex('lastLoginAt')])
bool get isRecentlyActive =>
lastLoginAt.isAfter(DateTime.now().subtract(const Duration(days: 7)));
}
// パフォーマンス最適化されたクエリサービス
class OptimizedQueryService {
final Isar isar;
OptimizedQueryService(this.isar);
// インデックスを活用した高速検索
Future<List<OptimizedUser>> findActiveUsersByAge(int minAge, int maxAge) async {
// 複合インデックス (isActive, age) を活用
return await isar.optimizedUsers
.where()
.isActiveAge(true, between: [minAge, maxAge])
.findAll();
}
// ハッシュインデックスを活用した検索
Future<OptimizedUser?> findUserByPhone(String phone) async {
return await isar.optimizedUsers
.where()
.phoneNumberEqualTo(phone)
.findFirst();
}
// 全文検索の例
Future<List<OptimizedUser>> searchUsersByBio(String searchTerm) async {
final words = searchTerm.toLowerCase().split(' ');
return await isar.optimizedUsers
.filter()
.biographyWordsAnyStartWith(words)
.findAll();
}
// 大文字小文字を無視した検索
Future<List<OptimizedUser>> findUsersByUsername(String username) async {
return await isar.optimizedUsers
.where()
.usernameEqualTo(username)
.findAll();
}
// 複雑な複合クエリ(最適化済み)
Future<List<OptimizedUser>> getRecentlyActiveSkillfulUsers(
List<String> requiredSkills,
) async {
return await isar.optimizedUsers
.filter()
.isRecentlyActiveEqualTo(true)
.and()
.repeat((q) => q.skillsAnyOf(requiredSkills), requiredSkills.length)
.sortByLastLoginAtDesc()
.findAll();
}
// バッチ操作によるパフォーマンス向上
Future<void> batchUpdateUsers(List<OptimizedUser> users) async {
await isar.writeTxn(() async {
await isar.optimizedUsers.putAll(users);
});
}
// 集計クエリの最適化
Future<Map<String, dynamic>> getUserAnalytics() async {
final total = await isar.optimizedUsers.count();
final active = await isar.optimizedUsers
.filter()
.isActiveEqualTo(true)
.count();
final recentlyActive = await isar.optimizedUsers
.filter()
.isRecentlyActiveEqualTo(true)
.count();
// 年齢分布の取得
final ageGroups = <String, int>{};
const ageRanges = [(18, 25), (26, 35), (36, 45), (46, 60), (61, 100)];
for (final (min, max) in ageRanges) {
final count = await isar.optimizedUsers
.filter()
.ageBetween(min, max)
.count();
ageGroups['$min-$max'] = count;
}
return {
'total': total,
'active': active,
'recentlyActive': recentlyActive,
'ageGroups': ageGroups,
'activePercentage': total > 0 ? (active / total * 100).round() : 0,
};
}
}
// インデックスのメンテナンスとモニタリング
class IndexMaintenanceService {
final Isar isar;
IndexMaintenanceService(this.isar);
// データベースの統計情報取得
Future<Map<String, dynamic>> getDatabaseStats() async {
final stats = <String, dynamic>{};
// 各コレクションのサイズ
stats['collections'] = {
'users': await isar.optimizedUsers.count(),
'posts': await isar.posts.count(),
'categories': await isar.categorys.count(),
};
// データベースファイルサイズ(概算)
stats['estimatedSize'] = await _getEstimatedSize();
return stats;
}
// データベースの圧縮
Future<void> compactDatabase() async {
await isar.compact();
}
// インデックスの再構築(必要な場合)
Future<void> rebuildIndexes() async {
// Isarでは自動的にインデックスが管理されるため、
// 通常は手動でのインデックス再構築は不要
// スキーマ変更時に自動的に処理される
}
Future<int> _getEstimatedSize() async {
// 実際の実装では、ファイルシステムのサイズを取得
// ここでは概算値を返す
final userCount = await isar.optimizedUsers.count();
final postCount = await isar.posts.count();
// 1ユーザーあたり約1KB、1投稿あたり約2KBと仮定
return userCount * 1024 + postCount * 2048;
}
}
エラーハンドリング
// 堅牢なデータベース操作サービス
class RobustIsarService {
final Isar isar;
RobustIsarService(this.isar);
// 安全なユーザー作成
Future<Result<User, String>> createUserSafely({
required String name,
required String email,
required int age,
List<String> hobbies = const [],
}) async {
try {
// 入力検証
final validation = _validateUserInput(name, email, age);
if (validation != null) {
return Result.error(validation);
}
// 重複チェック
final existingUser = await isar.users
.filter()
.emailEqualTo(email)
.findFirst();
if (existingUser != null) {
return Result.error('User with email $email already exists');
}
// ユーザー作成
final user = User(
name: name,
email: email,
age: age,
hobbies: hobbies,
);
await isar.writeTxn(() async {
await isar.users.put(user);
});
return Result.success(user);
} on IsarError catch (e) {
return Result.error('Database error: ${e.message}');
} catch (e) {
return Result.error('Unexpected error: $e');
}
}
// 安全なユーザー更新
Future<Result<User, String>> updateUserSafely(
int userId,
Map<String, dynamic> updates,
) async {
try {
final user = await isar.users.get(userId);
if (user == null) {
return Result.error('User not found');
}
// 更新フィールドの検証と適用
if (updates.containsKey('name')) {
final name = updates['name'] as String?;
if (name == null || name.trim().isEmpty) {
return Result.error('Name cannot be empty');
}
user.name = name.trim();
}
if (updates.containsKey('email')) {
final email = updates['email'] as String?;
if (email == null || !_isValidEmail(email)) {
return Result.error('Invalid email format');
}
// 重複チェック(自分以外)
final existingUser = await isar.users
.filter()
.emailEqualTo(email)
.and()
.not()
.idEqualTo(userId)
.findFirst();
if (existingUser != null) {
return Result.error('Email already exists');
}
user.email = email;
}
if (updates.containsKey('age')) {
final age = updates['age'] as int?;
if (age == null || age < 0 || age > 150) {
return Result.error('Invalid age');
}
user.age = age;
}
if (updates.containsKey('hobbies')) {
final hobbies = updates['hobbies'] as List<String>?;
user.hobbies = hobbies ?? [];
}
if (updates.containsKey('isActive')) {
final isActive = updates['isActive'] as bool?;
user.isActive = isActive ?? true;
}
await isar.writeTxn(() async {
await isar.users.put(user);
});
return Result.success(user);
} on IsarError catch (e) {
return Result.error('Database error: ${e.message}');
} catch (e) {
return Result.error('Unexpected error: $e');
}
}
// 安全なバッチ操作
Future<Result<BatchResult, String>> batchOperationSafely(
List<BatchOperation> operations,
) async {
try {
final results = BatchResult();
await isar.writeTxn(() async {
for (final operation in operations) {
try {
switch (operation.type) {
case BatchOperationType.create:
final user = operation.data as User;
await isar.users.put(user);
results.successful.add(operation);
break;
case BatchOperationType.update:
final userId = operation.id!;
final updates = operation.data as Map<String, dynamic>;
final user = await isar.users.get(userId);
if (user != null) {
_applyUpdates(user, updates);
await isar.users.put(user);
results.successful.add(operation);
} else {
results.failed.add(FailedOperation(operation, 'User not found'));
}
break;
case BatchOperationType.delete:
final userId = operation.id!;
final deleted = await isar.users.delete(userId);
if (deleted) {
results.successful.add(operation);
} else {
results.failed.add(FailedOperation(operation, 'User not found'));
}
break;
}
} catch (e) {
results.failed.add(FailedOperation(operation, e.toString()));
}
}
});
return Result.success(results);
} on IsarError catch (e) {
return Result.error('Database transaction failed: ${e.message}');
} catch (e) {
return Result.error('Batch operation failed: $e');
}
}
// データベースの整合性チェック
Future<Result<IntegrityReport, String>> checkDatabaseIntegrity() async {
try {
final report = IntegrityReport();
// ユーザーデータの整合性チェック
final users = await isar.users.where().findAll();
for (final user in users) {
if (user.name.trim().isEmpty) {
report.errors.add('User ${user.id}: Empty name');
}
if (!_isValidEmail(user.email)) {
report.errors.add('User ${user.id}: Invalid email format');
}
if (user.age < 0 || user.age > 150) {
report.errors.add('User ${user.id}: Invalid age ${user.age}');
}
}
// 重複チェック
final emailCounts = <String, int>{};
for (final user in users) {
emailCounts[user.email] = (emailCounts[user.email] ?? 0) + 1;
}
for (final entry in emailCounts.entries) {
if (entry.value > 1) {
report.errors.add('Duplicate email: ${entry.key} (${entry.value} occurrences)');
}
}
report.totalUsers = users.length;
report.checkCompleted = DateTime.now();
return Result.success(report);
} catch (e) {
return Result.error('Integrity check failed: $e');
}
}
// プライベートヘルパーメソッド
String? _validateUserInput(String name, String email, int age) {
if (name.trim().isEmpty) {
return 'Name cannot be empty';
}
if (!_isValidEmail(email)) {
return 'Invalid email format';
}
if (age < 0 || age > 150) {
return 'Age must be between 0 and 150';
}
return null;
}
bool _isValidEmail(String email) {
return RegExp(r'^[^@]+@[^@]+\.[^@]+$').hasMatch(email);
}
void _applyUpdates(User user, Map<String, dynamic> updates) {
updates.forEach((key, value) {
switch (key) {
case 'name':
user.name = value as String;
break;
case 'email':
user.email = value as String;
break;
case 'age':
user.age = value as int;
break;
case 'hobbies':
user.hobbies = List<String>.from(value as List);
break;
case 'isActive':
user.isActive = value as bool;
break;
}
});
}
}
// 結果型とエラーハンドリング用クラス
class Result<T, E> {
final T? _value;
final E? _error;
const Result._(this._value, this._error);
factory Result.success(T value) => Result._(value, null);
factory Result.error(E error) => Result._(null, error);
bool get isSuccess => _value != null;
bool get isError => _error != null;
T get value => _value!;
E get error => _error!;
R fold<R>(R Function(T value) onSuccess, R Function(E error) onError) {
if (isSuccess) {
return onSuccess(value);
} else {
return onError(error);
}
}
}
// バッチ操作関連クラス
enum BatchOperationType { create, update, delete }
class BatchOperation {
final BatchOperationType type;
final int? id;
final dynamic data;
BatchOperation.create(this.data) : type = BatchOperationType.create, id = null;
BatchOperation.update(this.id, this.data) : type = BatchOperationType.update;
BatchOperation.delete(this.id) : type = BatchOperationType.delete, data = null;
}
class BatchResult {
final List<BatchOperation> successful = [];
final List<FailedOperation> failed = [];
}
class FailedOperation {
final BatchOperation operation;
final String error;
FailedOperation(this.operation, this.error);
}
class IntegrityReport {
int totalUsers = 0;
final List<String> errors = [];
DateTime? checkCompleted;
bool get hasErrors => errors.isNotEmpty;
int get errorCount => errors.length;
}