Drift

DriftはDart・Flutter向けの使いやすく、リアクティブ、タイプセーフな永続化ライブラリです。SQLiteを基盤として、あらゆるSQLクエリを自動更新ストリームに変換できるリアクティブな機能を持ち、コンパイル時の型安全性とクエリ検証を提供します。SQLとDart両方でクエリを記述でき、複雑なSQL機能(WITH、WINDOW句等)にも対応した、現代的なモバイル・デスクトップアプリケーション開発に最適なデータベースソリューションです。

ORMDartFlutterSQLiteリアクティブタイプセーフデータベース

GitHub概要

simolus3/drift

Drift is an easy to use, reactive, typesafe persistence library for Dart & Flutter.

スター2,939
ウォッチ35
フォーク403
作成日:2019年2月3日
言語:Dart
ライセンス:MIT License

トピックス

dartdart-build-systemflutterpersistencereactivesqlite

スター履歴

simolus3/drift Star History
データ取得日時: 2025/7/17 06:57

ライブラリ

Drift

概要

DriftはDart・Flutter向けの使いやすく、リアクティブ、タイプセーフな永続化ライブラリです。SQLiteを基盤として、あらゆるSQLクエリを自動更新ストリームに変換できるリアクティブな機能を持ち、コンパイル時の型安全性とクエリ検証を提供します。SQLとDart両方でクエリを記述でき、複雑なSQL機能(WITH、WINDOW句等)にも対応した、現代的なモバイル・デスクトップアプリケーション開発に最適なデータベースソリューションです。

詳細

Drift 2025年版は、Flutter・Dartエコシステムにおける最も堅牢で機能豊富なSQLiteベースのORMライブラリとして確立されています。Android、iOS、macOS、Windows、Linux、Webを含むクロスプラットフォーム対応により、どこでも一貫したデータベース体験を提供。テーブル定義とクエリに基づく型安全なコード生成、包括的なマイグレーション支援、バッチアップデートとジョイン操作、トランザクション処理など、エンタープライズレベルの要求を満たす機能を備えています。リアクティブなクエリストリームにより、データ変更時の自動UI更新を実現し、現代的なアプリケーション開発に革新をもたらします。

主な特徴

  • リアクティブクエリ: 任意のSQLクエリを自動更新ストリームに変換
  • 完全な型安全性: コンパイル時のクエリ検証と型生成
  • クロスプラットフォーム: Android、iOS、Web、デスクトップ完全対応
  • 豊富なSQL機能: WITH句、WINDOW句など高度なSQL構文サポート
  • 包括的なマイグレーション: スキーマ変更とデータベース移行の完全支援
  • バッチ処理対応: 高効率なバッチアップデートとトランザクション

メリット・デメリット

メリット

  • 任意のクエリからリアクティブストリームを簡単に生成
  • コンパイル時の型安全性により実行時エラーを大幅削減
  • SQLとDart両方でのクエリ記述により柔軟な開発が可能
  • 優れたマイグレーション支援による安全なスキーマ変更
  • クロスプラットフォーム対応で一つのコードベースで全プラットフォーム
  • 本格的なエンタープライズアプリケーションに対応する豊富な機能

デメリット

  • セットアップが他のライブラリより複雑で学習コストが高い
  • ビルドランナーとコード生成に依存するため開発環境が重い
  • SQLiteベースのため、高度なリレーショナルデータベース機能に制限
  • 大規模データセットでのパフォーマンス制約が存在
  • 他のORMライブラリ(Floor等)と比較してバンドルサイズが大きい
  • コード生成プロセスによるビルド時間の増加

参考ページ

書き方の例

基本セットアップ

// pubspec.yaml
dependencies:
  drift: ^2.20.3
  drift_flutter: ^0.2.0
  path_provider: ^2.1.4
  path: ^1.9.0

dev_dependencies:
  drift_dev: ^2.20.2
  build_runner: ^2.4.12

// パッケージのインストール
// flutter pub get
// dart run build_runner build

モデル定義と基本操作

// database.dart
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'dart:io';

part 'database.g.dart';

// テーブル定義
class Users extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get name => text().withLength(min: 1, max: 50)();
  TextColumn get email => text().unique()();
  IntColumn get age => integer().nullable()();
  DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}

class Posts extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get title => text().withLength(min: 1, max: 100)();
  TextColumn get content => text()();
  IntColumn get userId => integer().references(Users, #id)();
  DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}

// データベースクラス
@DriftDatabase(tables: [Users, Posts])
class AppDatabase extends _$AppDatabase {
  AppDatabase() : super(driftDatabase(name: 'app_database'));

  @override
  int get schemaVersion => 1;

  // 基本的なCRUD操作
  Future<List<User>> getAllUsers() => select(users).get();

  Future<User?> getUserById(int id) => 
    (select(users)..where((tbl) => tbl.id.equals(id))).getSingleOrNull();

  Future<int> insertUser(UsersCompanion user) => 
    into(users).insert(user);

  Future<bool> updateUser(User user) => 
    update(users).replace(user);

  Future<int> deleteUser(int id) => 
    (delete(users)..where((tbl) => tbl.id.equals(id))).go();

  // リアクティブクエリ(自動更新ストリーム)
  Stream<List<User>> watchAllUsers() => select(users).watch();

  Stream<User?> watchUserById(int id) => 
    (select(users)..where((tbl) => tbl.id.equals(id))).watchSingleOrNull();
}

// 使用例
void main() async {
  final database = AppDatabase();

  // ユーザー挿入
  await database.insertUser(UsersCompanion.insert(
    name: '田中太郎',
    email: '[email protected]',
    age: const Value(30),
  ));

  // 全ユーザー取得
  final users = await database.getAllUsers();
  print('全ユーザー: $users');

  // リアクティブストリーム(データ変更時に自動更新)
  database.watchAllUsers().listen((users) {
    print('ユーザーリストが更新されました: ${users.length}人');
  });

  await database.close();
}

高度なクエリ操作

// 複雑なクエリ操作
extension DatabaseQueries on AppDatabase {
  // 条件付きクエリ
  Future<List<User>> getUsersByAgeRange(int minAge, int maxAge) {
    return (select(users)
      ..where((tbl) => tbl.age.isBetweenValues(minAge, maxAge))
      ..orderBy([(tbl) => OrderingTerm.asc(tbl.name)])
    ).get();
  }

  // JOIN操作
  Future<List<UserWithPosts>> getUsersWithPosts() {
    final query = select(users).join([
      innerJoin(posts, posts.userId.equalsExp(users.id))
    ]);

    return query.map((row) {
      final user = row.readTable(users);
      final post = row.readTable(posts);
      return UserWithPosts(user: user, posts: [post]);
    }).get();
  }

  // GROUP BY集計
  Future<List<UserPostCount>> getUserPostCounts() {
    final query = selectOnly(users)
      ..addColumns([users.id, users.name, posts.id.count()])
      ..join([leftOuterJoin(posts, posts.userId.equalsExp(users.id))])
      ..groupBy([users.id, users.name]);

    return query.map((row) => UserPostCount(
      userId: row.read(users.id)!,
      userName: row.read(users.name)!,
      postCount: row.read(posts.id.count()) ?? 0,
    )).get();
  }

  // 複雑な条件とサブクエリ
  Future<List<User>> getActiveUsersWithRecentPosts() {
    final recentPosts = selectOnly(posts)
      ..addColumns([posts.userId])
      ..where(posts.createdAt.isBiggerThanValue(
        DateTime.now().subtract(const Duration(days: 30))
      ));

    return (select(users)
      ..where((tbl) => tbl.id.isIn(recentPosts))
    ).get();
  }

  // カスタムSQL(高度な操作)
  Future<List<Map<String, Object?>>> getTopActiveUsers() {
    return customSelect(
      '''
      SELECT u.id, u.name, u.email, COUNT(p.id) as post_count
      FROM users u
      LEFT JOIN posts p ON u.id = p.user_id
      WHERE u.created_at >= ?
      GROUP BY u.id, u.name, u.email
      HAVING COUNT(p.id) > ?
      ORDER BY post_count DESC
      LIMIT ?
      ''',
      variables: [
        Variable.withDateTime(DateTime.now().subtract(const Duration(days: 90))),
        Variable.withInt(5),
        Variable.withInt(10),
      ],
    ).get();
  }
}

// データクラス定義
class UserWithPosts {
  final User user;
  final List<Post> posts;
  UserWithPosts({required this.user, required this.posts});
}

class UserPostCount {
  final int userId;
  final String userName;
  final int postCount;
  UserPostCount({required this.userId, required this.userName, required this.postCount});
}

リレーション操作

// リレーション定義の追加
@DriftDatabase(tables: [Users, Posts, Categories, PostCategories])
class AppDatabase extends _$AppDatabase {
  AppDatabase() : super(driftDatabase(name: 'app_database'));

  @override
  int get schemaVersion => 2;

  // マイグレーション
  @override
  MigrationStrategy get migration {
    return MigrationStrategy(
      onCreate: (Migrator m) async {
        await m.createAll();
      },
      onUpgrade: (Migrator m, int from, int to) async {
        if (from < 2) {
          await m.createTable(categories);
          await m.createTable(postCategories);
        }
      },
    );
  }
}

// 新しいテーブル
class Categories extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get name => text().withLength(min: 1, max: 50)();
  TextColumn get description => text().nullable()();
}

class PostCategories extends Table {
  IntColumn get postId => integer().references(Posts, #id)();
  IntColumn get categoryId => integer().references(Categories, #id)();

  @override
  Set<Column> get primaryKey => {postId, categoryId};
}

// リレーション操作のメソッド
extension RelationQueries on AppDatabase {
  // ユーザーの全投稿を取得
  Future<List<Post>> getPostsByUser(int userId) {
    return (select(posts)
      ..where((tbl) => tbl.userId.equals(userId))
      ..orderBy([(tbl) => OrderingTerm.desc(tbl.createdAt)])
    ).get();
  }

  // 投稿のカテゴリを取得
  Future<List<Category>> getCategoriesByPost(int postId) {
    final query = select(categories).join([
      innerJoin(
        postCategories,
        postCategories.categoryId.equalsExp(categories.id),
      )
    ])..where(postCategories.postId.equals(postId));

    return query.map((row) => row.readTable(categories)).get();
  }

  // カテゴリの投稿を取得
  Future<List<Post>> getPostsByCategory(int categoryId) {
    final query = select(posts).join([
      innerJoin(
        postCategories,
        postCategories.postId.equalsExp(posts.id),
      )
    ])..where(postCategories.categoryId.equals(categoryId));

    return query.map((row) => row.readTable(posts)).get();
  }

  // 複雑なリレーション(ユーザー→投稿→カテゴリ)
  Future<UserWithPostsAndCategories> getUserWithPostsAndCategories(int userId) async {
    final user = await getUserById(userId);
    if (user == null) throw Exception('User not found');

    final userPosts = await getPostsByUser(userId);
    
    final postsWithCategories = <PostWithCategories>[];
    for (final post in userPosts) {
      final categories = await getCategoriesByPost(post.id);
      postsWithCategories.add(PostWithCategories(
        post: post,
        categories: categories,
      ));
    }

    return UserWithPostsAndCategories(
      user: user,
      postsWithCategories: postsWithCategories,
    );
  }

  // トランザクション内での複合操作
  Future<void> createPostWithCategories(
    int userId,
    String title,
    String content,
    List<int> categoryIds,
  ) {
    return transaction(() async {
      // 投稿作成
      final postId = await into(posts).insert(PostsCompanion.insert(
        title: title,
        content: content,
        userId: userId,
      ));

      // カテゴリ関連付け
      for (final categoryId in categoryIds) {
        await into(postCategories).insert(PostCategoriesCompanion.insert(
          postId: postId,
          categoryId: categoryId,
        ));
      }
    });
  }
}

// データクラス
class PostWithCategories {
  final Post post;
  final List<Category> categories;
  PostWithCategories({required this.post, required this.categories});
}

class UserWithPostsAndCategories {
  final User user;
  final List<PostWithCategories> postsWithCategories;
  UserWithPostsAndCategories({required this.user, required this.postsWithCategories});
}

実用例

// 実際のFlutterアプリケーションでの使用例
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

// データベースプロバイダー
class DatabaseProvider extends ChangeNotifier {
  late final AppDatabase _database;
  AppDatabase get database => _database;

  DatabaseProvider() {
    _database = AppDatabase();
  }

  @override
  void dispose() {
    _database.close();
    super.dispose();
  }
}

// メインアプリ
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => DatabaseProvider(),
      child: MaterialApp(
        title: 'Drift Demo',
        home: UserListScreen(),
      ),
    );
  }
}

// ユーザーリスト画面(リアクティブ)
class UserListScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final database = context.read<DatabaseProvider>().database;

    return Scaffold(
      appBar: AppBar(title: const Text('ユーザー一覧')),
      body: StreamBuilder<List<User>>(
        stream: database.watchAllUsers(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }

          final users = snapshot.data ?? [];
          if (users.isEmpty) {
            return const Center(child: Text('ユーザーがいません'));
          }

          return ListView.builder(
            itemCount: users.length,
            itemBuilder: (context, index) {
              final user = users[index];
              return ListTile(
                title: Text(user.name),
                subtitle: Text(user.email),
                trailing: Text('${user.age ?? "N/A"}歳'),
                onTap: () => Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (_) => UserDetailScreen(userId: user.id),
                  ),
                ),
              );
            },
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _showAddUserDialog(context),
        child: const Icon(Icons.add),
      ),
    );
  }

  void _showAddUserDialog(BuildContext context) {
    final database = context.read<DatabaseProvider>().database;
    final nameController = TextEditingController();
    final emailController = TextEditingController();
    final ageController = TextEditingController();

    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('新しいユーザー'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            TextField(
              controller: nameController,
              decoration: const InputDecoration(labelText: '名前'),
            ),
            TextField(
              controller: emailController,
              decoration: const InputDecoration(labelText: 'メール'),
              keyboardType: TextInputType.emailAddress,
            ),
            TextField(
              controller: ageController,
              decoration: const InputDecoration(labelText: '年齢'),
              keyboardType: TextInputType.number,
            ),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('キャンセル'),
          ),
          TextButton(
            onPressed: () async {
              final age = int.tryParse(ageController.text);
              await database.insertUser(UsersCompanion.insert(
                name: nameController.text,
                email: emailController.text,
                age: age != null ? Value(age) : const Value.absent(),
              ));
              Navigator.pop(context);
            },
            child: const Text('追加'),
          ),
        ],
      ),
    );
  }
}

// ユーザー詳細画面
class UserDetailScreen extends StatelessWidget {
  final int userId;

  const UserDetailScreen({required this.userId});

  @override
  Widget build(BuildContext context) {
    final database = context.read<DatabaseProvider>().database;

    return Scaffold(
      appBar: AppBar(title: const Text('ユーザー詳細')),
      body: StreamBuilder<User?>(
        stream: database.watchUserById(userId),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }

          final user = snapshot.data;
          if (user == null) {
            return const Center(child: Text('ユーザーが見つかりません'));
          }

          return Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Card(
                  child: Padding(
                    padding: const EdgeInsets.all(16.0),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text('名前: ${user.name}', style: Theme.of(context).textTheme.titleLarge),
                        const SizedBox(height: 8),
                        Text('メール: ${user.email}'),
                        Text('年齢: ${user.age ?? "未設定"}'),
                        Text('作成日: ${user.createdAt.toString().split('.')[0]}'),
                      ],
                    ),
                  ),
                ),
                const SizedBox(height: 16),
                Text('投稿', style: Theme.of(context).textTheme.titleMedium),
                Expanded(
                  child: StreamBuilder<List<Post>>(
                    stream: database.getPostsByUser(userId).asStream(),
                    builder: (context, snapshot) {
                      final posts = snapshot.data ?? [];
                      if (posts.isEmpty) {
                        return const Center(child: Text('投稿がありません'));
                      }

                      return ListView.builder(
                        itemCount: posts.length,
                        itemBuilder: (context, index) {
                          final post = posts[index];
                          return Card(
                            child: ListTile(
                              title: Text(post.title),
                              subtitle: Text(
                                post.content,
                                maxLines: 2,
                                overflow: TextOverflow.ellipsis,
                              ),
                              trailing: Text(
                                post.createdAt.toString().split(' ')[0],
                              ),
                            ),
                          );
                        },
                      );
                    },
                  ),
                ),
              ],
            ),
          );
        },
      ),
    );
  }
}