无法从数据库中的 table 复制数据并将其复制到具有 Android 空间的另一个数据库中的另一个 table

Unable to copy data from a table in a database and copy it to another table in another database with Android room

我有一个用例,我需要将所有行内容从 token table in schema1.db 复制到另一个 table token in schema2.db。我们正在使用 Android 空间来满足我们的数据库要求。

// Migration task for the database schema2.db
 class MyMigration_From_4_To_5(private val context: Context): Migration(4, 5) {
    
     override fun migrate(database: SupportSQLiteDatabase) {

         database.execSQL(
            "ATTACH DATABASE 'db1.db' as 'db1'")
        database.execSQL("INSERT INTO token(userId, deviceToken) SELECT userId, deviceToken FROM db1.token")
        database.execSQL("DETACH DATABASE 'db1'")
        
     }
}

我的测试如下

class MyMigration_From_4_To_5Test {

    @get:Rule
    val helper = MigrationTestHelper(
        InstrumentationRegistry.getInstrumentation(),
        MyDatabase::class.java.canonicalName,
        FrameworkSQLiteOpenHelperFactory()
    )

    @Test
    @Throws(Exception::class)
    fun testMigrationCreatesTable() {
        val migration = MyMigration_From_4_To_5(InstrumentationRegistry.getInstrumentation().context)
        helper.createDatabase(MyDatabase.NAME, migration.startVersion).use {
            MatcherAssert.assertThat(
                it,
                Matchers.whenQueried("PRAGMA table_info(token)", Matchers.rowCount(CoreMatchers.`is`(2)))
            )
        }
        helper.createDatabase(MyDatabase.NAME, migration.endVersion).use {
           MatcherAssert.assertThat(
               it,
               Matchers.whenQueried(
                   "SELECT userId, deviceToken FROM token",
                   Matchers.rowCount(org.hamcrest.Matchers.greaterThan(0)) // Hamcrest type safe matcher which accepts cursor, and verifies the number of rows returned.
               )
            )
        }
    }
}

我在 运行 测试

时遇到以下错误
android.database.sqlite.SQLiteCantOpenDatabaseException: unable to open database: file:/data/user/0/com.myproject.db.test/databases/token (code 14 SQLITE_CANTOPEN) 
    at android.database.sqlite.SQLiteConnection.nativeExecuteForChangedRowCount(Native Method)

android.database.sqlite.SQLiteException: no such table: db1.token (code 1 SQLITE_ERROR[1]): , while compiling: INSERT INTO main.token(userId, deviceToken) SELECT userId, deviceToken FROM db1.token    
    at android.database.sqlite.SQLiteConnection.nativePrepareStatement(Native Method)

com.myproject.MyMigration_From_4_To_5Test > testMigrationCreatesTable[SM-G975U1 - 11] [31mFAILED [0m    
    java.lang.AssertionError:   
    Expected: query 'SELECT userId, deviceToken FROM token' matches: row count a value greater than <0> 

我也尝试了另一种方法

    override fun migrate(database: SupportSQLiteDatabase) {
        val sourceDatabasePath: File = context.getDatabasePath("db1")

            database.execSQL("ATTACH DATABASE '$sourceDatabasePath' AS 'db1'")
            database.execSQL("INSERT INTO main.token(userId, deviceToken) SELECT userId, deviceToken FROM db1.token")
            database.execSQL("DETACH DATABASE 'db1'")
    }

我为此使用了 sqlite's open draft syntax。但是我得到了同样的错误。

我的设备上存在 table db1.token,但我无法将数据从 db1.token 复制到 db2.token。如何将数据从 db1.token 复制到 db2.token

更新#2

我尝试了另一种方法,其中我使用 SQLiteOpenHelper 创建了一个数据库助手并以这种方式查询 table。

/**
 * Helper class to query the existing `token` database
 */
class TokenMigrationHelper(context: Context): SQLiteOpenHelper(context, TOKEN_DB_NAME, null, TOKEN_DB_VERSION) {

    companion object {
        const val TOKEN_DB_NAME = "db1.db" // Existing database name
        const val TOKEN_DB_VERSION = 6  // Token's database version
    }

    override fun onCreate(db: SQLiteDatabase?) {
        // Already created.
    }

    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
        // No need for an upgrade
    }
}

迁移任务更新

class MyMigration_From_4_To_5 @Inject constructor(
    private val context: Context
): Migration(
    4,
    5
) {

    override fun migrate(database: SupportSQLiteDatabase) {
        val tokenMigrationHelper = TokenMigrationHelper(context)
        val tokenDatabase: SQLiteDatabase = tokenMigrationHelper.readableDatabase
        with(tokenDatabase) {
            var cursor: Cursor? = null
            try {
                cursor = rawQuery("SELECT name, value FROM `${tokenMigrationHelper.databaseName}.token`", null)
                if (cursor.moveToFirst()) {
                    do {
                        database.execSQL(
                            "INSERT INTO `token` (hashedUserId, deviceToken) VALUES(?, ?)",
                            arrayOf(cursor.getString(0), cursor.getString(1))
                        )
                    } while (cursor.moveToNext())
                }
            } finally {
                cursor?.close()
            }
        }
    }
}

更新测试

@Test
fun testMigration__DataIsCopied() {
    val migration = MyMigration_From_4_To_5(InstrumentationRegistry.getInstrumentation().context)
    // Verify that the table already exists
    helper.createDatabase(EbayDatabase.NAME, migration.startVersion).use {
        MatcherAssert.assertThat(
            it,
            Matchers.whenQueried("PRAGMA table_info(token)", Matchers.rowCount(CoreMatchers.`is`(2)))
        )
    }
    // Verify that table has not been updated
    helper.createDatabase(EbayDatabase.NAME, migration.endVersion).use {
        MatcherAssert.assertThat(
            it,
            Matchers.whenQueried("PRAGMA table_info(token)", Matchers.rowCount(CoreMatchers.`is`(2)))
        )
    }
    // Verify that data has been inserted by the migration task
    helper.runMigrationsAndValidate(EbayDatabase.NAME, migration.endVersion, true, migration).use {
        it.execSQL("SELECT hashedUserId, deviceToken FROM token")
        MatcherAssert.assertThat(
            it,
            Matchers.whenQueried(
                "SELECT hashedUserId, deviceToken FROM token",
                Matchers.rowCount(org.hamcrest.Matchers.greaterThan(0))
           )
        )
    }
}

我收到以下错误

android.database.sqlite.SQLiteException: no such table: db1.db.token (code 1 SQLITE_ERROR): , while compiling: SELECT name, value FROM `db1.db.token`   
    at android.database.sqlite.SQLiteConnection.nativePrepareStatement(Native Method)

参考文献:

  1. https://medium.com/androiddevelopers/testing-room-migrations-be93cdb0d975
  2. https://medium.com/androiddevelopers/7-pro-tips-for-room-fbadea4bfbd1

您需要使用 'target context',即 InstrumentationRegistry.getInstrumentation().targetContext 作为打开数据库的上下文。

在错误中:

unable to open database: file:/data/user/0/com.myproject.db.test/databases/token

请注意,正在使用的应用程序包以 test 结尾,这是因为使用的上下文来自测试应用程序,而不是应用程序 'under test'。

我让它按如下方式工作

第 1 步:创建 SqLiteOpenHelper

的自定义实例
class DataMigrationHelper @Inject constructor(
    context: Context
): SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {

    override fun onCreate(db: SQLiteDatabase?) {
        // Already created.
    }

    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
        // No need for an upgrade
    }

    companion object {
        const val DB_NAME = "item_cache.db"
        const val DB_VERSION = 19
    }
}

第 2 步:将此实例传递给迁移任务

class MyMigration_From_4_To_5 @Inject constructor(
    private val migrationHelper: DataMigrationHelper
    ): Migration(4, 5) {
    
     override fun migrate(database: SupportSQLiteDatabase) {

         val migrationDatabase: SQLiteDatabase = migrationHelper.readableDatabase
         val nameValueList: MutableList<Array<Any>> = mutableListOf()
         with(migrationDatabase) {
             var cursor: Cursor? = null
             try {
                 cursor = rawQuery("SELECT userId, deviceToken FROM token", null)
                 if (cursor.moveToFirst()) {
                     do {
                         nameValueList.add(arrayOf(cursor.getString(0), cursor.getString(1)))
                     } while (cursor.moveToNext())
                 } else {
                     // To get around the compilation error
                    // 'if' must have both main and 'else' branches if used as an expression
                 }
             } catch (e: SQLiteException) {
                 // Swallow the exception.
             } finally {
                 cursor?.close()
             }
        }
        nameValueList.forEach {
            database.execSQL(
              "INSERT INTO `token`(userId, deviceToken) VALUES(?, ?)",
              it
            )
        }
     }
}