SQLDelight の データベースバージョン

「分かれば簡単だけど、分かるまで難しい」

そんなこと多いですよね。

なにを悩んでいたのか。というやつ。

SQLDelight は、1.0 となり、今現在、ドキュメントやリファレンスが少なくてはまります。

おおまかに「しくみ」を捉えてからやってみること大事です。

 

1.テーブル作成

テーブルを作成したい場合。SQLで、


CREATE TABLE player (
  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
  number INTEGER NOT NULL,
  name TEXT NOT NULL,
  time TEXT DEFAULT (strftime('%s', 'now')),
  UNIQUE (number, name)
);

のようなものを書きますよね。

これは、Player.sq というファイルに書いて、所定の位置に置きます。

この位置はデフォルトでは、パッケージ名 com.example.testdelight の場合、


/app/src/main/sqldelight/com/example/testdelight/Player.sq

となります。

 

2.クエリー作成

プログラムコード上で利用したい「メソッド名」と、それに対するSQLを箇条書きにします。


selectAll:
SELECT *
FROM player;

insert:
INSERT INTO player(number, name)
VALUES (?, ?);

changes:
SELECT changes();

count:
SELECT COUNT(id)
FROM player;

これも、前述の Player.sq ファイルに追記します。

これで、テーブル周りの設定は終わりです。

 

3.スキーマのバージョン

ここが少し分かりづらかったのですが、

「201901281」を新バージョンにしたい場合、

「1を引いたもの」をファイル名として、

「201901280.sqm」

として置きます。

今回は、テーブル定義の変更はないので、中身なしの空ファイルです。

少し不思議な感じがしますが、書き出してみると分かってきます。

なお、このファイルを設定しなければ、適用されるバージョンは「1」となります。

 

4.ビルドして書き出す

ここでビルドすると、以下のようなファイルが書き出されます。

それぞれ以下のコードとなっています。

これらを使って、コードを書いていきます。

 

まとめ

既存の .db ファイルに対して、バージョン更新を行いたい場合のキモとなるのは、書き出される Database ファイル。


object Schema : SqlDriver.Schema {

     override val version: Int
         get() = 201901281 // 201801280 + 1

     // ...

     override fun migrate(
         driver: SqlDriver,
         oldVersion: Int,
         newVersion: Int
     ) {
         if (oldVersion <= 201901280 && newVersion > 201901280) { // same .sqm file name

           // from 201801280.sqm contents
           // ...

         }
     }
 }

.sqm のファイル名の数字が、

「新バージョンの数字」
「適用される既存.db のバージョンの数字」

を決める。

SQLDelight 1.0 使い方 #1
SQLDelight 1.0 使い方 #2
SQLiteのユーザバージョンを利用する - Basic
Pragma statements supported by SQLite




「Android Debug Database」で SQLite の確認・操作する方法

Android内に、

ただ、これだけのテーブルを作って、


CREATE TABLE player (
  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
  number INTEGER NOT NULL,
  name TEXT NOT NULL,
  UNIQUE (number, name)
);

これだけのデータを入れて、


[
  { "number": 1, "name": "柴田勲" },
  { "number": 2, "name": "土井正三" },
  { "number": 3, "name": "張本勲" },
  { "number": 4, "name": "王貞治" },
  { "number": 5, "name": "シピン" },
  { "number": 6, "name": "高田繁" },
  { "number": 7, "name": "河埜和正" },
  { "number": 8, "name": "福嶋智春" },
  { "number": 9, "name": "新浦壽夫" }
]

で、実際にきちんと格納されているかどうか。

みんなはどうやって見てます?

 

コード上でログで吐く

selectAll() など書いてログに出力する。

うん、見づらい。

Flip Tables で List を見やすくログに表示する

 

コマンドラインとかSQLiteBrowser

.dbファイルをPC上にコピーして、それをそれらで開く。


$ sqlite3 test.db 
SQLite version 3.24.0 2018-06-04 14:10:15
Enter ".help" for usage hints.
sqlite> select * from player;
id          number      name
----------  ----------  ----------
1           1           柴田勲
2           2           土井正三
3           3           張本勲
4           4           王貞治
5           5           シピン
6           6           高田繁
7           7           河埜和正
8           8           福嶋智春
9           9           新浦壽夫

はい、いちいちPC上にコピーするのが面倒。

 

IDEのDatabaseビューア

dbファイルパーミッションの厳格化でIDEから直接見ることはできない。

ので、いちいち、PC上にコピー。

毎回、ビューアの設定やパスの指定や同期の確認。

はい、操作多すぎ。

 

Facebook製 Stetho

Stetho - A debug bridge for Android applications

デバッグコードを多少埋め込む。


dependencies {
  compile 'com.facebook.stetho:stetho:1.5.0'
}


public class MyApplication extends Application {
  public void onCreate() {
    super.onCreate();
    Stetho.initializeWithDefaults(this);
  }
}

埋め込んだら、Chromeで開く。

chrome://inspect/#devices

なんで、ID列が2列あるんだよっ!?

リードオンリー。

 

Android Debug Database

amitshekhariitbhu/Android-Debug-Database: A library for debugging android databases and shared preferences - Make Debugging Great Again

一行のみ。


debugImplementation 'com.amitshekhar.android:debug-db:1.0.4'

ログに出力されるURLをクリック。

ブラウザに表示される。

クエリー実行や行ごとの編集もブラウザから可能。

これにしよう。

「Make Debugging Great Again」ですね!

しかし、Room で見れない。
「room」 - Issues · amitshekhariitbhu/Android-Debug-Database

 

まとめ

どうにも、「File Explorer」を使ってのファイルコピー操作がだるい。

確認のたびに、接続や同期、ファイル上書き確認が必要。

しかも、コピーするファイルって、

data/data/your-application-package/databases 以下


your-database-name
your-database-name-shm
your-database-name-wal

の3つが必要なんじゃね?

しらんけど。

SQLiteのジャーナルモード - なべ’s blog


手動で行う AndroidX へのマイグレーション

The Reality of Migrating to AndroidX

Android Studio でやるなら

[Refectering] > [Migrate To AndroidX]

ということですが。

Googleから公開されてるようです、マッピングCSVファイル。

androidx-class-mapping.csv - developer.android.com

これを使って、Danさんのスクリプトで置換すると。

Simple AndroidX Migration Script

あら、build.gradle は?

消してインポート?

「Unresolved reference: R」と出る

Macにgnu-sedをインストールする – ルク – Medium


JetBrains Toolbox で Android Studio の Stable/Beta/Canary が同時に管理できる?

これ。

The New Toolbox App 1.13 with Android Studio inside! | JetBrains Blog

Jakeも何か言っています。


JetBrains のIDEのパッケージ群を管理したり起動できるランチャーのようです。

それに、新たに AndroidStudio も使えるようになった。ということでしょうか。

インストールしてみます。



 

Toolbox Extension

そして、それをChrome上のGitHub画面から起動できるChromeエクステンションも登場しています。

JetBrains Toolbox Extension - Chrome ウェブストア

GitHubページに表示されるアイコンをクリックすると、ソースをダウンロードして、ローカルで起動しようとします。

 

Toolbox(本体)

アドビのパッケージ管理のウィジェットにも似た雰囲気です。

OS画面上部の常駐のバー(?)にアイコンが表示されるのでそれから起動します。

ローカル内のプロジェクトの選択してのIDEの起動と、それぞれのIDEパッケージのバージョン管理が可能になります。

アドビのパッケージマネージャ的なやつと思ったらいいでしょう。

Android Studio/Intellij は、複数のバージョンが並行利用できそうです。

無料です。

👉 AndroidStudio 利用する Java (JDK) の選択・設定の方法 hatena-bookmark
👉 Android Studio Chipmunk の起動時がかわいい件 


今どきのアプリに必須な「複数のソースからのデータ取得」の実装

少し古いですが、ふと思い出した記事です。
ネットワークを使ったすべてのアプリに必須な考え方だと思います。

Loading data from multiple sources with RxJava

ネットワーク経由で問い合わせするデータがある場合、必要なときにそのまま取得することができますが、ディスクやメモリにキャッシュするほうが効率的です。

1. たまにネットワーク経由で新鮮なデータを取得する。
2. それ以外は、その結果をキャッシュしてできるだけ早く取得する。

RxJava を使ってこの実装をすると良いです。

 

基本となる流れ

Observable を使って、ネットワーク、ディスク、メモリーのそれぞれから取得します。

シンプルに2つのオペレーター concat() と first() を使います。

concat() は、複数の Observable を並び順に合成して、first() はその並び順から最初のものを実行します。よって、concat().first() とすると、複数のソースから最初のものが取得されます。


// Our sources (left as an exercise for the reader)
Observable<Data> memory = ...;
Observable<Data> disk = ...;
Observable<Data> network = ...;

// Retrieve the first source with data
Observable<Data> source = Observable
  .concat(memory, disk, network)
  .first();

このパターンの鍵となるのは、concat() が 必要なときにだけ、それぞれの子Observableを subscribe することです。

first() はシーケンスを早く止めるので、データがキャッシュされている場合に不要な遅いソースの問い合わせはありません。 言い換えれば、メモリから結果が返された場合、ディスクやネットワークにアクセスする必要はありません。 逆に、メモリとディスクがデータを持っていない場合は、新しくネットワークリクエストを行います。

 

データの保存

当然、次のステップは、ソースが入ってきたときにそれを保存することです。ネットワーク要求の結果をディスクに保存したり、ディスクへのリクエストをメモリにキャッシュしなければ、何の意味もありません。上記のコードは、常にネットワークリスエストを行うだけです。

私の考えた対策は、それぞれのソースがそれを発行するときにデータを保存/キャッシュすることです。


Observable<Data> networkWithSave = network.doOnNext(data -> {
  saveToDisk(data);
  cacheInMemory(data);
});

Observable<Data> diskWithCache = disk.doOnNext(data -> {
  cacheInMemory(data);
});

これで、networkWithSaveとdiskWithCacheを使用した場合、データはロード時に自動的に保存されます。

(この方法のもう1つの利点は、networkWithSave / diskWithCache はどこでも使用できることです。)

 

古いデータ

このままでは、古いままのデータが常に返されます。新鮮なデータの取得のために、たまにはサーバから取得しなければなりません。

解決策は、first() で。これはフィルタリングも実行できます。価値のないデータを除去するように設定します。


Observable<Data> source = Observable
  .concat(memory, diskWithCache, networkWithSave)
  .first(data -> data.isUpToDate());

これで、「最新」と見なされる最初のアイテムのみを発行します。したがって、そのデータソースが古い場合は、新しいデータが見つかるまで次のソースに進みます。

 

first() の代わりに takeFirst()

first() の代わりに takeFirst() を使うこともできます。

2つの違いは、いずれのソースも有効なデータを発行しない場合、first()は NoSuchElementException をスローするのに対し、takeFirst() は例外なしで完了することです。

どちらを使用するかは、データ不足を明示的に処理する必要があるかどうかによって異なります。

dlew/rxjava-multiple-sources-sample: Sample code demonstrating loading multiple data sources via RxJava