由于我不能成功地预制migration,我决定继续使用新的数据库.我在当前版本的应用程序中有一个名为Cars.db的数据库,在新版本中,应用程序将创建一个名为MyCars.db的新数据库,并将Cars.db中的数据插入到新的数据库中.

以下是一种解决方法,我是如何做到的:

class MainActivity : ComponentActivity() {

    private lateinit var data: Data

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        data = ViewModelProvider(this)[Data::class.java]

        setContent {

            .....

            AppTheme {

                .....

            }
           
            LaunchedEffect(key1 = Unit) {
                data.onMigrate() //-> Start migration
            }
        }
    }
}

我的AndroidViewModel个班级:

class Data(private val application: Application) : AndroidViewModel(application) {

    ....

    fun onMigrate() {
        try {
            val path = application.getDatabasePath("Cars.db") // will this work for all os versions >= 26 sdk?
            val database = SQLiteDatabase.openDatabase(path.path, null, SQLiteDatabase.OPEN_READONLY)
            val cursor = database.query("Cars", null, null, null, null, null, null)
            val carsList = ArrayList<Car>(cursor.count)
            while (cursor.moveToNext()) {
                val timestamp = try {
                    cursor.onStringColumn("Modified").toLong()
                } catch (_: Exception) {
                    currentTime
                }
                val color = cursor.getString(cursor.getColumnIndexOrThrow("Color")) ?: null
                carsList.add(
                    Car(
                        color = color,
                        timestamp = timestamp,
                        model = cursor.onStringColumn("Model"),
                        manufacture = cursor.onStringColumn("Manufacture"),
                        labels = cursor.onStringColumn("Tags").stringToList() // stringToList() is a custom extension of String.
                    )
                )
            }
            if (carsList.isNotEmpty()) {
                onScope {
                    carsRepo.insert(carsList)
                    delay(20) // -> Wait 20 ms
                    application.deleteDatabase("Cars.db") // delete the "Cars.db"
                }
            }

            cursor.close()

        } catch (_: Exception) {
        }
    }

    ....
}

然而,我不确定是否能获得旧数据库的路径.既然有许多不同制造商的不同设备,这val path = application.getDatabasePath("Cars.db")能在每个Android操作系统版本&gt;=sdk 26上运行以获取旧数据库的路径吗?

另一个问题是,无论official方式如何,这种迁移是否足够好?

推荐答案

这个Val Path=Application.getDatabasePath("Cars.db")可以在所有Android操作系统版本上运行吗&>=SDK 26

API%1中已存在getDatabasePath.

它应该可以在任何设备上运行,请注意,它是绝对路径.然而,尽管可能性不大,但在权限允许的情况下,App可以在其他路径上打开数据库.不过,Room将使用默认路径.

另一个问题是,无论官方方式如何,这种移民是否足够好?

从理论上讲,从另一个数据库提取数据,然后将该数据加载到Room数据库的核心原则是可行的.BUT (note questions posed are intended to be rhetorical)

似乎没有使用MyCars.db(加载从Cars.db提取的数据).

如果两个数据库中的任何一个都不存在(假设您添加代码以使用MyCars.db),需要考虑哪些因素?

每当应用程序运行时,如何停止运行该应用程序?它应该在应用程序运行的时候运行吗?

对于文件室迁移,只有在满足特定条件的情况下才运行迁移,即更改方案和更改数据库版本.测试这些条件的工作被内置到Room中.

  • 模式的散列是在编译项目时确定的,该散列存储在Room的表Room_master_table中.打开数据库时,会将编译后的散列与存储在数据库中的散列进行比较,如果更改,则会进行migrations.
  • 以类似的方式编译版本号,并将其存储在数据库中(存储在文件的头中,文件的前@Database个字节,USER_VERSION存储在60字节的偏移量处,长度为4字节)
    • 版本更改不会触发迁移,但如果需要迁移,则该值将是迁移的起始版本.迁移到的版本,即指定为@Database批注的Version参数的值.

由于我无法成功执行迁移,

迁移是可能的,之前提供的答案是基础.

在某些假设下(DATE_CREATED存储为yyyy-mm-dd格式,该标记存储为CSV),则已成功使用以下迁移(请注意嵌入的注释):-

    val Migration1To2 = object: Migration(1,2) {
        override fun migrate(db: SupportSQLiteDatabase) {
            db.execSQL("ALTER TABLE Cars RENAME TO Cars_original")
            db.execSQL(Car.tableCreateSQL) /* CREATE new Cars Table */

            /* ORIGINAL SCHEMA */
            /*
                    Room schema (as from java(generated)):-
                    CREATE TABLE IF NOT EXISTS `Cars` (
                        `Id` INTEGER PRIMARY KEY AUTOINCREMENT,
                        `Manufacture` TEXT,
                        `Model` TEXT,
                        `Date_created` TEXT,
                        `Date_modified` TEXT,
                        `Color` TEXT,
                        `Tags` TEXT
                    )
                 */

            /* NEW SCHEMA
                    Room schema (as from java(generated)):-
                    CREATE TABLE IF NOT EXISTS `Cars` (
                        `Id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
                        `Manufacture` TEXT NOT NULL,
                        `Model` TEXT NOT NULL,
                        `Labels` TEXT NOT NULL,
                        `Color` TEXT,
                        `Timestamp` INTEGER NOT NULL,
                        `Type` TEXT NOT NULL,
                        `Folder` TEXT NOT NULL
                        )
             */
            val conversion =
                "INSERT INTO Cars " +
                        /* The columns to be inserted (also defines the order of the values when inserting)
                            Note how labels column (formerly tags) has been left till last
                         */
                        "(id,manufacture,model,color,timestamp,type,folder,labels) " +
                        "SELECT " +
                        "id " /* UNCHANGED so exact value from original can be used */ +
                        ",coalesce(manufacture,'unknown manufacturer') " /* manufacturer was nullable so if null then change to unknown manufacturer */ +
                        ",coalesce(model,'unknown model') " /* NOT NULL */ +
                        ",color " /* unchanged */ +
                        /* handle Timestamp */
                        ",coalesce(" +
                                "strftime('%s',date_created)" /* assumes yyyy-mm-dd */ +
                                ",strftime('%s','2100-12-31 23:59:59')" +
                                ",0" /* if still null then 0 (should not get here )*/ +
                                ")" +
                        ",'${Type.Sedan}'" /* as no equivalent use default */ +
                        ",'${Folder.Used}'" /* as no equivalent use default */ +
                        ",replace(tags,',','${Converters.SEPARATOR}')" + /* replace commas with more solid separator (see Type Converter)*/
                " FROM Cars_original" +
                ";"
            db.execSQL(conversion)
            /* optional but suggested omitted so both new and original are available*/
            //db.execSQL("DROP TABLE IF EXISTS Cars_original")
        }
    }

已使用下列各项的地方:

enum class Type(val typename: String) {
    Sedan("Sedan"),Hatchback("Hatchback"),SUV("Suv")
}
enum class Folder(val status: Int) {
    Used(1), Unused(2)
}

class Converters {
    companion object {
        const val SEPARATOR = "~~"
    }
    @TypeConverter
    fun fromListStringToString(from: List<String>): String {
        val rv = StringBuilder()
        var i = 0
        for (s in from) {
            rv.append(s)
            if (i++ < from.size) rv.append(SEPARATOR)
        }
        return rv.toString()
    }

    @TypeConverter
    fun toListStringFromString(from: String): List<String> {
        val rv = ArrayList<String>()
        for(s in from.split(SEPARATOR)) {
            rv.add(s)
        }
        return rv.toList()
    }
}

@Entity(tableName = "Cars")
data class Car(
    @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "Id") var id: Long = 0,
    @ColumnInfo(name = "Manufacture") var manufacture: String = "",
    @ColumnInfo(name = "Model") var model: String = "",
    @ColumnInfo(name = "Labels") var labels: List<String> = listOf(),
    @ColumnInfo(name = "Color") var color: String? = null,
    @ColumnInfo(name = "Timestamp") var timestamp: Long = 0L,
    @ColumnInfo(name = "Type") var type: Type = Type.Sedan,
    @ColumnInfo(name = "Folder") var folder: Folder = Folder.Used

)  {
     companion object {
         val tableCreateSQL = "CREATE TABLE IF NOT EXISTS `Cars` (" +
                 "`Id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " +
                 "`Manufacture` TEXT NOT NULL, " +
                 "`Model` TEXT NOT NULL, " +
                 "`Labels` TEXT NOT NULL, " +
                 "`Color` TEXT, " +
                 "`Timestamp` INTEGER NOT NULL, " +
                 "`Type` TEXT NOT NULL, " +
                 "`Folder` TEXT NOT NULL" +
                 ")"
     }
}

SO

  1. 将原来的Cars表重命名为Cars_original
  2. 新的EXPECTED CARS表是在成功编译后使用从生成的Java复制的CREATE语句创建的.
    1. 在回答有关如上实现的类型转换器的问题时(从标记到标签的转换非常简单,只需将,替换为100)
  3. 数据通过INSERT SELECT插入到新的CARS表中,也就是从查询插入到表中.
  • 再次注意 comments

由于Cars.Origal被保留(通常会在成功迁移后被删除(例如,FULLY TESTED)),则可以显示比较:

Original Cars table:-

enter image description here

New Cars table after the migration:-

enter image description here

  • 曾经是CSV的Tags现在是相同的值,但使用了不同的分隔符,并且位于Labels列中.SQLite replace函数用于转换存储的字符串.

  • Timestamp是Unix时间戳(数字),它是从Date_Created列转换而来的,假定日期的格式为yyyy-mm-dd hh:mm:ss,使用SQLite的日期时间函数(Strftime).

  • 类型和文件夹,因为a)它们具有NOT NULL约束,以及b)因为原始文件中没有等价的列,所以已经按照ENUM类将它们设置为缺省值.

    • 如果列具有NOT NULL约束,则必须提供一个值.

Kotlin相关问答推荐

在Kotlin 有更好的结合方式?

如何在 Spring Boot 3 中为内部类提供运行时提示

返回 kotlin 中的标签和 lambda 表达式

如何使用成员引用在 Kotlin 中创建属性的分层路径

如何在 kotlin 中使用带有泛型的密封类

正则表达式 FindAll 不打印结果 Kotlin

Android数据绑定在自定义视图中注入ViewModel

如何将超过 2 个 api 调用的结果与 Coroutines Flow 结合起来?

下拉通知面板时是否可以暂停Android中的任何视频(媒体播放器)应用程序?

模拟异常 - 没有找到答案

IntentService (kotlin) 的默认构造函数

Kotlin 中的数据类

Kotlin - mutableMapOf() 会保留我输入的顺序

Android Studio 4.0.0 Java 8 库在 D8 和 R8 构建错误中脱糖

如何在 Kotlin 中传递有界通配符类型参数?

Spring Boot:更改属性占位符符号

TornadoFX 中设置 PrimaryStage 或 Scene 属性的方法

我们如何在Java注释声明中引用Kotlin常量?

可以在函数参数中使用解构吗?

在 Kotlin 中编写一个等于 Int.MIN_VALUE 的十六进制整数文字