Drift
DriftはDart・Flutter向けの使いやすく、リアクティブ、タイプセーフな永続化ライブラリです。SQLiteを基盤として、あらゆるSQLクエリを自動更新ストリームに変換できるリアクティブな機能を持ち、コンパイル時の型安全性とクエリ検証を提供します。SQLとDart両方でクエリを記述でき、複雑なSQL機能(WITH、WINDOW句等)にも対応した、現代的なモバイル・デスクトップアプリケーション開発に最適なデータベースソリューションです。
GitHub概要
simolus3/drift
Drift is an easy to use, reactive, typesafe persistence library for Dart & Flutter.
トピックス
スター履歴
ライブラリ
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],
),
),
);
},
);
},
),
),
],
),
);
},
),
);
}
}