データベースの変更が反映されない
解決したいこと
Flutterで単語帳アプリを作っています。
データベースは、Driftというパッケージを使用しています。
(https://pub.dev/packages/drift)
「名前(name)」の項目をユニークキーにしたくて、追加で設定を加えたのですが、反映されません。
解決方法を教えていただけると嬉しいです。
※かなりのプログラミング初心者です。
発生している問題・エラー
ユニークキーが反映されず、同じ名前で単語登録ができてしまいます。
該当するソースコード
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
import 'package:sqlite3/sqlite3.dart';
part 'database.g.dart';
class Words extends Table {
TextColumn get name => text().unique()();//ここでユニークキーを設定しました。
TextColumn get english => text()();
TextColumn get read => text()();
TextColumn get sort => text()();
IntColumn get rank => integer()();
TextColumn get start => text()();
TextColumn get end => text()();
TextColumn get nerve => text()();
TextColumn get medulla => text()();
TextColumn get action => text()();
TextColumn get sub => text()();
TextColumn get comment => text()();
@override
Set<Column<Object>>? get primaryKey => {name};//プライマリーキーもnameで設定しました。
}
@DriftDatabase(tables: [Words])
class MyDatabase extends _$MyDatabase {
final String dbPath;
MyDatabase({required this.dbPath})
: super(_openConnection(dbPath));
@override
int get schemaVersion => 1;
/*
@override
MigrationStrategy get migration {
return MigrationStrategy(
onCreate: (Migrator m) async {
await m.createAll();
},
onUpgrade: (Migrator m, int from, int to) async {
if (from < 2) {
// we added the dueDate property in the change from version 1 to
// version 2
await m.createTable(words);
}
},
);
}
*/
//スキーマバージョンを2にあげて、マイグレーションをしたらいいのかと思ったけど、
//これをしてもできませんでした。
//Creates
Future addWord(Word word) => into(words).insert(word);
//Read
Future<List<Word>> get allWords => select(words).get();
//Update
Future updateWord(Word word) => update(words).replace(word);
//Delete
Future deleteWord(Word word) =>
(delete(words)..where((t) => t.name.equals(word.name))).go();
}
LazyDatabase _openConnection(String dbPath) {
return LazyDatabase(() async {
final file = File(dbPath);
if (Platform.isAndroid) {
await applyWorkaroundToOpenSqlite3OnOldAndroidVersions();
}
final cachebase = (await getTemporaryDirectory()).path;
sqlite3.tempDirectory = cachebase;
return NativeDatabase.createInBackground(file);
});
}
単語の追加・編集画面(ここは不要なのかもしれませんが、一応…)
//「編集画面」
import 'package:drift/native.dart';
import 'package:drift/remote.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:kinnikutango/db/database.dart';
import 'package:kinnikutango/main.dart';
import 'package:kinnikutango/parts/item_field.dart';
import 'package:kinnikutango/screens/wordlist.dart';
enum EditStatus { ADD, EDIT }
class EditScreen extends StatefulWidget {
final EditStatus status;
final Word? word;
EditScreen({required this.status, this.word});
@override
State<EditScreen> createState() => _EditScreenState();
}
class _EditScreenState extends State<EditScreen> {
TextEditingController sortController = TextEditingController();
TextEditingController nameController = TextEditingController();
TextEditingController englishController = TextEditingController();
TextEditingController readController = TextEditingController();
TextEditingController startController = TextEditingController();
TextEditingController endController = TextEditingController();
TextEditingController nerveController = TextEditingController();
TextEditingController medullaController = TextEditingController();
TextEditingController actionController = TextEditingController();
TextEditingController subController = TextEditingController();
TextEditingController commentController = TextEditingController();
int _selectedRank = 1;
String _titleText = "";
@override
void initState() {
super.initState();
if (widget.status == EditStatus.ADD) {
_titleText = "新しい単語の追加";
sortController.text = "";
nameController.text = "";
englishController.text = "";
readController.text = "";
startController.text = "";
endController.text = "";
nerveController.text = "";
medullaController.text = "";
actionController.text = "";
subController.text = "";
commentController.text = "";
_selectedRank = 1;
} else {
_titleText = "登録した単語の修正";
sortController.text = widget.word!.sort;
nameController.text = widget.word!.name;
englishController.text = widget.word!.english;
readController.text = widget.word!.read;
startController.text = widget.word!.start;
endController.text = widget.word!.end;
nerveController.text = widget.word!.nerve;
medullaController.text = widget.word!.medulla;
actionController.text = widget.word!.action;
subController.text = widget.word!.sub;
commentController.text = widget.word!.comment;
_selectedRank = widget.word!.rank;
}
}
@override
void dispose() {
sortController.dispose();
nameController.dispose();
englishController.dispose();
readController.dispose();
startController.dispose();
endController.dispose();
nerveController.dispose();
medullaController.dispose();
actionController.dispose();
subController.dispose();
commentController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvoked: (bool didPop) {
if (didPop) return;
_backToWordListScreen();
},
child: Scaffold(
appBar: AppBar(
title: Text(_titleText),
centerTitle: true,
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () => _backToWordListScreen(),
tooltip: "一覧画面",
),
actions: [
IconButton(
icon: Icon(Icons.done),
onPressed: () => _onWordRegistered(),
tooltip: "登録",
),
],
),
body: SingleChildScrollView(
child: Column(
children: [
_sortInputPart(),
_nameInputPart(),
_englishInputPart(),
_rankInputPart(),
_readInputPart(),
_startInputPart(),
_endInputPart(),
_nerveInputPart(),
_medullaInputPart(),
_actionInputPart(),
_subInputPart(),
_commentInputPart(),
],
),
),
),
);
}
Widget _sortInputPart() {
return ItemField(
fieldController: sortController,
label: "分類",
);
}
Widget _nameInputPart() {
return ItemField(
fieldController: nameController,
label: "名前",
);
}
Widget _englishInputPart() {
return ItemField(
fieldController: englishController,
label: "英語",
);
}
Widget _rankInputPart() {
return DropdownButton<int>(
value: _selectedRank,
onChanged: (int? newValue) {
if (newValue != null) {
setState(() {
_selectedRank = newValue;
});
}
},
items: <int>[1, 2, 3, 4, 5].map<DropdownMenuItem<int>>((int value) {
return DropdownMenuItem<int>(
value: value,
child: Text(value.toString()),
);
}).toList(),
);
}
Widget _readInputPart() {
return ItemField(
fieldController: readController,
label: "読み方",
);
}
Widget _startInputPart() {
return ItemField(
fieldController: startController,
label: "起始",
);
}
Widget _endInputPart() {
return ItemField(
fieldController: endController,
label: "停止",
);
}
Widget _nerveInputPart() {
return ItemField(
fieldController: nerveController,
label: "神経",
);
}
Widget _medullaInputPart() {
return ItemField(
fieldController: medullaController,
label: "髄節",
);
}
Widget _actionInputPart() {
return ItemField(
fieldController: actionController,
label: "作用",
);
}
Widget _subInputPart() {
return ItemField(
fieldController: subController,
label: "補助作用",
);
}
Widget _commentInputPart() {
return ItemField(
fieldController: commentController,
label: "コメント",
);
}
void _backToWordListScreen() {
Navigator.pushReplacement(
context, MaterialPageRoute(builder: (context) => WordListScreen()));
}
_onWordRegistered() {
if (widget.status == EditStatus.ADD) {
_insertWord();
} else {
_updateWord();
}
}
_insertWord() async {
if (nameController.text == "") {
SnackBar(
content: Text('名前が入力されていません'),
duration: Duration(seconds: 3),
);
return;
}
var word = Word(
sort: sortController.text,
name: nameController.text,
english: englishController.text,
rank: _selectedRank,
read: readController.text,
start: startController.text,
end: endController.text,
nerve: nerveController.text,
medulla: medullaController.text,
action: actionController.text,
sub: subController.text,
comment: commentController.text,
);
try {
await database.addWord(word);
print("単語をデータベースに追加しました");
sortController.clear();
nameController.clear();
englishController.clear();
readController.clear();
startController.clear();
endController.clear();
nerveController.clear();
medullaController.clear();
actionController.clear();
subController.clear();
commentController.clear();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('登録が完了しました'),
duration: Duration(seconds: 3),
),
);
} on SqliteException catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('この名前は既に登録されていますので登録できません'),
duration: Duration(seconds: 3),
),
);
} on DriftRemoteException catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('この名前は既に登録されていますので登録できません'),
duration: Duration(seconds: 3),
),
);
}
}
void _updateWord() async {
if (nameController.text == "") {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('名前が入力されていません'),
duration: Duration(seconds: 3),
),
);
return;
}
var word = Word(
sort: sortController.text,
name: nameController.text,
english: englishController.text,
rank: _selectedRank,
read: readController.text,
start: startController.text,
end: endController.text,
nerve: nerveController.text,
medulla: medullaController.text,
action: actionController.text,
sub: subController.text,
comment: commentController.text,
);
try {
await database.updateWord(word);
_backToWordListScreen();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('修正が完了しました'),
duration: Duration(seconds: 3),
),
);
} on SqliteException catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('何らかの問題が発生して登録できませんでした。$e'),
duration: Duration(seconds: 3),
),
);
} on DriftRemoteException catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('何らかの問題が発生して登録できませんでした。$e'),
duration: Duration(seconds: 3),
),
);
}
}
}
自分で試したこと
◆まだリリースもしていなくて開発中なので、スキーマバージョンを上げたりマイグレーションの記述をしませんでした。代わりに、データベースのパスを取得してきて、手動でそのデータベースを削除して、新たにアプリを開き直しました。
→開き直す前に、アプリ上で追加で登録した単語が消えて、きちんと初期のデータベースが表示されましたが、ユニークキーは効きませんでした。
◆スキーマバージョンを上げてマイグレーションの記述をしてみましたが、ダメでした。
◆
TextColumn get name => text()();
と、
@override
Set<Column<Object>>? get primaryKey => {name};
の組み合わせでもだめでした。
◆逆に、
TextColumn get name => text()unique()();
とプライマリーキーを設定しない組み合わせもだめでした。
(調べたら、ユニークキーとプライマリーキーは別物みたいなので、上記二つは意味ないかもしれません。)
◆DB Browser for SQLiteで、
PRAGMA table_info(Words);
を実行してみたら、nameカラムのpk列の値が1でないといけないらしいのに、0でした。(1だと、nameカラムがプライマリキー(ユニークキー)として設定されていることを示しているそうです)
◆公式リファレンスによると、カスタム制約?を使ったちょっと違ったユニークキーの設定の仕方も載っていたので、それも試しましたが、だめでした。
TextColumn get name => text().customConstraint('UNIQUE NOT NULL')();
@override
List<String> get customConstraints => [
'UNIQUE(name)'
];
◆データベースコードの自動生成は以下の二つを試しました。
①「コードに変更を加えて再生成したい場合は、
flutter pub run build_runner build --delete-conflicting-outputs
を実行しましょう。」
とFlutter大学の記事で書かれていたので、その通りにしました。
(https://blog.flutteruniv.com/flutter-drift/)
②再生成ではなく、自動生成されたファイルを削除して、新たに作り直しました。
flutter pub run build_runner build
↑このコマンド
他に困っていること
ちなみに、関係あるのかないのか、分かりませんが、他にも困ったことがあります。
void _updateWord() async {
if (nameController.text == "") {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('名前が入力されていません'),
duration: Duration(seconds: 3),
),
);
return;
}
以上のように記述しましたが、なぜか「名前が入力されていません」と表示されません。
単語はきちんと登録できないようになっているのですが…
初心者のため、質問の仕方が下手で申し訳ございません。
どうか、よろしくお願いします。