Skip to content

Instantly share code, notes, and snippets.

@t3rmian
Last active May 31, 2020 15:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save t3rmian/8ffe844882d4c009abd730cb98f75dac to your computer and use it in GitHub Desktop.
Save t3rmian/8ffe844882d4c009abd730cb98f75dac to your computer and use it in GitHub Desktop.
Android Room - Backup to a secondary database on a destructive migration
import android.app.Application
import androidx.room.Room
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@Module(
subcomponents = [
ActivityComponent::class,
FragmentComponent::class
]
)
class AppModule(private val app: Application) {
private val migrationDatabase: Lazy<MigrationDatabase> = lazy {
Room.databaseBuilder(app.applicationContext, MigrationDatabase::class.java, "migration.db")
.fallbackToDestructiveMigration()
.build()
}
@Provides
@AppScope
fun provideApp(): Application = app
@Provides
@Singleton
fun provideDatabase(productApi: ProductApi): ProductDatabase {
return Room
.databaseBuilder(app.applicationContext, ProductDatabase::class.java, "prod.db")
.fallbackToDestructiveMigration()
.openHelperFactory(
BackupOpenHelperFactory(
FrameworkSQLiteOpenHelperFactory(),
listOf(ProductMigrationBackup(migrationDatabase))
)
)
.build()
}
}
import androidx.sqlite.db.SupportSQLiteDatabase
interface Backup {
fun backup(database: SupportSQLiteDatabase)
}
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteOpenHelper
import timber.log.Timber
class BackupCallback(
private val delegate: SupportSQLiteOpenHelper.Callback,
private val backups: List<Backup>
) : SupportSQLiteOpenHelper.Callback(delegate.version) {
override fun onDowngrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
backups.forEach { it.backup(db) }
delegate.onDowngrade(db, oldVersion, newVersion)
}
override fun onCreate(db: SupportSQLiteDatabase) {
delegate.onCreate(db)
}
override fun onOpen(db: SupportSQLiteDatabase) {
delegate.onOpen(db)
}
override fun onConfigure(db: SupportSQLiteDatabase) {
delegate.onConfigure(db)
}
override fun onCorruption(db: SupportSQLiteDatabase) {
try {
backups.forEach { it.backup(db) }
} catch (e: Exception) {
Timber.e(e, "Could not backup the corrupted database")
}
delegate.onCorruption(db)
}
override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
backups.forEach { it.backup(db) }
delegate.onUpgrade(db, oldVersion, newVersion)
}
}
class BackupOpenHelperFactory(
private val delegate: SupportSQLiteOpenHelper.Factory,
private val backups: List<Backup>
) :
SupportSQLiteOpenHelper.Factory {
override fun create(configuration: SupportSQLiteOpenHelper.Configuration): SupportSQLiteOpenHelper {
val decoratedConfiguration =
SupportSQLiteOpenHelper.Configuration.builder(configuration.context)
.name(configuration.name)
.callback(BackupCallback(configuration.callback, backups))
.build()
return delegate.create(decoratedConfiguration)
}
}
import androidx.room.TypeConverter
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
object MapConverter {
private val GSON = Gson()
@TypeConverter
@JvmStatic
fun stringToMap(value: String): Map<String, String> {
return GSON.fromJson(value, object : TypeToken<Map<String, String>>() {}.type)
}
@TypeConverter
@JvmStatic
fun mapToString(value: Map<String, String>): String {
return GSON.toJson(value)
}
}
import androidx.room.*
@Dao
interface MigrationDao {
@Query("SELECT * FROM migration_data")
fun findAll(): List<MigrationData>
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(data: MigrationData)
@Delete
fun delete(data: MigrationData)
}
import androidx.room.*
@Entity(tableName = "migration_data")
data class MigrationData(
@ColumnInfo(name = "data") val data: Map<String, String>,
@PrimaryKey @ColumnInfo(name = "id") var id: Int = data.hashCode()
)
import androidx.room.*
@TypeConverters(value = [MapConverter::class])
@Database(entities = [MigrationData::class], version = 2)
abstract class MigrationDatabase : RoomDatabase() {
abstract fun migrationDao(): MigrationDao
}
import androidx.room.*
import java.util.*
@Entity(
tableName = "products", indices = [Index(
value = ["name", "tag"],
unique = true
)]
)
data class Product(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") var id: Int = 0,
@ColumnInfo(name = "code") val code: String,
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "creation_date") val creationDate: Date,
@ColumnInfo(name = "tag") val tag: String? = null,
@ColumnInfo(name = "image") val image: ByteArray? = null
)
import androidx.room.*
@Dao
interface ProductDao {
@Query("SELECT * FROM products")
fun findAll(): List<Product>
@Insert
fun insert(product: Product)
@Delete
fun delete(user: Product)
}
import androidx.room.*
@TypeConverters(value = [MapConverter::class])
@Database(entities = [Product::class], version = 25)
abstract class ProductDatabase : RoomDatabase() {
abstract fun productDao(): ProductDao
}
import androidx.sqlite.db.SupportSQLiteDatabase
import dev.termian.nutrinote.data.db.MigrationDatabase
import dev.termian.nutrinote.data.dto.MigrationData
class ProductMigrationBackup(private val migrationDatabase: Lazy<MigrationDatabase>) : Backup {
override fun backup(database: SupportSQLiteDatabase) {
val migrationDao = migrationDatabase.value.migrationDao()
database.query("SELECT code FROM products").use { cursor ->
while (cursor.moveToNext()) {
migrationDao.insert(
MigrationData(
data = mapOf(
"table_name" to "products",
"code" to cursor.getString(0)
)
)
)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment