ObjectBox

ObjectBoxは、高性能軽量NoSQLデータベースです。モバイル・IoTデバイスに最適化されたオブジェクト指向アプローチを採用し、作成・更新操作で他の選択肢より最大70倍高速なパフォーマンスを実現します。Dartネイティブの型安全なAPIにより、Flutterアプリケーションでの優れた開発体験を提供します。

NoSQLDart高性能軽量モバイルIoTオブジェクトデータベース

ライブラリ

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'),
          ),
        ],
      ),
    );
  }
}