🐕

【Flutter】sqfliteパッケージによるDBファイルの配置場所について、もう少し考えてみる

2022/02/15に公開

「何」を考えるのか

sqfliteは、FlutterでSQLiteを利用する際によく用いられるパッケージです。
SQLiteで管理するデータは、「〇〇.db」のようなファイル名で、端末内に保存されることになります。

この「〇〇.db」のファイルを、端末のどこに配置したらいいのか、について今回、考えたいと思います。

後々、コード例を2パターン書きましたが、あくまで一例としてみていただけると幸いです。

長々と説明ありますが、知って欲しいと思っていることは、
iOSの場合、sqfliteに用意されているgetDatabasePathには注意が必要(使わない選択肢がドキュメントに言及されている)
ということだけです。

書くこと・書かないこと

  • 書くこと
     配置場所に関する話(オンリー)
  • 書かないこと
     sqfliteの詳しい使い方
     こちらはドキュメント等、ご参照ください。

要件

配置場所の要件は、セキュリティを担保するため(外から悪さができないよう)
外部から(ユーザーからも他のアプリからも)アクセスできない場所とします。

外部からアクセスできるようにする場合とは記述方法が異なるのでご注意ください。(私には経験のないパターンです)

sqfliteに用意されている「getDatabasePath」について

sqfliteには「getDatabasePath」というそれらしいメソッドが用意されています。sqfliteの使い方を説明した記事でも、このメソッドを使ったものが多い印象です。
では「getDatabasePath」では、どこのパスを取得することになるのでしょうか。
「getDatabasePath」のドキュメントをみてみると、以下のように説明されています。

Get the default databases location.
On Android, it is typically data/data/
On iOS and MacOS, it is the Documents directory.
Note for iOS: Using path_provider is recommended to get the databases directory. The most appropriate location on iOS would be the Library directory that you could get from the path_provider package (https://pub.dev/documentation/path_provider/latest/path_provider/getLibraryDirectory.html).

一行目で「デフォルトのデータベースの場所を取得する」とあり、AndroidとiOSのそれぞれの説明が続きます。
そう、特筆すべきは、AndroidとiOSで挙動が異なる ことです。

プラットフォーム別に挙動が異なるので、以下プラットフォームごとに考えていきます。

Androidの場合

「getDatabasePath」の話をする前に、
DBファイルをどこに保存すべきか、Androidでは「セキュアコーディングガイド」というものに記述があったります。

参考)
https://qiita.com/serisawa/items/62c996c23c4513e3b253
https://www.jssec.org/dl/android_securecoding.pdf
https://www.jssec.org/dl/android_securecoding_20201101/4_using_technology_in_a_safe_way.html#:~:text=どちらのメソッドも該当するアプリだけが読み書き権限を与えられ、他のアプリからはアクセスができないディレクトリ(パッケージディレクトリ)のサブディレクトリ以下のパスが取得できる。
「配置場所」で検索しすると該当の記述が出てきます。
2020-11-01の方はWebサイトで閲覧できますが、リンク切れになるかもしれません。

引用すると

Context#getDatabasePath(String name)、もしくはContext#getFilesDirで取得できるディレクトリに配置する

とのこと。
上記メソッドから返却されるパスは外部からアクセスできない場所であり、要件を満たします。

さて「getDatabasePath」のAndroidの説明に戻ると

On Android, it is typically data/data/

とあります。
実はこれContext#getDatabasePath(String name)の結果を返しています。
GithubのコードさかのぼっていくとonGetDatabasesPathCallといメソッドに行き着き、そこでContext#getDatabasePath(String name)を呼び出していることがわかります。
Githubで該当メソッドが書かれているファイル

ちょっと不親切かもしれませんが、詳しいたどり方についてはここでは書きません。(あまり書くのもよくない気がするので)
ネイティブのどこの処理を実行しているかは、以下を参考にたどりました。
https://qiita.com/kurun_pan/items/db6c8fa94bbfb5c0c8d7

Context#getDatabasePath(String name)が呼び出されているということなので、セキュアコーディングガイドを満たしています。
AndroidではgetDatabasePathを使っても問題なさそうです。

iOSの場合

iOSの場合、「getDatabasePath」のドキュメントに詳細な説明は記載されています。

On iOS and MacOS, it is the Documents directory.
Note for iOS: Using path_provider is recommended to get the databases directory. The most appropriate location on iOS would be the Library directory that you could get from the path_provider package (https://pub.dev/documentation/path_provider/latest/path_provider/getLibraryDirectory.html).

まず、getDatabasePathでは「ドキュメントディレクトリ」を返すとのこと。
このドキュメントディレクトリはユーザのアクセスを許すこともできるので、今回の要件では不適切だと言えます。

各ディレクトリの説明)
https://docs.microsoft.com/ja-jp/xamarin/ios/app-fundamentals/file-system#application-directories

よって、ドキュメントで薦められている通り、path_providergetLibraryDirectoryを利用するのがいいかと思います。

(コード例は後で)
getLibraryDirectoryはライブラリディレクトリを返し、ライブラリディレクトリはドキュメントにあるように、ユーザや他のアプリからアクセスすることはできません。
また、ライブラリディレクトリ内であれば、自動でバックアップも取ってくれると説明では書いてあります。

https://pub.dev/packages/path_provider
https://pub.dev/documentation/path_provider/latest/path_provider/getLibraryDirectory.html

path_providerを使った別の選択肢

path_providerを使うのであれば、別の選択肢も生まれてくるでしょう。
スマートフォンアプリで関わるディレクトリ&内部ストレージのものに限定すると以下のメソッドたちが考えられます。
(ドキュメントの日本語訳をざっくりのせただけなので、正確な情報を確認したい場合はドキュメントを参照ください)

メソッド名 説明
getApplicationDocumentsDirectory ユーザーが作成したデータや、アプリケーションでは再現できないデータをアプリケーションが置くことができるディレクトリへのパス。

Androidの利用API : getDataDirectory API,
iOSの利用API : NSDocumentDirectory API
getApplicationSupportDirectory アプリケーションがアプリケーションサポートファイルを置くことができるディレクトリへのパス。ユーザーに公開したくないファイルに使用してください。アプリでは、このディレクトリをユーザーデータファイルに使用してはいけません。

Androidの利用API : getFilesDir API,
iOSの利用API : NSApplicationSupportDirectory API
getLibraryDirectory sqlite.dbなど、永続的にバックアップされ、ユーザーには表示されないファイルをアプリケーションが保存するためのディレクトリへのパス。Androidでは、同等のパスが存在しません。

iOSの利用API : NSLibraryDirectory API
※ソースコードたどりました
getTemporaryDirectory バックアップされていない、ダウンロードしたファイルのキャッシュを保存するのに適したデバイス上の一時ディレクトリへのパス。このディレクトリのファイルは、いつでも消去することができます。

Androidの利用API : getCacheDir API,
iOSの利用API : NSCachesDirectory API

getApplicationDocumentsDirectoryはAndroidもiOSもユーザからアクセスできる場所を提供するので不適切です。
またgetTemporaryDirectoryも名前の通り永続化できないディレクトリであり、DBファイルを保存するべきではありません。

よって、path_providerで要件を満たすのは

Android
getApplicationSupportDirectory(getFilesDirを利用している)

iOS
getApplicationSupportDirectory
getLibraryDirectory

考えられるコード例

Android・iOS両プラットフォームにリリースするアプリだとして書きます。

データベースを開く(なければ作成する)処理

import 'package:sqflite/sqflite.dart';

// データベースを開く(なければ作成する)処理のみ
Future<Database> getDatabase() async {
  // getDbPathは自作メソッド
  final path = await getDbPath();
  final db = await openDatabase(path, version: 1,
      onCreate: (Database db, int version) async {
    await db.execute('CREATE TABLE ..省略..');
  });
  return db;
}

getDbPathの中身が、今回述べた部分です。
以下に例として2パターン書きます。

①getDatabasePathのドキュメントの説明に従う場合

import 'dart:io';

import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';

Future<String> getDbPath() async {
  var dbFilePath = '';

  if (Platform.isAndroid) {
    // Androidであれば「getDatabasesPath」を利用
    dbFilePath = await getDatabasesPath();
  } else if (Platform.isIOS) {
    // iOSであれば「getLibraryDirectory」を利用
    final dbDirectory = await getLibraryDirectory();
    dbFilePath = dbDirectory.path;
  } else {
    // プラットフォームが判別できない場合はExceptionをthrow
    // 簡易的にExceptionをつかったが、自作Exceptionの方がよいと思う。
    throw Exception('Unable to determine platform.');
  }
  // 配置場所のパスを作成して返却
  final path = join(dbFilePath, 'sample.db');
  return path;
}

Exceptionをthrowするかは意見が分かれるところかもしれません。
私は万一「ありえない場合」が起きたときにDBファイルの作成をしてほしくなかったので、上記のようにしました。

②プラットフォームごとに場合分けをしたくない場合

import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';

Future<String> getDbPath() async {
  final dbDirectory = await getApplicationSupportDirectory();
  final dbFilePath = dbDirectory.path;
  final path = join(dbFilePath, 'sample.db');
  return path;
}

path_providerで両プラットフォーム条件を満たしたgetApplicationSupportDirectoryを使いました。
こちらの方が、記述としてはすっきりしますね。
もし、該当するディレクトリが返せない場合はgetApplicationSupportDirectory内でExceptionがthrowされます。

まとめ

①getDatabasePathのドキュメントの説明に従う場合、
AndroidはsqflitegetDatabasePathを利用
iOSはpath_providergetLibraryDirectoryを利用して、
プラットフォームごとに場合分けをします。

こちらの方が、各種ドキュメントで想定されるDBファイルの配置場所になります。

②プラットフォームごとに場合分けをしたくない場合
path_providergetApplicationSupportDirectory利用すれば要件を満たせます。

ここからは、個人的コメントですが、
②にするのはクロスプラットフォームの都合(path_providerの仕様の都合)を押し付けている感があるように思います。
①だと、各種ドキュメントの後押しがありますが、「自然なDBファイル配置場所となる分、予測しやすいんじゃないか」と考えることもできます。

一例ですので、何を採用するかはおまかせします。

ただ最初に書いた通り、iOSの場合はsqfliteに用意されているgetDatabasePath使うのは避けた方がいい、と私は思います。
path_providerを使っていても、getApplicationDocumentsDirectory使っている例も多いですが、私は避けた方がいいと思います)

最後の紹介

個人開発でFlutter製のアプリをリリースしました🎉🎉🎉🎉🎉🎉🎉
今回の話は、個人開発時に調べたこぼれ話でした。

Android
https://play.google.com/store/apps/details?id=work.sendfun.explore_yourself_app

iOS
https://apps.apple.com/jp/app/一日一問/id1606720822

「一日一問」というアプリで、
「問いかけ」を記録しておいて、ランダム表示する「無理なく考えたいことを考える」アプリです。
良ければつかってみてください〜

使い方
https://scrapbox.io/beeeyan-trial-box/一日一問の使い方

以上です!

Discussion