Flutter on the WebでSQLiteを使う

SQLite本家によるwasmサポートが開始されており、ブラウザ上でもSQLiteが使えるようになっています。

wasm版のSQLiteは1MBを切るという軽量具合なので、Webアプリで使うには嬉しいですね☺️

Flutter Webでももちろん使えるので、やり方メモを残します。

# 導入

Dart用のORMであるDriftを作られている方による、sqlite.wasmのビルド済みバイナリがあります。

ありがてぇ、ありがてぇ。

https://github.com/simolus3/sqlite3.dart/releases

そのままDriftを使うという道もありますが、build_runnerを使う仕組みなので一定規模以下だとメンドクセェ_:(´ཀ`」 ∠):_

ちょろっとDB使いたいだけの時にORMは大袈裟なので、sqlite3パッケージで直接SQL叩く方が簡単だと思います(プレースホルダが使えます)

sqlite3パッケージはプロジェクトフォルダ内で次のコマンドで追加します。

```shell
> flutter pub add sqlite3
```
sqlite3.wasmはライブラリなのでlib下(のwebの下)に配置します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140718/607759b0-3827-8843-bc12-9a1acad5aff0.png)

# 利用方法

基本的な流れとしては

1. sqlite3.wasmを読み込む
1. ファイル置き場を決める(メモリ or ストレージ)
1. ファイル名を決めてopen

となります。

### インメモリDBで使用する

読み込みも保存もしなくて本当にちょっと使いたい時用です。

基本的には動作確認で使用することになると思います。

```dart
// sqlite3.wasmを読み込む
final sqlite3 = await WasmSqlite3.loadFromUrl(Uri.parse('sqlite3.wasm'));

// ファイル置き場を決める(メモリ)
sqlite3.registerVirtualFileSystem(InMemoryFileSystem(), makeDefault: true);

// ファイル名を決めてopen(":memory:"(SQLite規定値)。ラッパー関数が使える)
var db = sqlite3.openInMemory();
```

### ブラウザのストレージに保存する

ユーザーデータなどを置くのはここになると思います。

```dart
// sqlite3.wasmを読み込む
final sqlite3 = await WasmSqlite3.loadFromUrl(Uri.parse('sqlite3.wasm'));

// ファイル置き場を決める(IndexedDB)
final fileSystem = await IndexedDbFileSystem.open(dbName: 'indexedDBの名前');
sqlite3.registerVirtualFileSystem(fileSystem, makeDefault: true);

// ファイル名を決めてopen
var db = sqlite3.open("ファイル名");
```

こんな感じで保存されます。

![456115520_8839903346092574_8604917256199807913_n.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140718/bbd0701b-4e40-f2f5-9358-ed90529d9f38.jpeg)

### Web上のファイルを読み込む

作成済みのデータを読み込む時はこのやり方になると思います。

メモリ上にファイルを作ってデータをコピーしてから使います。

```dart
// 作成済みデータの配置場所
var path = "assets/パス名/ファイル名";

// データファイルの読み込み
ByteData data = await rootBundle.load(path);
Uint8List bytes =
    data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);

// InMemoryFileSystem上にファイルを作成して書き込む
var fileSystem = InMemoryFileSystem();
var file = fileSystem
    .xOpen(Sqlite3Filename(fileSystem.xFullPathName(path)),  // とりあえず同じpathで
        SqlFlag.SQLITE_OPEN_READWRITE | SqlFlag.SQLITE_OPEN_CREATE)
    .file;
file.xTruncate(0);
file.xWrite(bytes, 0);
file.xClose();

// sqlite3.wasmを読み込む
final sqlite3 = await WasmSqlite3.loadFromUrl(Uri.parse('sqlite3.wasm'));

// ファイル置き場を決める(メモリ)
sqlite3.registerVirtualFileSystem(fileSystem, makeDefault: true);

// ファイル名を決めてopen(InMemoryFileSystem上に作成したファイル)
return sqlite3.open(path);
```

**※実際に使うときは例外処理等を忘れないように**

# クエリの発行

プレースホルダには？を使います。

文字列操作、ダメ、絶対。

### 戻り値が不要なクエリ

execute関数を使います。

```dart
db.execute("SQLを記述", ["置換する値"]);

// prepare版
var prepared = db.prepare("SQLを記述");
prepared.execute(["置換する値"]);
```

### 結果が必要なクエリ

どんなSQLだろうとselect関数を使います。

```dart
var result = db.select("SQLを記述", ["置換する値"]);
result.forEach(print);

// prepare版
var prepared = db.prepare("SQLを記述");
var result = prepared.select(["置換する値"]);
```

結果はResultSet型で、forやforEachでアクセスできます。

# もう少し突っ込んで使ってみる

### 複数のDBをまとめる

データの管理の都合上sqliteのファイルを複数に分けたくなる場合があります。

同じFileSystem上に作成したファイルならattachで開けます。

```dart
// DBのロード
Future<void> loadDB(
     CommonDatabase db,
     InMemoryFileSystem fileSystem,
     String dbName,
     String path
   ) async {

  // assetからデータの読み込み
  ByteData data = await rootBundle.load(path);
  Uint8List bytes =
      data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);

  // SQLite3のInMemoryFileSystemに書き込む
  var file = fileSystem
      .xOpen(Sqlite3Filename(fileSystem.xFullPathName(path)),
          SqlFlag.SQLITE_OPEN_READWRITE | SqlFlag.SQLITE_OPEN_CREATE)
      .file;
  file.xTruncate(0);
  file.xWrite(bytes, 0);
  file.xClose();

  // Attach Open
  db.execute("""attach database "$path" as $dbName""");
}
```
アクセス時に`table名`の前に`db名`を付けて`db名.table名`のようにアクセスします。

### テーブル作成用SQLを拾う

sqlite_masterにスキーマが格納されています。

IndexedDB上にDBを作るときとか、外部でテーブルを作っておけて便利です。

```dart
var schemas =
  memoryDB.select("SELECT * FROM user.sqlite_master WHERE type='table'");
for (var schema in schemas) {
  localStorageDB.execute((schema["sql"] as String)
      .replaceFirst("CREATE TABLE", "CREATE TABLE IF NOT EXISTS"));
}
```