torikatsu923.dev

サイト内検索

    Roomとかデータレイヤー実装で詰まったこと ・便利なこと

    素人なりにいろいろ調べてみたのですが間違ってる部分があったら教えていただけると助かります。以前このアプリ を開発していた際に日付を扱っていたのですが型の変換とかオーバーフローとかの問題でかなり苦しめられました。

    はじめに

    こんにちは。とりかつです。
    今回も前回に引き続き冷蔵庫管理アプリ「RefMA」 の改修に関する記事です。
    今日は前回の記事で予告していた通りRoomとかデータレイヤーの実装で詰まったこととか便利だったことを紹介します。以下は今回の記事のトピックです。

    • DB設計の日付問題
      • 日付はString型?
      • java8で出たLocalDateTimeとかのお話
      • Roomは任意の型を使えちゃう??
    • Roomの細かい設定について
      • 外部キーの設定
      • Entity,Daoの使ったほうが良さそうなやつ
    • Kotlinコルーチンについて
      • コルーチンって何?
      • コルーチンの実装方法

    素人なりにいろいろ調べてみたのですが間違ってる部分があったら教えていただけると助かります。

    DB設計の日付問題

     以前このアプリ を開発していた際に日付を扱っていたのですが型の変換とかオーバーフローとかの問題でかなり苦しめられました。なので今回は日付の型定義をしっかり詰めて行こうと思います。 特に知見もないので手探りにはなりますが自分なりに答えを見つけてみました。  以下は最終的に決まった食品のテーブル設計図です。(図の書き方あってるかな)

    f:id:torikatsu923:20191113131125p:plain

    日付はString型?

     データベースで日付って何で扱ったらいいんでしょうか。 なんとなくミリ取得してLong型に落とし込めばいいかなって思ったんですけども

    • オーバーフローが怖い
    • Long ↔️ 日付の型、パースした文字列の変換が面倒・キャストしまくるのが怖い

    って理由からLong型は避ける方向にしました。
    とりあえずいろいろな記事みてた結果、日付系のクラスをStringになんとかキャストして扱う方針で落ち着きました。

    java8で出たLocalDateTimeとかのお話

    日付系のクラスを使うことになったんですけどjavaって日付関係のクラスめっちゃややこしいんですよね。
    日付の型はCalendarがあって、書式変更にはSimpleDateFormatつかって、他の型に直すときになぜかDateを経由させないといけなかったり…
    はっきりいってめちゃめちゃややこしいです。
     滅入りながら調べ続けているとjava8で新しく追加された日付関係のクラスを見つけました!

    • java.time.LocalDateTime
      • タイムゾーンのない日時。
      • 内部にLocalDateとLocalTimeのインスタンスを持っている。
      • 例:2015-12-15T23:30:59.999
    • java.time.ZonedDateTime
      • タイムゾーン付きの日時。
      • 例:2015-12-15T23:30:59.999+09:00[Asia/Tokyo]
    • java.time.OffsetDateTime
      • オフセット付きの日時。
      • 例:2015-12-15T23:30:59.999+09:00

    引用: Java8の日時APIはとりあえずこれだけ覚えとけ

    ちなみに変換にはDateTimeFormatterを使っとけ場便利らしいです。
    とにかく便利そうなのでここら辺を使うことにしました。
    今回のアプリでは賞味期限を扱うので年月日だけで良さそうなのでLocalDateTimeが内包しているDateTimeを採用しました。

    Roomは任意の型を使えちゃう??

     RoomのEntity作ってるときに思ったんですけども、Entityの宣言って普通に変数名と型宣言してるだけですよね。ならもしかしたらDateTimeとか任意の型を使えちゃうんじゃないかな?って思ったらそのまさかでした。
     厳密にはStringとかにキャストしているんですけども、そのキャストをそこまで意識しなくても実装できるって点がとても便利だと思います。  実装にはRoomのTypeCoverterってやつを使います。
    実装手順は

    • とりあえずクラスを作成。名前はHogehogeConverterが良さそうです
    • 宣言したクラス内で先頭に@TypeConverterを宣言したメソッドを定義
    • このメソッドは引数と戻り値が1個づつのものでなければダメらしいです

    以下は実装例です。自分はLocalDate↔️Stringで変更するので以下のように実装しました。

     internal class LocalDateConverter {
         /**
          * LocalDate → フォーマット文字列
          * フォーマットはyyyy-MM-dd(デフォルト)
          * */
         @TypeConverter
         fun fromLocalDate(localDate: LocalDate): String {
             return localDate.toString()
         }
    
         /**
          * フォーマット文字列 → LocalDate
          * フォーマットはyyyy-MM-dd(デフォルト)
          * */
         @TypeConverter
         fun toLocalDate(stringDate: String): LocalDate {
             return LocalDate.parse(stringDate)
         }
     }

    まずこのConverterは外部から利用することがないのでinternalで宣言してあります。もしかしたらnullチェックいるかもしれませんが、UseCaseでバリデーションをしっかりするつもりなので、nullが渡ってくるケースが思い浮かばないため一旦保留にしてあります。
     これでConverterの実装は完了です。結構簡単ですね。ちゃんと変換されるか心配だったためテストをしておきました。

    つぎにこのConverterを設定する必要があるので設定をします。 Roomでは実装する際に

    • Entity(テーブル的なやつ)
    • Dao(DataAccessObject)
    • Database

    の3つのクラスを実装する必要がありました。詳しくは前回の記事をみてみてください。

    この3つのうちDatabaseにConverterを設定します。  以下は実装例です。

    @Database(entities = [Food::class, Category::class], version = 1)
    @TypeConverters(LocalDateConverter::class)
    abstract class MyDatabase: RoomDatabase() {
    
        ・・・・・・
    
    }

    2行目に注目してください。2行目で

    @TypeConverters(Converterのクラス名::class)

    でConverterを設定しています。これだけです。 ここでの注意ですがDatabaseに宣言するアノテーションはTypeConverterではなくTypeConvertersです。最後にsがあるかどうかの違いですが、入力補完使ってるとうっかりTypeConverterを宣言してしまいエラーに気づくまで時間取られちゃいます。私はここで1時間ぐらいハマりました笑

     これで実装は完了です。結構簡単で便利そうですよね。

    Roomの細かい設定について

    前回と今回でRoomについてざっと触れてきましたが、紹介してきたもの以外にもいろんな設定とかあります。本項ではその中でもこれ使ったほうがいいんじゃない?みたいなものをざっくり紹介します。

    外部キーの設定

     RoomはSQLiteのラッパーライブラリです。SQLiteはRDBMSなので外部キーとかを設定できます。食品テーブルの外部キーがカテゴリーテーブルのIDになります。以下は二つのテーブルの関係のイメージです。

    テーブル関係イメージ

    この関係に沿って外部キーを設定していきます。以下は実装例です。 CategoryクラスはEntityになります。

    @Entity(tableName = "foods",
            foreignKeys = arrayOf(ForeignKey(
                entity = Category::class,
                parentColumns = arrayOf("id"),
                childColumns = arrayOf("categoryId")
            ))
    )
    data class Food (
        @PrimaryKey
        var id: Int,
    
        var categoryId: Int,
    
        ・・・・・・
    )

    実装手順としては@Entityの引数?内でforeignKeyに

    • Entity = エンティティクラス::class
    • parentColums = arrayOf(親テーブルでの外部キーの変数名)
    • childColumns = arrayOf(子テーブルの外部キーの変数名)

    の3つの要素を定義してあげる感じです。おそらくこれで実装はOKです。

    Entity,Daoの使ったほうが良さそうなやつ

     個人的にこれは使ったほうがいいって思ったものとして Entityでは

    • @Entity(tablename = hoge)
      • テーブル名は自分で決めたほうがどっかで変なバグとかを防げそう
    • @ColumnInfo(name = "任意のカラム名")
      • 変数名がスモールキャメルとかの時にカラム名を指定しておくことで思わぬところでカラム名の違いにハマらずに済みそう

    Daoでは - @Insert(onConfloict = Roomで用意された定数) - コンフリのハンドリングがこれだけで済むから便利そう - 思わぬ挙動を防げそう

    こんなものが挙げられます。公式とか見るともっとあるので必要に応じて積極的に使っていくのがいいかと思います。

    Kotlinコルーチンについて

    コルーチンって何?

    Qiitaの記事によると以下のようにありました。

    Coroutineとは「特定のスレッドに束縛されない、中断可能な計算インスタンス」です。 非同期処理で用いられますが、Threadよりも軽量で、実行途中で処理を中断・再開することができます。

    引用: 【Kotlin】Coroutineを理解する

    制御できる非同期処理みたいなやつですね。コルーチンって言葉は至る所で聞くので積極的に使って慣れていこうってことで、今回の改修では採用することにしました。

    コルーチンの実装方法

    実装はめちゃシンプルです。 Daoのメソッドの先頭にsuspendを宣言するだけです。
    以下は実装例です。

    @Dao
    interface HogeDao {
    
        @Insert
        suspend fun insert(hoge: Hoge)
    
        @Update
        suspend fun update(hoge: Hoge)
    
        @Delete
        suspend fun delete(hoge: Hoge)
    }

    簡単ですね(仕組みは難しそう)後注意ですがsuspendを宣言したメソッドを呼び出す時、そのメソッドもsuspendでなければダメなようです。またコルーチンについて理解が深まったらまとめようと思います。

    おわりに

     今日も結構ボリューミーな内容になってしまいました。次回ではRoomでのLiveDataの使い方についてまとめようと思います。 また何か間違っているところがあればコメントにて指摘していただければ幸いです。

    参考文献

    -––