torikatsu.dev

Flutterとかプログラミングとかガジェットとか書きます

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
  • java.time.ZonedDateTime
  • 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のラッパーライブラリです。SQLiteRDBMSなので外部キーとかを設定できます。食品テーブルの外部キーがカテゴリーテーブルのIDになります。以下は二つのテーブルの関係のイメージです。

f:id:torikatsu923:20191114151830p:plain
テーブル関係イメージ

この関係に沿って外部キーを設定していきます。以下は実装例です。 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の使い方についてまとめようと思います。 また何か間違っているところがあればコメントにて指摘していただければ幸いです。

参考文献

Room使ってデータレイヤーを実装してみた

はじめに

こんにちは!とりかつです。
前回に引き続き冷蔵庫管理アプリ「RefMA」 の改修に関する記事です。
今日はRoomを使ったデータレイヤー(緑色の部分)の実装をしていきます。

f:id:torikatsu923:20191113123158p:plain
設計

思ったより記事のボリュームがやばくなりそうなので何回かに分けていこうと思います。
今回紹介する内容は

  • Roomについて
    • Entity(テーブルっぽいもの)の作成
    • Dao(Data Access Object)の作成
    • RoomDatabaseを使って実装
  • Repositoryの実装

です。どれも自分なりに理解したものなので間違ってたらすみませんm( )m
LiveDataの実装はまた次回しようと思いますのでよろしくお願いします。 あと言い忘れてましたがKoitlinで開発していきます。

Roomについて

AndroidDevelopersみると

Room永続化ライブラリは、SQLiteののパワーをフルに活用しながら、より堅牢なデータベース・アクセスを可能にするためのSQLiteの上に抽象化レイヤーを提供します。

ってありました。とりあえず便利だからつかっとけばOKって認識でいいと思います。あとRoomはLiveData使えるらしいので使わない手はないですね!

f:id:torikatsu923:20191113142351p:plain

Roomのアーキテクチャです。RoomDatabaseを介してDaoを取得、DaoからEntityを取得、更新するって感じですね。

Roomの実装は大きく分けて以下の工程に分かれます。

  • build.gradleに依存関係の設定
  • Entity(テーブルっぽいもの)の作成
  • Dao(Data Access Object)の作成
  • RoomDatabaseを使って実装

では早速実装していきましょう!

build.gradleに依存関係の設定

build.gradleに依存関係を設定していきましょう

apply plugin: 'kotlin-kapt'

dependencies{
    def roomVersion = '2.2.1'
    implementation "android.arch.persistence.room:runtime:$roomVersion"
    kapt "android.arch.persistence.room:compiler:$roomVersion"
    androidTestImplementation "android.arch.persistence.room:testing:$roomVersion"
}

Roomの最新バージョンは公式に載ってるので確認しましょう。

Entity(テーブルっぽいもの)の作成

Entityはプロパティしか持たないのでdataクラスで宣言するといいと思います。以下のコードスニペットはEntityの実装例です。

@Entity
data class Hoge (
    @PrimaryKey
    var id: Int,

    var name: String
)

めっちゃシンプルですね。

  • クラスの先頭に@Entityを宣言します
  • 主キーにするものは@PrimaryKeyを変数の前に宣言します。

これだけです。

Dao(Data Access Object)の作成

Daoはinterfaceで宣言します。以下は実装例です。

@Dao
interface HogeDao {
    @Query( "select * from hoges")
    suspend fun getAllHoges() : List<hoge>

    @Query("select * from hoges where id = :id")
    suspend fun getHoge(id: Int) : Hoge

    @Insert
    suspend fun insert(hoge: Hoge)

    @Update
    suspend fun update(hoge: Hoge)

    @Delete
    suspend fun delete(hoge: Hoge)
}

実装の手順は@Daoを先頭に宣言してあとは必要なメソッドを書いてくだけです。メソッドの先頭には以下のアノテーションをつけなければならないです。

  • クエリ:@Query( / SQL/ )
  • 挿入:@Insert
  • 更新:@Update
  • 削除:@Delete

@Queryでメソッドの引数(fun getHoge(id: Int))を使いたい時は、SQL文でid = :idみたいに: を変数の前につけてあげればOKです。
@DeleteではHogeクラスを渡しているだけで特にSQL書いたりしてないですが、どうやらデフォルトで主キーで判別してくれるらしいです。
@Insertとかの設定?でコンフリクトのハンドリングできるらしいですがまたいつかまとめようと思います。

RoomDatabaseを使って実装

いよいよRoom実装も終わりが見えてきました。Databaseの実装は以下のコード通りやれば良さそうです。

@Database(entities = [HogeDao::class], version = 1)
abstract class MyDatabase: RoomDatabase() {

    abstract fun hogeDao(): HogeDao

    companion object{
        @Volatile
        private var instance: MyDatabase? = null
        private const val databaseName = "hoge.db"

        fun getInstance(context: Context): MyDatabase =
            instance ?: synchronized(this) {
                Room.databaseBuilder(context,
                    MyDatabase::class.java, databaseName)
                    .build()
            }
    }
}

Roomでは抽象クラスでデータベースクラスを定義しておけばBuilderが勝手にクラスを作成してくれるらしいです。さすがBuilder...
実装手順としては

  • クラスをabstractで宣言する
  • クラスの先頭に@Database(entities = [さっき作ったDao::class], version = 1)って宣言する(ここで宣言するversionはマイグレーションとかで使うらしいです)
  • クラス内にさっき作ったDaoを戻り値とする抽象メソッドを定義
  • companion objectを宣言してその中でインスタンスを生成する処理を書く

って感じです。どうやらインスタンスの生成はsynchronizedを宣言しておくといいらしいです。

とりあえずこれでRoomの実装は終わりました!

findAll()を呼び出したかったら以下のようにすれば呼び出せます!

MyDatabase.getInstance(context).hogeDao().findAll()

Repositoryの実装

 最後にRepositoryの実装を紹介します。まだ理解が完全ではないので間違ってたらすみません。

そもそもリポジトリってなんだ?

Qiitaの記事によると

Repositoryパターンとは永続化を隠蔽するためのデザインパターンで、DAO(DataAccessObject)パターンに似ていますが、より高い抽象度でエンティティの操作から現実の永続化ストレージを完全に隠蔽します 例えばDBコネクションやストレージのパス等はReposiotoryのインターフェースからは隠蔽され、Repositoryのユーザは永続化ストレージが何であるか(例えばMySQLやRedis等)を意識することなく保存や検索の操作を行うことができるようになります。

引用:やはりお前たちのRepositoryは間違っている

とありました。
 おそらく生のSQL文書いたりデータベースにアクセスするためのインスタンスを取得したり...とかの処理を隠蔽してfindAll()するだけで全件取得できちゃうぜみたいな設計のことだと思います。
 今回の改修では関心の分離が大きなテーマの一つだったので使わない手はないですね!

Repositoryの実装

 データベースアクセスは先に紹介したように

MyDatabase.getInstance(context).hogeDao().findAll()

こうすればいいわけですがこれってなんだか毎回書いてたらデータベースアクセスとビジネスロジックが混在してコードがぐちゃぐちゃになりそうですよね。ってことなのでデータベースアクセスの処理をRepositoryに書いて責務を分離しようと思います。

class HogeRepository(val context: Context) {
    private val hogeDao: HogeDao by lazy { MyDatabase.getInstance(context).hogeDao() }

    fun getAll(): List<Hoge> {
        MyDatabase.getInstance(context).hogeDao().findAll()
   
    ......
    }
}

こんな感じにしてみました!これならデータベースアクセスとビジネスロジックが綺麗に分けられてて良さそうですね!  Daoを取得する際にby lazy { ・・・ }ってしましたが、これは使うときになったら取得する方がなんとなく良さそう()って理由と出来るだけnullableな変数を宣言したくない理由からこうしました。lateinitも使えそうですがこっちの方がコードがスッキリしますよね。

おわりに

 今回はRoomの使い方とRepositoryの実装をざっくりまとめてみました。次回は実際に自分が実装している中で詰まったこととか、これ便利だなって感じたものを紹介したいと思います。

参考文献とか

アプリの改修方針

アプリの改修方針について

はじめに

こんにちは。管理人のとりかつです。
冷蔵庫管理アプリ「RefMA」
↑こいつを改修していくわけですが、アプリの現状と方針をざっくり紹介したいと思います。

現在のアプリの問題点

一応ソースコード をあげておきます。
正直、問題点しか見当たりませんがとりあえず

この辺について改修を進めてきたいと考えています。 いまのアプリは単一モジュールで、MVVMとかの存在は一切なく神Activityのみで構成されてる感じです。神Activityが集まってできているんで神話みたいなアプリですね()
おかげで保守性のかけらもなくバグもたくさんあります。 とりあえずこいつを堅牢なアプリにしていろんな人に使ってもらいたいってのが最終目標です。

設計とかアーキテクチャとか

まずはじめに設計から手をつけてきたいと思います。

f:id:torikatsu923:20191112175310p:plain
設計

まず手をつけるところ

公式にもあったんですけども各レイヤーは下の要素のみに依存するということだったのでなんとなくデータレイヤーから手をつけていきます!
今回はデータバインディング使いたいということなのでLiveDataを扱える(らしい)Roomをデータベースに採用したいと思います。

おわりに

今回はざっくりとした方針についてまとめました。
次回以降ではRoom使ったデータレイヤーの実装をゴリゴリ進めていきたいと思います!

はじめまして!

ブログを始めました!

はじめに

はじめまして!ブログ管理人のとりかつです。
私は大学生でITエンジニアを目指しているのですが改めてアウトプットが大事だと感じたのでブログを書くことにしました。
このブログでは以前私がリリースした冷蔵庫管理アプリ「RefMA」の大幅改修をメインテーマとして書いていくつもりです。
一応JSとかJavaとかKotlinとか触ったことありますがRxJavaとかMVVMとかさっぱりのプログラミング素人です。()
内容はかなり素人向けになるのでもしAndroid開発の勉強始めたよ!って人いたら一緒に勉強していきましょう!

追記

ツイッターやってるんでもしよければぜひ! twitter.com