diff --git a/app/build.gradle b/app/build.gradle index 0481495..1d13f39 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,6 +13,11 @@ android { versionCode 1 versionName "1.0" testInstrumentationRunner "com.karumi.TestRunner" + javaCompileOptions { + annotationProcessorOptions { + arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] + } + } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -23,6 +28,9 @@ android { minifyEnabled false } } + sourceSets { + androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) + } } configurations { @@ -75,7 +83,7 @@ dependencies { } androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.1.0" - testImplementation "androidx.room:room-testing:2.1.0-alpha04" + androidTestImplementation "androidx.room:room-testing:2.1.0-alpha04" } shot { diff --git a/app/schemas/com.karumi.jetpack.superheroes.common.SuperHeroesDatabase/1.json b/app/schemas/com.karumi.jetpack.superheroes.common.SuperHeroesDatabase/1.json new file mode 100644 index 0000000..8bd46b8 --- /dev/null +++ b/app/schemas/com.karumi.jetpack.superheroes.common.SuperHeroesDatabase/1.json @@ -0,0 +1,58 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "3298ad790ab4e90668532dfc9d342b54", + "entities": [ + { + "tableName": "superheroes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `photo` TEXT, `isAvenger` INTEGER NOT NULL, `description` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "photo", + "columnName": "photo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isAvenger", + "columnName": "isAvenger", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"3298ad790ab4e90668532dfc9d342b54\")" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.karumi.jetpack.superheroes.common.SuperHeroesDatabase/2.json b/app/schemas/com.karumi.jetpack.superheroes.common.SuperHeroesDatabase/2.json new file mode 100644 index 0000000..4f87eff --- /dev/null +++ b/app/schemas/com.karumi.jetpack.superheroes.common.SuperHeroesDatabase/2.json @@ -0,0 +1,58 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "4250c22060f75f31b611cfdc08f9177d", + "entities": [ + { + "tableName": "superheroes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`superhero_id` TEXT NOT NULL, `superhero_name` TEXT NOT NULL, `superhero_photo` TEXT, `superhero_isAvenger` INTEGER NOT NULL, `superhero_description` TEXT NOT NULL, PRIMARY KEY(`superhero_id`))", + "fields": [ + { + "fieldPath": "superHero.id", + "columnName": "superhero_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "superHero.name", + "columnName": "superhero_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "superHero.photo", + "columnName": "superhero_photo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "superHero.isAvenger", + "columnName": "superhero_isAvenger", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "superHero.description", + "columnName": "superhero_description", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "superhero_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"4250c22060f75f31b611cfdc08f9177d\")" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/karumi/jetpack/superheroes/data/repository/room/MigrationTest.kt b/app/src/androidTest/java/com/karumi/jetpack/superheroes/data/repository/room/MigrationTest.kt new file mode 100644 index 0000000..3f9d335 --- /dev/null +++ b/app/src/androidTest/java/com/karumi/jetpack/superheroes/data/repository/room/MigrationTest.kt @@ -0,0 +1,76 @@ +package com.karumi.jetpack.superheroes.data.repository.room + +import android.support.test.InstrumentationRegistry +import androidx.room.testing.MigrationTestHelper +import androidx.sqlite.db.SupportSQLiteDatabase +import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory +import com.karumi.jetpack.superheroes.common.Migrations +import com.karumi.jetpack.superheroes.common.SuperHeroesDatabase +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test + +class MigrationTest { + companion object { + private const val TEST_DB = "migration-test" + private const val SUPER_HERO_ID = "IronMan" + private const val SUPER_HERO_NAME = "Iron Man" + private val SUPER_HERO_URL: String? = null + private const val SUPER_HERO_IS_AVENGER = true + private const val SUPER_HER_DESCRIPTION = "Iron Man is a super hero" + } + + @Rule + @JvmField + val helper: MigrationTestHelper = MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + SuperHeroesDatabase::class.java.canonicalName, + FrameworkSQLiteOpenHelperFactory() + ) + + @Test + fun migrate1To2() = withDb( + fromVersion = 1, + toVersion = 2, + given = { insertSuperHeroInVersion1() }, + then = { assertSuperHeroExistsInVersion2(this) } + ) + + private fun withDb( + fromVersion: Int, + toVersion: Int, + given: SupportSQLiteDatabase.() -> Unit, + then: SupportSQLiteDatabase.() -> Unit + ) { + val db = helper.createDatabase(TEST_DB, fromVersion) + given(db) + helper.runMigrationsAndValidate(TEST_DB, toVersion, true, *Migrations.all) + then(db) + helper.closeWhenFinished(db) + } + + private fun assertSuperHeroExistsInVersion2(db: SupportSQLiteDatabase) { + val cursor = db.query("SELECT * FROM superheroes") + cursor.moveToFirst() + assertEquals(SUPER_HERO_ID, cursor.getString(0)) + assertEquals(SUPER_HERO_NAME, cursor.getString(1)) + assertEquals(SUPER_HERO_URL, cursor.getString(2)) + assertEquals(SUPER_HERO_IS_AVENGER.toInt(), cursor.getInt(3)) + assertEquals(SUPER_HER_DESCRIPTION, cursor.getString(4)) + } + + private fun SupportSQLiteDatabase.insertSuperHeroInVersion1() { + execSQL( + """ + INSERT INTO superheroes VALUES( + "$SUPER_HERO_ID", + "$SUPER_HERO_NAME", + $SUPER_HERO_URL, + ${SUPER_HERO_IS_AVENGER.toInt()}, + "$SUPER_HER_DESCRIPTION" + )""" + ) + } +} + +private fun Boolean.toInt() = if (this) 1 else 0 diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/SuperHeroesApplication.kt b/app/src/main/java/com/karumi/jetpack/superheroes/SuperHeroesApplication.kt index 4bc942d..5af42a7 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/SuperHeroesApplication.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/SuperHeroesApplication.kt @@ -2,7 +2,6 @@ package com.karumi.jetpack.superheroes import android.app.Application import android.content.Context -import androidx.room.Room import com.karumi.jetpack.superheroes.common.SuperHeroesDatabase import com.karumi.jetpack.superheroes.data.repository.LocalSuperHeroDataSource import com.karumi.jetpack.superheroes.data.repository.RemoteSuperHeroDataSource @@ -33,12 +32,7 @@ class SuperHeroesApplication : Application(), KodeinAware { private fun appDependencies(): Kodein.Module { return Kodein.Module("Application dependencies", allowSilentOverride = true) { bind() with singleton { - Room.databaseBuilder( - this@SuperHeroesApplication, - SuperHeroesDatabase::class.java, - "superheroes-db" - ).fallbackToDestructiveMigration() - .build() + SuperHeroesDatabase.build(this@SuperHeroesApplication) } bind() with provider { val database: SuperHeroesDatabase = instance() diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/common/SuperHeroesDatabase.kt b/app/src/main/java/com/karumi/jetpack/superheroes/common/SuperHeroesDatabase.kt index bb596dc..cc5fa74 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/common/SuperHeroesDatabase.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/common/SuperHeroesDatabase.kt @@ -1,11 +1,62 @@ package com.karumi.jetpack.superheroes.common +import android.content.Context import androidx.room.Database +import androidx.room.Room import androidx.room.RoomDatabase +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase import com.karumi.jetpack.superheroes.data.repository.room.SuperHeroDao import com.karumi.jetpack.superheroes.data.repository.room.SuperHeroEntity -@Database(entities = [SuperHeroEntity::class], version = 1) +@Database(entities = [SuperHeroEntity::class], version = SuperHeroesDatabase.version) abstract class SuperHeroesDatabase : RoomDatabase() { abstract fun superHeroesDao(): SuperHeroDao + + companion object { + const val version = 2 + fun build(context: Context): SuperHeroesDatabase = + Room.databaseBuilder(context, SuperHeroesDatabase::class.java, "superheroes-db") + .addMigrations(*Migrations.all) + .build() + } +} + +object Migrations { + val from1To2 = object : Migration(1, 2) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + """ + CREATE TABLE `superheroes_temp` ( + `superhero_id` TEXT NOT NULL, + `superhero_name` TEXT NOT NULL, + `superhero_photo` TEXT, + `superhero_isAvenger` INTEGER NOT NULL DEFAULT 0, + `superhero_description` TEXT NOT NULL, + PRIMARY KEY(`superhero_id`) + ) + """ + ) + database.execSQL( + """ + INSERT INTO `superheroes_temp`( + `superhero_id`, + `superhero_name`, + `superhero_photo`, + `superhero_isAvenger`, + `superhero_description` + ) SELECT + `id`, + `name`, + `photo`, + `isAvenger`, + `description` + FROM `superheroes`""" + ) + database.execSQL("DROP TABLE superheroes") + database.execSQL("ALTER TABLE `superheroes_temp` RENAME TO `superheroes`") + } + } + + val all = arrayOf(from1To2) } \ No newline at end of file diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/LocalSuperHeroDataSource.kt b/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/LocalSuperHeroDataSource.kt index a52bc8c..59eabaf 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/LocalSuperHeroDataSource.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/LocalSuperHeroDataSource.kt @@ -20,13 +20,11 @@ class LocalSuperHeroDataSource( } suspend fun save(superHero: SuperHero): SuperHero { - dao.insertAll(listOf(superHero.toEntity())) + dao.update(superHero.toEntity()) return superHero } - private fun SuperHeroEntity.toSuperHero(): SuperHero = - SuperHero(id, name, photo, isAvenger, description) + private fun SuperHeroEntity.toSuperHero(): SuperHero = superHero - private fun SuperHero.toEntity(): SuperHeroEntity = - SuperHeroEntity(id, name, photo, isAvenger, description) + private fun SuperHero.toEntity(): SuperHeroEntity = SuperHeroEntity(this) } \ No newline at end of file diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/room/SuperHeroDao.kt b/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/room/SuperHeroDao.kt index b1b3f35..f5867ee 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/room/SuperHeroDao.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/room/SuperHeroDao.kt @@ -4,18 +4,22 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.Update @Dao interface SuperHeroDao { @Query("SELECT * FROM superheroes") suspend fun getAll(): List - @Query("SELECT * FROM superheroes WHERE id = :id") + @Query("SELECT * FROM superheroes WHERE superhero_id = :id") suspend fun getById(id: String): SuperHeroEntity? @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertAll(superHeroes: List) + @Update + suspend fun update(superHero: SuperHeroEntity) + @Query("DELETE FROM superheroes") suspend fun deleteAll() } \ No newline at end of file diff --git a/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/room/SuperHeroEntity.kt b/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/room/SuperHeroEntity.kt index 2d098e2..854251a 100644 --- a/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/room/SuperHeroEntity.kt +++ b/app/src/main/java/com/karumi/jetpack/superheroes/data/repository/room/SuperHeroEntity.kt @@ -1,13 +1,10 @@ package com.karumi.jetpack.superheroes.data.repository.room +import androidx.room.Embedded import androidx.room.Entity -import androidx.room.PrimaryKey +import com.karumi.jetpack.superheroes.domain.model.SuperHero -@Entity(tableName = "superheroes") +@Entity(tableName = "superheroes", primaryKeys = ["superhero_id"]) data class SuperHeroEntity( - @PrimaryKey val id: String, - val name: String, - val photo: String?, - val isAvenger: Boolean, - val description: String + @Embedded(prefix = "superhero_") val superHero: SuperHero ) \ No newline at end of file