コンセプトは f(RDB)=> Firebase
モチベーション
Firebase Realtime Databaseを使う上でこのような問題点がある
- あえて非正規化した場所が、バグによって正しく更新されず不整合になるリスク
- 正確なアクセス権限設定が極めて難しい
- 枯れたRDBほどマイグレーションツールが揃っていないためAlter Table的な操作をしたくない
そこで、Realtime Databaseの前にRDBを置き、RDBからReactiveにRealtime Databaseを更新すれば解決できると考えた。
実装
MySQLから更新イベントを受信するためにはmysqlbinlogというコマンドを使う。
これは本来全然別のことに使うためのものだが、せっかくなので使う。
RDBの全データを引数にRealtime Database上の全データを作る純粋関数を書く。
mysqlbinlogが標準出力に何かを書いたら、それに反応してfirebaseを更新する。
サンプル
void main() async {
var command = "mysqlbinlog --read-from-remote-server --short-form --host=localhost --user=your_name --password=*** --stop-never mysql-bin-changelog.000001";
var process = await Process.start(command , []);
process.stdout.listen(onStdout);
}
class UserEntity {
int userID;
String name;
}
class MessageEntity {
int messageID;
DateTime time;
String text;
int userID;
}
void onStdout(List<int> data) async {
print("-" * 10);
var conn = await MySqlConnection.connect();
var result = await conn.executeStreamed("select * from users");
List<UserEntity> users = [];
await for (var row in result) {
var user = new UserEntity()
..userID = row[0]
..name = row[1];
users.add(user);
}
var result2 = await conn.executeStreamed("select * from messages");
List<MessageEntity> messages = [];
await for (var row in result2) {
var message = new MessageEntity()
..messageID = row[0]
..time = row[1] as DateTime
..text = new Utf8Decoder().convert((row[2] as Blob).toBytes())
..userID = row[3];
messages.add(message);
}
messages.sort((m1, m2) => m1.time.compareTo(m2.time));
var fbMessages = messages.map((m) => <String, dynamic>{
"time": m.time.toLocal().toIso8601String(),
"text": m.text,
"userName": users.firstWhere((u) => u.userID == m.userID).name,
}).toList();
await fbClient.put("/messages",fbMessages);
}
今後の課題
今はパフォーマンスが悪すぎるので改善したい。
- バイナリログをきちんと読み取って、毎回全データを落としてくるのではなく、関係のある行のみを落としてくるようにしたい
- Realtime Databaseに全データを書き込んでいるので、今回の出力と前回の出力を比較して差分更新できるようにしたい
- 今はRealtime Databaseに書き込む全てのデータを毎回作っているので、関数を小分けにして、無関係な部分は実行しないようにしたい
また、クライアント側からのアクションをMySQLに書き込むところも上手く抽象化したい