ObjectBox
ObjectBoxは、高性能軽量NoSQLデータベースです。モバイル・IoTデバイスに最適化されたオブジェクト指向アプローチを採用し、作成・更新操作で他の選択肢より最大70倍高速なパフォーマンスを実現します。Dartネイティブの型安全なAPIにより、Flutterアプリケーションでの優れた開発体験を提供します。
ライブラリ
ObjectBox
概要
ObjectBoxは、高性能軽量NoSQLデータベースです。モバイル・IoTデバイスに最適化されたオブジェクト指向アプローチを採用し、作成・更新操作で他の選択肢より最大70倍高速なパフォーマンスを実現します。Dartネイティブの型安全なAPIにより、Flutterアプリケーションでの優れた開発体験を提供します。
詳細
ObjectBox 2025年版は、パフォーマンス重視プロジェクトの最適解として確立されています。企業レベルのオフラインファーストアプリケーションでの採用が増加しており、特にリアルタイム性が要求されるモバイルアプリケーションで威力を発揮します。ベクトル検索、時系列データ、同期機能など、エンタープライズグレードの機能を備えながら、リソース制約の厳しいデバイスでも効率的に動作します。
主な特徴
- 超高速パフォーマンス: 他のソリューションより最大70倍高速
- 軽量設計: 最小限のメモリフットプリント
- 型安全API: Dartネイティブの完全な型サポート
- オフラインファースト: ローカルファーストアーキテクチャ
- ACID準拠: トランザクションの完全性保証
- データ同期: ObjectBox Syncによるデバイス間同期
メリット・デメリット
メリット
- 圧倒的なパフォーマンス(特に書き込み操作)
- 最小限のボイラープレートコード
- 優れた型安全性とコード生成
- バッテリー効率の高い設計
- リアルタイムクエリとリアクティブAPI
- エッジコンピューティングに最適
デメリット
- 商用ライセンスが必要(オープンソース版は制限あり)
- SQLベースのクエリに慣れた開発者には学習コスト
- リレーショナルデータベースと比べて高度なクエリ機能が限定的
- 比較的新しいためコミュニティが小規模
- マイグレーション戦略が複雑
参考ページ
書き方の例
基本セットアップ
# pubspec.yaml
dependencies:
objectbox: ^2.0.0
objectbox_flutter_libs: ^2.0.0
dev_dependencies:
build_runner: ^2.0.0
objectbox_generator: ^2.0.0
// lib/models/user.dart
import 'package:objectbox/objectbox.dart';
@Entity()
class User {
@Id()
int id = 0;
String name;
String email;
int? age;
@Property(type: PropertyType.date)
DateTime createdAt;
// リレーション
final posts = ToMany<Post>();
User({
required this.name,
required this.email,
this.age,
DateTime? createdAt,
}) : createdAt = createdAt ?? DateTime.now();
}
@Entity()
class Post {
@Id()
int id = 0;
String title;
String content;
@Property(type: PropertyType.date)
DateTime createdAt;
int viewCount;
// 逆リレーション
final author = ToOne<User>();
Post({
required this.title,
required this.content,
this.viewCount = 0,
DateTime? createdAt,
}) : createdAt = createdAt ?? DateTime.now();
}
// コード生成実行
// flutter pub run build_runner build
基本的なCRUD操作
import 'package:objectbox/objectbox.dart';
import 'objectbox.g.dart'; // 生成されるファイル
class ObjectBoxDatabase {
late final Store store;
late final Box<User> userBox;
late final Box<Post> postBox;
ObjectBoxDatabase._create(this.store) {
userBox = Box<User>(store);
postBox = Box<Post>(store);
}
static Future<ObjectBoxDatabase> create() async {
final store = await openStore();
return ObjectBoxDatabase._create(store);
}
// CREATE - 新規作成
int createUser(User user) {
return userBox.put(user);
}
// バッチ作成
List<int> createUsers(List<User> users) {
return userBox.putMany(users);
}
// READ - 読み取り
User? getUserById(int id) {
return userBox.get(id);
}
List<User> getAllUsers() {
return userBox.getAll();
}
// クエリビルダー
List<User> searchUsers({
String? nameContains,
int? minAge,
int? maxAge,
}) {
final queryBuilder = userBox.query();
if (nameContains != null) {
queryBuilder.contains(User_.name, nameContains);
}
if (minAge != null) {
queryBuilder.greaterOrEqual(User_.age, minAge);
}
if (maxAge != null) {
queryBuilder.lessOrEqual(User_.age, maxAge);
}
return queryBuilder
.order(User_.name)
.build()
.find();
}
// UPDATE - 更新
void updateUser(User user) {
userBox.put(user);
}
// 部分更新
void updateUserAge(int userId, int newAge) {
final user = userBox.get(userId);
if (user != null) {
user.age = newAge;
userBox.put(user);
}
}
// DELETE - 削除
bool deleteUser(int id) {
return userBox.remove(id);
}
// 複数削除
int deleteUsers(List<int> ids) {
return userBox.removeMany(ids);
}
// 条件付き削除
int deleteOldUsers(DateTime before) {
final query = userBox.query(
User_.createdAt.lessThan(before.millisecondsSinceEpoch)
).build();
final users = query.find();
query.close();
return userBox.removeMany(users.map((u) => u.id).toList());
}
void close() {
store.close();
}
}
高度な機能
// リレーション操作
class BlogRepository {
final ObjectBoxDatabase db;
BlogRepository(this.db);
// 投稿を作成し、著者と関連付け
int createPost(Post post, int authorId) {
final author = db.userBox.get(authorId);
if (author == null) {
throw Exception('Author not found');
}
post.author.target = author;
return db.postBox.put(post);
}
// ユーザーの全投稿を取得
List<Post> getUserPosts(int userId) {
final user = db.userBox.get(userId);
return user?.posts.toList() ?? [];
}
// 投稿と著者情報を一緒に取得
List<PostWithAuthor> getPostsWithAuthors() {
final posts = db.postBox.getAll();
return posts.map((post) {
return PostWithAuthor(
post: post,
authorName: post.author.target?.name ?? 'Unknown',
);
}).toList();
}
// トランザクション処理
void transferPosts(int fromUserId, int toUserId) {
db.store.runInTransaction(TxMode.write, () {
final fromUser = db.userBox.get(fromUserId);
final toUser = db.userBox.get(toUserId);
if (fromUser == null || toUser == null) {
throw Exception('User not found');
}
// 全投稿を新しいユーザーに移動
final posts = fromUser.posts.toList();
for (final post in posts) {
post.author.target = toUser;
db.postBox.put(post);
}
});
}
}
// リアクティブクエリ
class ReactiveQueries {
final ObjectBoxDatabase db;
ReactiveQueries(this.db);
// ユーザー一覧をストリームで監視
Stream<List<User>> watchUsers() {
return db.userBox
.query()
.order(User_.name)
.watch(triggerImmediately: true)
.map((query) => query.find());
}
// 特定条件のユーザーを監視
Stream<List<User>> watchActiveUsers() {
final thirtyDaysAgo = DateTime.now().subtract(Duration(days: 30));
return db.userBox
.query(User_.createdAt.greaterThan(thirtyDaysAgo.millisecondsSinceEpoch))
.watch()
.map((query) => query.find());
}
// 投稿数の変化を監視
Stream<int> watchPostCount() {
return db.postBox
.query()
.watch()
.map((query) => query.count());
}
}
// パフォーマンス最適化
class PerformanceOptimization {
final ObjectBoxDatabase db;
PerformanceOptimization(this.db);
// インデックス付きクエリ
List<User> fastEmailSearch(String email) {
// emailフィールドに@Index()アノテーションを付けることで高速化
return db.userBox
.query(User_.email.equals(email))
.build()
.find();
}
// ページネーション
List<User> getPaginatedUsers({
required int page,
required int pageSize,
}) {
final offset = (page - 1) * pageSize;
return db.userBox
.query()
.order(User_.name)
.build()
.find()
.skip(offset)
.take(pageSize)
.toList();
}
// 集計関数
UserStatistics getUserStatistics() {
final query = db.userBox.query();
final totalUsers = query.build().count();
final ageQuery = query.build();
final ages = ageQuery.property(User_.age).find();
ageQuery.close();
final validAges = ages.whereType<int>().toList();
return UserStatistics(
totalUsers: totalUsers,
averageAge: validAges.isEmpty
? 0
: validAges.reduce((a, b) => a + b) / validAges.length,
maxAge: validAges.isEmpty ? 0 : validAges.reduce((a, b) => a > b ? a : b),
minAge: validAges.isEmpty ? 0 : validAges.reduce((a, b) => a < b ? a : b),
);
}
}
// DTOクラス
class PostWithAuthor {
final Post post;
final String authorName;
PostWithAuthor({
required this.post,
required this.authorName,
});
}
class UserStatistics {
final int totalUsers;
final double averageAge;
final int maxAge;
final int minAge;
UserStatistics({
required this.totalUsers,
required this.averageAge,
required this.maxAge,
required this.minAge,
});
}
実用例
// Flutterアプリケーションでの使用例
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// プロバイダー定義
final objectBoxProvider = Provider<ObjectBoxDatabase>((ref) {
throw UnimplementedError('Initialize in main()');
});
final userListProvider = StreamProvider<List<User>>((ref) {
final db = ref.watch(objectBoxProvider);
return db.userBox
.query()
.order(User_.name)
.watch(triggerImmediately: true)
.map((query) => query.find());
});
// メイン関数
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final objectBox = await ObjectBoxDatabase.create();
runApp(
ProviderScope(
overrides: [
objectBoxProvider.overrideWithValue(objectBox),
],
child: MyApp(),
),
);
}
// ユーザー一覧画面
class UserListScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final usersAsync = ref.watch(userListProvider);
return Scaffold(
appBar: AppBar(title: Text('Users')),
body: usersAsync.when(
data: (users) => ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
title: Text(user.name),
subtitle: Text(user.email),
trailing: Text('Age: ${user.age ?? "N/A"}'),
onTap: () => _showUserDetails(context, user),
);
},
),
loading: () => Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _addUser(context, ref),
child: Icon(Icons.add),
),
);
}
void _showUserDetails(BuildContext context, User user) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => UserDetailScreen(userId: user.id),
),
);
}
void _addUser(BuildContext context, WidgetRef ref) {
showDialog(
context: context,
builder: (context) => AddUserDialog(),
);
}
}
// ユーザー追加ダイアログ
class AddUserDialog extends ConsumerStatefulWidget {
@override
_AddUserDialogState createState() => _AddUserDialogState();
}
class _AddUserDialogState extends ConsumerState<AddUserDialog> {
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _ageController = TextEditingController();
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Add User'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _nameController,
decoration: InputDecoration(labelText: 'Name'),
),
TextField(
controller: _emailController,
decoration: InputDecoration(labelText: 'Email'),
keyboardType: TextInputType.emailAddress,
),
TextField(
controller: _ageController,
decoration: InputDecoration(labelText: 'Age'),
keyboardType: TextInputType.number,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Cancel'),
),
ElevatedButton(
onPressed: _saveUser,
child: Text('Save'),
),
],
);
}
void _saveUser() {
final db = ref.read(objectBoxProvider);
final user = User(
name: _nameController.text,
email: _emailController.text,
age: int.tryParse(_ageController.text),
);
db.createUser(user);
Navigator.pop(context);
}
@override
void dispose() {
_nameController.dispose();
_emailController.dispose();
_ageController.dispose();
super.dispose();
}
}
// ユーザー詳細画面
class UserDetailScreen extends ConsumerWidget {
final int userId;
UserDetailScreen({required this.userId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final db = ref.watch(objectBoxProvider);
final user = db.getUserById(userId);
if (user == null) {
return Scaffold(
appBar: AppBar(title: Text('User Not Found')),
body: Center(child: Text('User not found')),
);
}
return Scaffold(
appBar: AppBar(
title: Text(user.name),
actions: [
IconButton(
icon: Icon(Icons.delete),
onPressed: () => _deleteUser(context, ref),
),
],
),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Email: ${user.email}', style: TextStyle(fontSize: 18)),
SizedBox(height: 8),
Text('Age: ${user.age ?? "Not specified"}', style: TextStyle(fontSize: 18)),
SizedBox(height: 8),
Text('Created: ${user.createdAt}', style: TextStyle(fontSize: 14)),
SizedBox(height: 24),
Text('Posts (${user.posts.length})', style: Theme.of(context).textTheme.headlineSmall),
Expanded(
child: ListView.builder(
itemCount: user.posts.length,
itemBuilder: (context, index) {
final post = user.posts[index];
return Card(
child: ListTile(
title: Text(post.title),
subtitle: Text(post.content, maxLines: 2, overflow: TextOverflow.ellipsis),
trailing: Text('Views: ${post.viewCount}'),
),
);
},
),
),
],
),
),
);
}
void _deleteUser(BuildContext context, WidgetRef ref) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Delete User'),
content: Text('Are you sure you want to delete this user?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Cancel'),
),
ElevatedButton(
onPressed: () {
final db = ref.read(objectBoxProvider);
db.deleteUser(userId);
Navigator.of(context)..pop()..pop();
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: Text('Delete'),
),
],
),
);
}
}