SQLite (sqflite)
sqfliteはFlutter向けの軽量で高性能なSQLiteデータベースライブラリで、「iOS、Android、macOSでのローカルデータ永続化の決定版」として位置づけられています。Flutterエコシステムにおいて事実上の標準的なSQLite実装として、ネイティブパフォーマンスと型安全性を両立し、モバイルアプリケーションでの構造化データ管理に特化した包括的なソリューションを提供します。
ライブラリ
SQLite (sqflite)
概要
sqfliteはFlutter向けの軽量で高性能なSQLiteデータベースライブラリで、「iOS、Android、macOSでのローカルデータ永続化の決定版」として位置づけられています。Flutterエコシステムにおいて事実上の標準的なSQLite実装として、ネイティブパフォーマンスと型安全性を両立し、モバイルアプリケーションでの構造化データ管理に特化した包括的なソリューションを提供します。
詳細
sqflite 2025年版は、Flutter 3.0以降の最新機能と完全統合し、iOS、Android、macOS、およびWeb(実験的)での一貫したSQLiteデータベース体験を実現します。ネイティブSQLiteエンジンとの直接統合により、高いパフォーマンスとメモリ効率を維持しながら、Dart FutureベースのAPIとasync/awaitサポートによる現代的な非同期プログラミングパターンを提供。データベーススキーマ管理、マイグレーション、バッチ処理、トランザクション制御を包括的にサポートし、エンタープライズレベルのモバイルアプリケーション開発に対応します。
主な特徴
- マルチプラットフォーム対応: iOS、Android、macOS、Web(実験的)での統一API
- 高性能ネイティブ実装: 直接SQLiteエンジン統合による最適化されたパフォーマンス
- 非同期処理サポート: Future/async-awaitベースの現代的なAPI設計
- 包括的CRUD機能: 完全なCRUD操作とSQL文実行サポート
- スキーマ管理: データベースバージョニングとマイグレーション機能
- トランザクション制御: ACID特性保証による安全なデータ操作
メリット・デメリット
メリット
- Flutterエコシステムでの標準的地位と豊富な学習リソース
- ネイティブパフォーマンスによる高速なデータベース操作
- 軽量な実装でアプリサイズへの影響最小化
- 堅牢なACID特性による安全なトランザクション処理
- 直感的なDart APIによる低い学習コストと高い開発効率
- 豊富なコミュニティサポートとサードパーティ統合
デメリット
- SQLiteの制約による高度なリレーショナル機能の限界
- ローカルストレージのみでクラウド同期機能は別途実装が必要
- 大容量データセットでのパフォーマンス制約
- Web実装は実験的段階で制限事項が存在
- 複雑なクエリ構築時の生SQL記述による可読性課題
- NoSQLデータモデルには不適合
参考ページ
書き方の例
プロジェクトセットアップと依存関係
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
sqflite: ^2.4.2
path: ^1.8.3
dev_dependencies:
flutter_test:
sdk: flutter
# Dart imports
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
データベース初期化とセットアップ
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
class DatabaseHelper {
static const _databaseName = "app_database.db";
static const _databaseVersion = 1;
// テーブル定義
static const String tableUsers = 'users';
static const String tablePosts = 'posts';
// ユーザーテーブルのカラム
static const String columnUserId = 'id';
static const String columnUserName = 'name';
static const String columnUserEmail = 'email';
static const String columnUserAge = 'age';
static const String columnUserCreatedAt = 'created_at';
// 投稿テーブルのカラム
static const String columnPostId = 'id';
static const String columnPostTitle = 'title';
static const String columnPostContent = 'content';
static const String columnPostUserId = 'user_id';
static const String columnPostCreatedAt = 'created_at';
static Database? _database;
// シングルトンパターンでデータベースインスタンス管理
static Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDatabase();
return _database!;
}
// データベース初期化
static Future<Database> _initDatabase() async {
String path = join(await getDatabasesPath(), _databaseName);
return await openDatabase(
path,
version: _databaseVersion,
onCreate: _onCreate,
onUpgrade: _onUpgrade,
);
}
// テーブル作成
static Future<void> _onCreate(Database db, int version) async {
// ユーザーテーブル作成
await db.execute('''
CREATE TABLE $tableUsers (
$columnUserId INTEGER PRIMARY KEY AUTOINCREMENT,
$columnUserName TEXT NOT NULL,
$columnUserEmail TEXT UNIQUE NOT NULL,
$columnUserAge INTEGER,
$columnUserCreatedAt TEXT NOT NULL
)
''');
// 投稿テーブル作成
await db.execute('''
CREATE TABLE $tablePosts (
$columnPostId INTEGER PRIMARY KEY AUTOINCREMENT,
$columnPostTitle TEXT NOT NULL,
$columnPostContent TEXT NOT NULL,
$columnPostUserId INTEGER NOT NULL,
$columnPostCreatedAt TEXT NOT NULL,
FOREIGN KEY ($columnPostUserId) REFERENCES $tableUsers ($columnUserId)
)
''');
// インデックス作成
await db.execute('''
CREATE INDEX idx_posts_user_id ON $tablePosts($columnPostUserId)
''');
await db.execute('''
CREATE INDEX idx_users_email ON $tableUsers($columnUserEmail)
''');
}
// データベースアップグレード
static Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
if (oldVersion < 2) {
// バージョン2のマイグレーション例
await db.execute('''
ALTER TABLE $tableUsers ADD COLUMN profile_image TEXT
''');
}
if (oldVersion < 3) {
// バージョン3のマイグレーション例
await db.execute('''
CREATE TABLE categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT
)
''');
}
}
// データベース接続終了
static Future<void> closeDatabase() async {
final db = _database;
if (db != null) {
await db.close();
_database = null;
}
}
}
モデルクラス定義
// ユーザーモデル
class User {
final int? id;
final String name;
final String email;
final int? age;
final DateTime createdAt;
User({
this.id,
required this.name,
required this.email,
this.age,
required this.createdAt,
});
// データベースからのマップ変換
factory User.fromMap(Map<String, dynamic> map) {
return User(
id: map[DatabaseHelper.columnUserId],
name: map[DatabaseHelper.columnUserName],
email: map[DatabaseHelper.columnUserEmail],
age: map[DatabaseHelper.columnUserAge],
createdAt: DateTime.parse(map[DatabaseHelper.columnUserCreatedAt]),
);
}
// データベースへのマップ変換
Map<String, dynamic> toMap() {
return {
DatabaseHelper.columnUserId: id,
DatabaseHelper.columnUserName: name,
DatabaseHelper.columnUserEmail: email,
DatabaseHelper.columnUserAge: age,
DatabaseHelper.columnUserCreatedAt: createdAt.toIso8601String(),
};
}
// デバッグ用文字列表現
@override
String toString() {
return 'User{id: $id, name: $name, email: $email, age: $age, createdAt: $createdAt}';
}
// コピーウィズメソッド
User copyWith({
int? id,
String? name,
String? email,
int? age,
DateTime? createdAt,
}) {
return User(
id: id ?? this.id,
name: name ?? this.name,
email: email ?? this.email,
age: age ?? this.age,
createdAt: createdAt ?? this.createdAt,
);
}
}
// 投稿モデル
class Post {
final int? id;
final String title;
final String content;
final int userId;
final DateTime createdAt;
Post({
this.id,
required this.title,
required this.content,
required this.userId,
required this.createdAt,
});
factory Post.fromMap(Map<String, dynamic> map) {
return Post(
id: map[DatabaseHelper.columnPostId],
title: map[DatabaseHelper.columnPostTitle],
content: map[DatabaseHelper.columnPostContent],
userId: map[DatabaseHelper.columnPostUserId],
createdAt: DateTime.parse(map[DatabaseHelper.columnPostCreatedAt]),
);
}
Map<String, dynamic> toMap() {
return {
DatabaseHelper.columnPostId: id,
DatabaseHelper.columnPostTitle: title,
DatabaseHelper.columnPostContent: content,
DatabaseHelper.columnPostUserId: userId,
DatabaseHelper.columnPostCreatedAt: createdAt.toIso8601String(),
};
}
@override
String toString() {
return 'Post{id: $id, title: $title, content: $content, userId: $userId, createdAt: $createdAt}';
}
}
// ユーザーと投稿の結合モデル
class UserWithPosts {
final User user;
final List<Post> posts;
UserWithPosts({
required this.user,
required this.posts,
});
@override
String toString() {
return 'UserWithPosts{user: $user, posts: ${posts.length} posts}';
}
}
基本的なCRUD操作
class UserRepository {
// ユーザー作成
static Future<int> insertUser(User user) async {
final Database db = await DatabaseHelper.database;
try {
final Map<String, dynamic> userMap = user.toMap();
userMap.remove(DatabaseHelper.columnUserId); // auto-incrementのためIDを除去
int userId = await db.insert(
DatabaseHelper.tableUsers,
userMap,
conflictAlgorithm: ConflictAlgorithm.abort,
);
print('ユーザーが作成されました: ID $userId');
return userId;
} catch (e) {
print('ユーザー作成エラー: $e');
rethrow;
}
}
// 全ユーザー取得
static Future<List<User>> getAllUsers() async {
final Database db = await DatabaseHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
DatabaseHelper.tableUsers,
orderBy: '${DatabaseHelper.columnUserCreatedAt} DESC',
);
return List.generate(maps.length, (i) {
return User.fromMap(maps[i]);
});
}
// ID指定でユーザー取得
static Future<User?> getUserById(int id) async {
final Database db = await DatabaseHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
DatabaseHelper.tableUsers,
where: '${DatabaseHelper.columnUserId} = ?',
whereArgs: [id],
);
if (maps.isNotEmpty) {
return User.fromMap(maps.first);
}
return null;
}
// メールでユーザー検索
static Future<User?> getUserByEmail(String email) async {
final Database db = await DatabaseHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
DatabaseHelper.tableUsers,
where: '${DatabaseHelper.columnUserEmail} = ?',
whereArgs: [email],
);
if (maps.isNotEmpty) {
return User.fromMap(maps.first);
}
return null;
}
// ユーザー情報更新
static Future<bool> updateUser(User user) async {
final Database db = await DatabaseHelper.database;
try {
int count = await db.update(
DatabaseHelper.tableUsers,
user.toMap(),
where: '${DatabaseHelper.columnUserId} = ?',
whereArgs: [user.id],
);
print('ユーザー更新: $count 件');
return count > 0;
} catch (e) {
print('ユーザー更新エラー: $e');
return false;
}
}
// ユーザー削除
static Future<bool> deleteUser(int id) async {
final Database db = await DatabaseHelper.database;
try {
// 関連する投稿も削除(カスケード削除)
await db.delete(
DatabaseHelper.tablePosts,
where: '${DatabaseHelper.columnPostUserId} = ?',
whereArgs: [id],
);
int count = await db.delete(
DatabaseHelper.tableUsers,
where: '${DatabaseHelper.columnUserId} = ?',
whereArgs: [id],
);
print('ユーザー削除: $count 件');
return count > 0;
} catch (e) {
print('ユーザー削除エラー: $e');
return false;
}
}
// 年齢範囲でユーザー検索
static Future<List<User>> getUsersByAgeRange(int minAge, int maxAge) async {
final Database db = await DatabaseHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
DatabaseHelper.tableUsers,
where: '${DatabaseHelper.columnUserAge} BETWEEN ? AND ?',
whereArgs: [minAge, maxAge],
orderBy: '${DatabaseHelper.columnUserAge} ASC',
);
return List.generate(maps.length, (i) {
return User.fromMap(maps[i]);
});
}
// ユーザー数カウント
static Future<int> getUserCount() async {
final Database db = await DatabaseHelper.database;
var result = await db.rawQuery('SELECT COUNT(*) FROM ${DatabaseHelper.tableUsers}');
return Sqflite.firstIntValue(result) ?? 0;
}
}
class PostRepository {
// 投稿作成
static Future<int> insertPost(Post post) async {
final Database db = await DatabaseHelper.database;
try {
final Map<String, dynamic> postMap = post.toMap();
postMap.remove(DatabaseHelper.columnPostId);
int postId = await db.insert(
DatabaseHelper.tablePosts,
postMap,
conflictAlgorithm: ConflictAlgorithm.abort,
);
print('投稿が作成されました: ID $postId');
return postId;
} catch (e) {
print('投稿作成エラー: $e');
rethrow;
}
}
// ユーザーの投稿取得
static Future<List<Post>> getPostsByUserId(int userId) async {
final Database db = await DatabaseHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
DatabaseHelper.tablePosts,
where: '${DatabaseHelper.columnPostUserId} = ?',
whereArgs: [userId],
orderBy: '${DatabaseHelper.columnPostCreatedAt} DESC',
);
return List.generate(maps.length, (i) {
return Post.fromMap(maps[i]);
});
}
// 全投稿取得
static Future<List<Post>> getAllPosts() async {
final Database db = await DatabaseHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
DatabaseHelper.tablePosts,
orderBy: '${DatabaseHelper.columnPostCreatedAt} DESC',
);
return List.generate(maps.length, (i) {
return Post.fromMap(maps[i]);
});
}
// 投稿削除
static Future<bool> deletePost(int id) async {
final Database db = await DatabaseHelper.database;
try {
int count = await db.delete(
DatabaseHelper.tablePosts,
where: '${DatabaseHelper.columnPostId} = ?',
whereArgs: [id],
);
return count > 0;
} catch (e) {
print('投稿削除エラー: $e');
return false;
}
}
}
高度なクエリとJOIN操作
class AdvancedQueries {
// ユーザーと投稿のJOIN
static Future<List<UserWithPosts>> getUsersWithPosts() async {
final Database db = await DatabaseHelper.database;
// ユーザー取得
final users = await UserRepository.getAllUsers();
List<UserWithPosts> result = [];
for (User user in users) {
final posts = await PostRepository.getPostsByUserId(user.id!);
result.add(UserWithPosts(user: user, posts: posts));
}
return result;
}
// 生SQLでの複雑なJOINクエリ
static Future<List<Map<String, dynamic>>> getUserPostStats() async {
final Database db = await DatabaseHelper.database;
final List<Map<String, dynamic>> result = await db.rawQuery('''
SELECT
u.${DatabaseHelper.columnUserId} as user_id,
u.${DatabaseHelper.columnUserName} as user_name,
u.${DatabaseHelper.columnUserEmail} as user_email,
COUNT(p.${DatabaseHelper.columnPostId}) as post_count,
MAX(p.${DatabaseHelper.columnPostCreatedAt}) as latest_post_date
FROM ${DatabaseHelper.tableUsers} u
LEFT JOIN ${DatabaseHelper.tablePosts} p
ON u.${DatabaseHelper.columnUserId} = p.${DatabaseHelper.columnPostUserId}
GROUP BY u.${DatabaseHelper.columnUserId}, u.${DatabaseHelper.columnUserName}, u.${DatabaseHelper.columnUserEmail}
ORDER BY post_count DESC
''');
return result;
}
// 日付範囲での投稿検索
static Future<List<Post>> getPostsByDateRange(DateTime startDate, DateTime endDate) async {
final Database db = await DatabaseHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
DatabaseHelper.tablePosts,
where: '${DatabaseHelper.columnPostCreatedAt} BETWEEN ? AND ?',
whereArgs: [startDate.toIso8601String(), endDate.toIso8601String()],
orderBy: '${DatabaseHelper.columnPostCreatedAt} DESC',
);
return List.generate(maps.length, (i) {
return Post.fromMap(maps[i]);
});
}
// フルテキスト検索(LIKE使用)
static Future<List<Post>> searchPosts(String keyword) async {
final Database db = await DatabaseHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
DatabaseHelper.tablePosts,
where: '${DatabaseHelper.columnPostTitle} LIKE ? OR ${DatabaseHelper.columnPostContent} LIKE ?',
whereArgs: ['%$keyword%', '%$keyword%'],
orderBy: '${DatabaseHelper.columnPostCreatedAt} DESC',
);
return List.generate(maps.length, (i) {
return Post.fromMap(maps[i]);
});
}
// ページネーション対応
static Future<List<Post>> getPostsPaginated(int page, int pageSize) async {
final Database db = await DatabaseHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
DatabaseHelper.tablePosts,
orderBy: '${DatabaseHelper.columnPostCreatedAt} DESC',
limit: pageSize,
offset: page * pageSize,
);
return List.generate(maps.length, (i) {
return Post.fromMap(maps[i]);
});
}
}
トランザクション処理とバッチ操作
class TransactionOperations {
// トランザクション使用例
static Future<void> createUserWithPosts(User user, List<Post> posts) async {
final Database db = await DatabaseHelper.database;
await db.transaction((txn) async {
try {
// ユーザー作成
final Map<String, dynamic> userMap = user.toMap();
userMap.remove(DatabaseHelper.columnUserId);
int userId = await txn.insert(
DatabaseHelper.tableUsers,
userMap,
);
// 投稿作成
for (Post post in posts) {
final Map<String, dynamic> postMap = post.toMap();
postMap.remove(DatabaseHelper.columnPostId);
postMap[DatabaseHelper.columnPostUserId] = userId;
await txn.insert(
DatabaseHelper.tablePosts,
postMap,
);
}
print('ユーザーと${posts.length}件の投稿を作成しました');
} catch (e) {
print('トランザクションエラー: $e');
rethrow; // ロールバックが実行される
}
});
}
// バッチ操作
static Future<void> batchInsertUsers(List<User> users) async {
final Database db = await DatabaseHelper.database;
Batch batch = db.batch();
for (User user in users) {
final Map<String, dynamic> userMap = user.toMap();
userMap.remove(DatabaseHelper.columnUserId);
batch.insert(
DatabaseHelper.tableUsers,
userMap,
);
}
try {
List<dynamic> results = await batch.commit(noResult: false);
print('バッチ処理完了: ${results.length}件のユーザーを作成');
} catch (e) {
print('バッチ処理エラー: $e');
rethrow;
}
}
// 複雑なトランザクション例
static Future<void> transferPostsBetweenUsers(int fromUserId, int toUserId) async {
final Database db = await DatabaseHelper.database;
await db.transaction((txn) async {
// ユーザー存在確認
final fromUser = await txn.query(
DatabaseHelper.tableUsers,
where: '${DatabaseHelper.columnUserId} = ?',
whereArgs: [fromUserId],
);
final toUser = await txn.query(
DatabaseHelper.tableUsers,
where: '${DatabaseHelper.columnUserId} = ?',
whereArgs: [toUserId],
);
if (fromUser.isEmpty || toUser.isEmpty) {
throw Exception('指定されたユーザーが見つかりません');
}
// 投稿の移転
int updatedCount = await txn.update(
DatabaseHelper.tablePosts,
{DatabaseHelper.columnPostUserId: toUserId},
where: '${DatabaseHelper.columnPostUserId} = ?',
whereArgs: [fromUserId],
);
print('$updatedCount件の投稿を移転しました');
});
}
}
実用的なFlutterウィジェット統合
// ユーザー一覧画面
class UserListScreen extends StatefulWidget {
@override
_UserListScreenState createState() => _UserListScreenState();
}
class _UserListScreenState extends State<UserListScreen> {
List<User> users = [];
bool isLoading = true;
@override
void initState() {
super.initState();
_loadUsers();
}
Future<void> _loadUsers() async {
try {
final loadedUsers = await UserRepository.getAllUsers();
setState(() {
users = loadedUsers;
isLoading = false;
});
} catch (e) {
setState(() {
isLoading = false;
});
_showErrorDialog('ユーザー読み込みエラー: $e');
}
}
Future<void> _addUser() async {
final user = User(
name: 'テストユーザー ${DateTime.now().millisecondsSinceEpoch}',
email: 'test${DateTime.now().millisecondsSinceEpoch}@example.com',
age: 25,
createdAt: DateTime.now(),
);
try {
await UserRepository.insertUser(user);
_loadUsers(); // リストを再読み込み
} catch (e) {
_showErrorDialog('ユーザー作成エラー: $e');
}
}
Future<void> _deleteUser(int userId) async {
try {
bool deleted = await UserRepository.deleteUser(userId);
if (deleted) {
_loadUsers(); // リストを再読み込み
}
} catch (e) {
_showErrorDialog('ユーザー削除エラー: $e');
}
}
void _showErrorDialog(String message) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('エラー'),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('OK'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('ユーザー一覧'),
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: _loadUsers,
),
],
),
body: isLoading
? Center(child: CircularProgressIndicator())
: users.isEmpty
? Center(child: Text('ユーザーがいません'))
: ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
leading: CircleAvatar(
child: Text(user.name.substring(0, 1)),
),
title: Text(user.name),
subtitle: Text('${user.email} • 年齢: ${user.age ?? '不明'}'),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => _deleteUser(user.id!),
),
onTap: () {
// ユーザー詳細画面への遷移
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => UserDetailScreen(user: user),
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: _addUser,
child: Icon(Icons.add),
),
);
}
}
// ユーザー詳細画面
class UserDetailScreen extends StatefulWidget {
final User user;
UserDetailScreen({required this.user});
@override
_UserDetailScreenState createState() => _UserDetailScreenState();
}
class _UserDetailScreenState extends State<UserDetailScreen> {
List<Post> posts = [];
bool isLoading = true;
@override
void initState() {
super.initState();
_loadUserPosts();
}
Future<void> _loadUserPosts() async {
try {
final userPosts = await PostRepository.getPostsByUserId(widget.user.id!);
setState(() {
posts = userPosts;
isLoading = false;
});
} catch (e) {
setState(() {
isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.user.name),
),
body: Column(
children: [
Padding(
padding: EdgeInsets.all(16.0),
child: Card(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('ユーザー情報', style: Theme.of(context).textTheme.titleLarge),
SizedBox(height: 8),
Text('名前: ${widget.user.name}'),
Text('メール: ${widget.user.email}'),
Text('年齢: ${widget.user.age ?? '不明'}'),
Text('作成日: ${widget.user.createdAt.toString().substring(0, 19)}'),
],
),
),
),
),
Expanded(
child: isLoading
? Center(child: CircularProgressIndicator())
: posts.isEmpty
? Center(child: Text('投稿がありません'))
: ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
final post = posts[index];
return Card(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: ListTile(
title: Text(post.title),
subtitle: Text(
post.content.length > 100
? '${post.content.substring(0, 100)}...'
: post.content,
),
trailing: Text(
post.createdAt.toString().substring(0, 16),
style: Theme.of(context).textTheme.bodySmall,
),
),
);
},
),
),
],
),
);
}
}