pomako3212tuli
@pomako3212tuli

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

データベースの変更が反映されない

解決したいこと

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;
    }

以上のように記述しましたが、なぜか「名前が入力されていません」と表示されません。
単語はきちんと登録できないようになっているのですが…

初心者のため、質問の仕方が下手で申し訳ございません。
どうか、よろしくお願いします。

0

1Answer

DB Browser for SQLite の Database Structure タブで Words テーブルの name カラムを見たとき、 Schema に UNIQUE が含まれていますか?

1Like

Comments

  1. @pomako3212tuli

    Questioner

    ご質問をいただき、ありがとうございます。
    なぜか、私のDB Browser for SQLiteは日本語で、見ているところが合っているのか自信がないのですが、UNIQUEは含まれていないように思います。
    スクリーンショットを添付します。sqliteスクショ.png

  2. そこで合っています。 text().unique()() による UNIQUE 制約が効いていないようですね。また明示的に .nullable() を呼ばないと NOT NULL 制約がつくはずですが、それもすべてのカラムについていないようです。スキーマの適用がうまくいっていないのかもしれません。

    データベースファイルを消し、 Words の定義に適当なカラムを追加して、データベースコードを再生成してからアプリを開いたとき、新しいデータベースファイルにカラムは追加されていますか?

  3. @pomako3212tuli

    Questioner

    ご回答いただいたのに、返信が遅くなってしまい、申し訳ございません。
    カラムをひとつ追加してみましたが、きちんと追加できていないようでした。

    今更なのですが、ひとつ大事なことを書き忘れていました。
    私のアプリでは、Excelで作ったcsvファイルからデータベースを作って、それをassetsフォルダに入れ、読み込ませるようにしています。
    もしかしたら、こちらのやり方が間違っているのかもしれないと、カラムを追加しながら思いました…

    void main() async{
      WidgetsFlutterBinding.ensureInitialized();
      var dbPath = await getDbPath();
      database = MyDatabase(dbPath: dbPath);
    
      runApp(MyApp());
    }
    
    Future<String> getDbPath() async{
      var dbDir = await getApplicationDocumentsDirectory();
      var dbPath = join(dbDir.path, "my_database.db");
    
      if(FileSystemEntity.typeSync(dbPath) == FileSystemEntityType.notFound){
        ByteData byteData = await rootBundle.load("assets/db/my_database.db");
        List<int> bytes = byteData.buffer.asUint8List(byteData.offsetInBytes, byteData.lengthInBytes);
        await File(dbPath).writeAsBytes(bytes);
      }
      return dbPath;
    }
    

    前回の私の質問をご覧いただいた方がよいかもしれません。
    https://qiita.com/pomako3212tuli/questions/60fa822c34e0ad5d0003)

    何かヒントをいただけると幸いです。
    どうぞよろしくお願いいたします。

  4. そのやり方でも構いませんが、 assets からコピーしてデータベースファイルを作るのであれば、 Words クラスのスキーマ定義はデータベースに影響しません。 CSV からデータベースファイルを作る時点で UNIQUE 制約などを設定し、それと一致するように Words クラスを変更してください。

  5. @pomako3212tuli

    Questioner

    今、データベースファイルを作り直してアプリを起動してみたら、無事にユニーク制約が効きました!!
    本当にありがとうございます。
    最初に作る時にユニークにしないといけないことを、全く知りませんでした。
    かなりの時間を費やしても解決しなかったのに、uasiさんに教えていただいて、やっと解決することができました。
    御礼申し上げます。

Your answer might help someone💌