読者の皆様,こんにちは!えんぴつです. 12月15日,アプリ開発団体jackではjackFesという一大イベントが行われました🥳 そんなめでたい場所で,僭越ながら発表させていただいたので,その内容を記事にして皆さんにもお届けします😘
導入
私は現在,Flutterで家計簿アプリを作っています.その過程で,Flutterでデータベースを扱う際のプラクティスについて考える機会があったので,考えたことを共有しようとおもいます. ただ,データベースの扱い方について語る,と言ってもこれがベストプラクティスだとは思っていません.むしろ教えてほしいくらい初心者なので,もしいい方法知ってたら教えてください. さて,Flutterでデータベースといえば - sqflite - shared_preferences あたりが有名でしょう.Flutter データベース で検索したら真っ先にでてくるライブラリだと思います. ちなみに,sqfliteはリレーショナルデータベース,shared_preferencesは非リレーショナルデータベースを提供してくれます. リレーショナルデータベースとは,たくさんの表からなり,SQLと呼ばれる言語で操作するタイプのものです.この記事では表の列の名前をフィールド名といったり,カラム名と言ったりする,くらいの理解をしておいてもらえれば大丈夫です.
逆に,非リレーショナルデータベースとは,文字通りリレーショナルデータベースでは無いタイプのデータベースです.例としては,key-value型のデータベースが代表的です.こちらは(一般的には)リレーショナルデータベースよりも簡単に扱えるという利点があります.
両者とも,一長一短あるので,適したほうを使いましょうという話もあるのですが,この記述の本題ではないのでこれ以上は立ち入らないことにします. さて,この記事での主役はリレーショナルデータベースの方です.どうして取り沙汰されるのかというと,Flutterでリレーショナルデータベースの機能を提供してくれるsqfliteに,ORM(object relational mapper)が用意されていないのがとてもproblematicだからです. ORMはざっくり,データベースから取ってきた生のデータを型にはめる機能を持つものだと思ってください.後で詳しく話しますが,ORMが無いと色々厄介なことがあるんですね.
…と,いう流れで発表を進めようと思ったので,ちょろっとFlutter ORMで検索してみたんですが,当然というべきか,ORMを提供するライブラリ,あるんですね.Driftというライブラリです.だったらそれを使えば万事解決です.つまり,ここから先の話はすべて茶番です. みんなはコーディングを始める前にちゃんと技術選定をしよう!
リテラルは悪
ともあれ,ORMが無いとどんな厄介なことがあるのかという話から初めましょう.ORMがなくてリレーショナルデータベースを扱うのならば,SQLを書く必要がありますね.Dart上では,SQLは文字列として直接記述することになります.つまりリテラルです.ORMがなくてもクエリビルダくらいあるだろというツッコミもあるかとは思いますが,結局テーブル名やフィールド名はリテラルで与えることになります. これはよろしくない.なぜかといえば,リテラルで記述してしまうと,第一に補完が効きません.フィールド名なんていちいち覚えていられないですよね?また,タイポしても実行時までなんのエラーもでてくれないので大変です.フィールド名を変更しようなどと思い立った日にはリテラルを一つ一つ置換してやらなければなりません.このとき置換漏れがあればバグになります.そんなの耐えられない.
解決?
というわけで,フィールド名やテーブル名の記述はできるだけ一箇所に集めたくなります.近くにあれば確認するのも早いし,置換漏れも減りますからね. これを実現するために,たくさんあるテーブルごとに対応するクラスを作ってやって,フィールド名やテーブル名はそのクラスの中でしか使わないことにしましょう.こうすることで非常に見通しが良くなります.
…おや?
おやおやおや…??
コードが爆発してしまいました.なにがいけなかったのでしょうか. まず第一に,フィールド名やテーブル名を要する処理を単一のクラスに詰め込もうとすると,そのクラスが際限なく肥大化してしまいます.一つのクラスにしては責任が重すぎるんですね. さらに悪いのは,データベースを処理するにあたって,単一のテーブルのみを参照したり編集したりする処理ばかりではないということです.考えてみればあたりまえで,リレーショナルデータベースとはそもそも複数の表の間の関係に重きをおいた構造なので,一つのテーブルで閉じるわけがないんですね.だからリレーショナルと言うのです. こうすると,何が起きるのかというと,実現したい処理は単一のものでも,同じ抽象度の複数のクラスに渡って書かれることになります.結果として,その処理に対して責任を追っているクラスが曖昧になるんですね. 責任は重いのに,その責任が曖昧,つまり最悪ということです. というわけで,クラスの責任が肥大化しないようにしましょう.フィールド名やテーブル名を単一のクラスに押し込めるのではなく,変数やオブジェクトとして定義しておく,という方策を取ります.こうして,テーブルクラスの責務は単にフィールド名とフィールドの種類を定義することにとどまりました.
テーブルクラスの責務を小さくしたということは,あぶれたタスクが発生する,ということですが,このあぶれた分はTransactionとして実装することにします. つまり,Transactionが,テーブルクラスが定義したフィールド名などをつかって,SQLを組み立て,データベースとやり取りをするわけです.
ここで,Transactionにおいて,リテラルの使用が避けられていることに注目してください.下線の部分ですね.これなら,補完も効くし,タイポしていたらエディタが教えてくれます. これで万事解決…
とはまぁ,行かなかったんですが. なぜなのか,その理由はTransaction同士でまとまりがなさすぎることにあります.
上は実際のコードからクラス名を引っ張ってきたものですが,その全てが同一の抽象度で存在します.あまりにも多すぎる.流石にファイルで分割はしていますが,それでも分かりづらいことこの上ないです.Transactionから呼ばれるサブモジュールてきなものはSubTransactionとして纏めているのですが,そっちもそっちで同じ抽象度に乱立しています.こうなると,全体像も掴みづらいし,コードの共通化も図りづらいです. 加えて,利点として挙げた補完も中途半端です.なぜかというと,そもそもテーブルを表すクラスの名前がややこしいからです.なんだDtCatLinkerFEって.覚えられるかい.(DisplayTicketCategoryLinkerFieldEnumの略です). 略称がややこしいというのもそうなんですが,そもそもテーブルの数が多いんですね.一つ一つ覚えていられません.補完がよく効くのはDtCatLinkerFEまで打ったあとの話ですから,結局,若干コーディングしづらい状態が続くのです. と,いうわけで,この記事の結論は つらい です.最後まで読んでいただきありがとうございました😘