💾

【Flutter】日記アプリのDBデータのインポート方法

2025/03/05に公開

DB データのファイルをインポート

個人開発で日記アプリを開発しており、データのバックアップ・インポート処理を実装する必要があるため、キャッチアップした内容を記しておきます。

前回、エクスポートに関する記事を記載していますので、先にこちらをご覧いただきたいです。今回はその記事の続編となっています。こちらの記事では DB ファイル・Txt ファイル・CSV ファイルでのエクスポート方法を紹介しており、今回はこれらのファイルをインポートする記事となります。

https://zenn.dev/muranaka/articles/aeadf9050be966

サンプルコード

https://github.com/muranakar/file_import

実装例

Simulator Screen Recording - iPhone 16 Pro Max - 2025-02-05 at 16.21.22.gif

ファイル選択

まずは、各インポート方法に共通するファイル選択処理を実装します。この部分では、file_picker パッケージを使用してファイル選択ダイアログを表示します。

// データベースファイルをインポートする関数
Future<void> _importDatabase(BuildContext context, WidgetRef ref) async {
  try {
    // FilePickerを使用してファイル選択ダイアログを表示
    // FileType.anyを指定することで、すべての種類のファイルを選択可能にします
    final result = await FilePicker.platform.pickFiles(type: FileType.any);

    // ファイルが選択された場合の処理
    if (result != null && result.files.single.path != null) {
      // 選択されたファイルのパスからFileオブジェクトを作成
      final file = File(result.files.single.path!);

      // データベースインポート処理を実行
      await DiaryDatabase.instance.importDatabase(file);

      // Riverpodのプロバイダーを更新して、UIを最新の状態に反映
      ref.invalidate(diariesProvider);

      // 成功メッセージをスナックバーで表示
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('データベースをインポートしました')),
      );

      // インポート完了後に前の画面に戻る
      Navigator.pop(context, true);
    }
  } catch (e) {
    // エラーが発生した場合はエラーメッセージを表示
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('インポートに失敗しました: $e')),
    );
  }
}

データベースファイルインポートの実装

データベースファイルを直接インポートする処理を実装します。この方法は、既存のデータベースを置き換えます。データベースを削除すると、再度 DB の初期設定等が必要となるため、今回は上書き処理としました。

/// データベースファイルを直接インポートする関数
/// @param file インポートするデータベースファイル
Future<void> importDatabase(File file) async {
  // アプリケーションのデータベースディレクトリのパスを取得
  // getDatabasesPath()は、プラットフォーム固有のデータベース保存場所を返します
  final dbPath = await getDatabasesPath();

  // 既存のデータベースファイルパスを生成
  // joinは、プラットフォーム固有のパス区切り文字を使用してパスを結合します
  final dbFile = File(join(dbPath, 'diary.db'));

  // 新しいデータベースファイルで既存のファイルを上書き
  // copyメソッドは、ファイルのバイナリコピーを作成します
  await file.copy(dbFile.path);

  // データベース接続を再初期化
  // これにより、新しいデータベースファイルを使用するように設定されます
  database = await initDB('diary.db');
}

テキストファイルインポートの実装

テキストファイルからデータをインポートする処理を実装します。特定のフォーマットに従ったテキストファイルを解析します。この方法は、一つのテーブルの読み込みであればフォーマットを解析して、苦労せずに対応可能です。ですが、複数のテーブルが存在する場合は、処理が多くなりコード量が増える可能性があるため、DB ファイルのインポートをおすすめします。正直インポートには向いていませんが、ユーザーが txt ファイルを用いて DB にインポートしたい場合は実装する必要があります。

/// テキストファイルから日記データをインポートする関数
/// テキストファイルは以下の形式である必要があります:
/// タイトル: [タイトル]
/// 本文: [内容]
Future<void> importFromTxt(File file) async {
  try {
    // ファイルの内容を文字列として読み込み
    // readAsString()は、ファイル全体をUTF-8エンコードされた文字列として読み込みます
    final content = await file.readAsString();

    // ファイルの内容を行単位で分割
    // split('\n')は、改行文字で文字列を分割して配列を作成します
    final lines = content.split('\n');

    // データベース接続を取得
    final db = await instance.database;

    // 各行を解析してデータベースに挿入
    for (var i = 0; i < lines.length; i++) {
      // タイトル行を検出
      // startsWith()を使用して、行が「タイトル: 」で始まるかチェック
      if (lines[i].startsWith('タイトル: ')) {
        // タイトルを抽出(「タイトル: 」の部分を除去)
        final title = lines[i].substring(6);

        // 次の行から本文を抽出(「本文: 」の部分を除去)
        final content = lines[i + 1].substring(3);

        // データベースに新しい日記エントリーを挿入
        // ConflictAlgorithm.replaceは、同じIDのデータが存在する場合は上書きする
        await db.insert(
          'diaries',
          {
            'title': title,
            'content': content,
            // 現在の日時を作成日時として使用
            'created_at': DateTime.now().toIso8601String(),
          },
          conflictAlgorithm: ConflictAlgorithm.replace,
        );
      }
    }
  } catch (e) {
    // エラーが発生した場合は、上位層に伝播させる
    rethrow;
  }
}

CSV ファイルインポートの実装

CSV ファイルからデータをインポートする処理を実装します。この方法は、構造化されたデータを効率的にインポートできます。スプレットシートなどで入力して、それを DB にインポートしたい場合は活用できます。Txt ファイル同様で一つのテーブルを構造化してデータをインポートする分には問題ないが、テーブルが増えると処理が多くなるため、DB ファイルのインポートをおすすめします。

/// CSVファイルから日記データをインポートする関数
/// CSVは以下の形式である必要があります:
/// ID,タイトル,本文,作成日時
Future<void> importFromCsv(File file) async {
  try {
    // ファイルの内容を文字列として読み込み
    final content = await file.readAsString();

    // ファイルの内容を行単位で分割
    final lines = content.split('\n');

    // データベース接続を取得
    final db = await instance.database;

    // 各行を処理(1行目はヘッダーなのでスキップ)
    for (var i = 1; i < lines.length; i++) {
      // 行をカンマで分割してフィールドを取得
      final values = lines[i].split(',');

      // 必要なフィールドがすべて存在することを確認
      if (values.length >= 4) {
        // 各フィールドを適切な型に変換
        final id = int.parse(values[0]);  // IDを数値に変換
        final title = values[1].replaceAll('"', '');  // ダブルクォートを除去
        final content = values[2].replaceAll('"', '');  // ダブルクォートを除去
        final createdAt = values[3];

        // デバッグ用のログ出力
        print('インポート中のデータ:');
        print('ID: $id');
        print('タイトル: $title');
        print('本文: $content');
        print('作成日時: $createdAt');

        // データベースに挿入
        await db.insert(
          'diaries',
          {
            'id': id,
            'title': title,
            'content': content,
            'created_at': createdAt,
          },
          conflictAlgorithm: ConflictAlgorithm.replace,
        );
      }
    }
  } catch (e) {
    // エラーが発生した場合は、上位層に伝播させる
    rethrow;
  }
}

以上になります。

サンプルコード

https://github.com/muranakar/file_import

Discussion