Roomとかデータレイヤー実装で詰まったこと ・便利なこと
はじめに
こんにちは。とりかつです。
今回も前回に引き続き冷蔵庫管理アプリ「RefMA」 の改修に関する記事です。
今日は前回の記事で予告していた通りRoomとかデータレイヤーの実装で詰まったこととか便利だったことを紹介します。以下は今回の記事のトピックです。
- DB設計の日付問題
- 日付はString型?
- java8で出たLocalDateTimeとかのお話
- Roomは任意の型を使えちゃう??
- Roomの細かい設定について
- 外部キーの設定
- Entity,Daoの使ったほうが良さそうなやつ
- Kotlinコルーチンについて
- コルーチンって何?
- コルーチンの実装方法
素人なりにいろいろ調べてみたのですが間違ってる部分があったら教えていただけると助かります。
DB設計の日付問題
以前このアプリ を開発していた際に日付を扱っていたのですが型の変換とかオーバーフローとかの問題でかなり苦しめられました。なので今回は日付の型定義をしっかり詰めて行こうと思います。 特に知見もないので手探りにはなりますが自分なりに答えを見つけてみました。 以下は最終的に決まった食品のテーブル設計図です。(図の書き方あってるかな)
日付はString型?
データベースで日付って何で扱ったらいいんでしょうか。 なんとなくミリ取得してLong型に落とし込めばいいかなって思ったんですけども
- オーバーフローが怖い
- Long ↔️ 日付の型、パースした文字列の変換が面倒・キャストしまくるのが怖い
って理由からLong型は避ける方向にしました。
とりあえずいろいろな記事みてた結果、日付系のクラスをStringになんとかキャストして扱う方針で落ち着きました。
java8で出たLocalDateTimeとかのお話
日付系のクラスを使うことになったんですけどjavaって日付関係のクラスめっちゃややこしいんですよね。
日付の型はCalendarがあって、書式変更にはSimpleDateFormatつかって、他の型に直すときになぜかDateを経由させないといけなかったり…
はっきりいってめちゃめちゃややこしいです。
滅入りながら調べ続けているとjava8で新しく追加された日付関係のクラスを見つけました!
ちなみに変換には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よりも軽量で、実行途中で処理を中断・再開することができます。
制御できる非同期処理みたいなやつですね。コルーチンって言葉は至る所で聞くので積極的に使って慣れていこうってことで、今回の改修では採用することにしました。
コルーチンの実装方法
実装はめちゃシンプルです。
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を使ったデータレイヤー(緑色の部分)の実装をしていきます。
思ったより記事のボリュームがやばくなりそうなので何回かに分けていこうと思います。
今回紹介する内容は
- Roomについて
- Entity(テーブルっぽいもの)の作成
- Dao(Data Access Object)の作成
- RoomDatabaseを使って実装
- Repositoryの実装
- そもそもリポジトリってなんだ?
- Repositoryの実装
です。どれも自分なりに理解したものなので間違ってたらすみませんm( )m
LiveDataの実装はまた次回しようと思いますのでよろしくお願いします。
あと言い忘れてましたがKoitlinで開発していきます。
Roomについて
Room永続化ライブラリは、SQLiteののパワーをフルに活用しながら、より堅牢なデータベース・アクセスを可能にするためのSQLiteの上に抽象化レイヤーを提供します。
ってありました。とりあえず便利だからつかっとけばOKって認識でいいと思います。あとRoomはLiveData使えるらしいので使わない手はないですね!
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等)を意識することなく保存や検索の操作を行うことができるようになります。
とありました。
おそらく生の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が集まってできているんで神話みたいなアプリですね()
おかげで保守性のかけらもなくバグもたくさんあります。
とりあえずこいつを堅牢なアプリにしていろんな人に使ってもらいたいってのが最終目標です。
設計とかアーキテクチャとか
まずはじめに設計から手をつけてきたいと思います。
- しっかりと関心の分離をしたい
- AACとかデータバインディングとか使ってみたい
- ライフサイクルのハンドリングが楽になる?ViewModel便利そう
って理由からこんな感じにしました。
公式のアーキテクチャガイドも参考にしました。
まず手をつけるところ
公式にもあったんですけども各レイヤーは下の要素のみに依存するということだったのでなんとなくデータレイヤーから手をつけていきます!
今回はデータバインディング使いたいということなのでLiveDataを扱える(らしい)Roomをデータベースに採用したいと思います。
おわりに
今回はざっくりとした方針についてまとめました。
次回以降ではRoom使ったデータレイヤーの実装をゴリゴリ進めていきたいと思います!
はじめまして!
ブログを始めました!
はじめに
はじめまして!ブログ管理人のとりかつです。
私は大学生でITエンジニアを目指しているのですが改めてアウトプットが大事だと感じたのでブログを書くことにしました。
このブログでは以前私がリリースした冷蔵庫管理アプリ「RefMA」の大幅改修をメインテーマとして書いていくつもりです。
一応JSとかJavaとかKotlinとか触ったことありますがRxJavaとかMVVMとかさっぱりのプログラミング素人です。()
内容はかなり素人向けになるのでもしAndroid開発の勉強始めたよ!って人いたら一緒に勉強していきましょう!
追記
ツイッターやってるんでもしよければぜひ! twitter.com