Android SQLite 在更改表后返回陈旧数据

问题描述 投票:0回答:1

我们在 Android SQLite 上遇到了一个奇怪的错误:当我们创建数据库时,关闭它,重新打开它,添加列和查询,例如再次计算列数,我们得到旧值。

我们正在使用最新的构建工具和目标 SDK 版本 (33)。这是一个演示该问题的简短测试:

import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase.*
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteOpenHelper
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.After
import org.junit.Assert.assertArrayEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class SmallBugTest {

    private val TEST_DB = "small-bug-test"
    private val context = InstrumentationRegistry.getInstrumentation().context
    private lateinit var db: SupportSQLiteDatabase

    @Before
    fun setup() {
        //The bug doesn't seem to appear if everything is done is one session (at least with this suite), so let's simulate two sessions
        /* Database is created */
        SupportSQLiteOpenHelper.Configuration(context, TEST_DB,
            object : SupportSQLiteOpenHelper.Callback(1) {
                override fun onCreate(db: SupportSQLiteDatabase) = createDb(db)
                override fun onUpgrade(
                    db: SupportSQLiteDatabase,
                    oldVersion: Int,
                    newVersion: Int
                ) = throw AssertionError()
            }).let { FrameworkSQLiteOpenHelperFactory().create(it).writableDatabase }.close()

        /* Database is closed, e.g. the app has been closed */

        ////////////////////////////////////////////

        /* Database is opened, not created */

        db = SupportSQLiteOpenHelper.Configuration(context, TEST_DB,
            object : SupportSQLiteOpenHelper.Callback(1) {
                override fun onCreate(db: SupportSQLiteDatabase) = throw AssertionError()
                override fun onUpgrade(
                    db: SupportSQLiteDatabase,
                    oldVersion: Int,
                    newVersion: Int
                ) = throw AssertionError()
            }).let { FrameworkSQLiteOpenHelperFactory().create(it).writableDatabase }
    }

    private fun createDb(db: SupportSQLiteDatabase) {
        db.execSQL(
            "CREATE TABLE `TEST_TABLE` (" + //
                    "`column0` TEXT NOT NULL," +
                    "`column1` TEXT NOT NULL" +
                    ")"
        )
        db.execSQL("INSERT INTO `TEST_TABLE` (`column0`, `column1`) VALUES ('content0', 'content1')")

        db.version = 1
    }

    @Test
    fun causeBug() { //If this test is successful, the bug occurred
        db.query("SELECT * FROM TEST_TABLE").close()

        modifySchema(db)

        db.query("SELECT * FROM TEST_TABLE").use {
            it.moveToFirst()
            assertArrayEquals(
                arrayOf("column0", "column1"),
                it.columnNames
            ) //Should be ["column0", "column1", "column2"]
        }
    }

    private fun modifySchema(db: SupportSQLiteDatabase) {
        db.execSQL("ALTER TABLE `TEST_TABLE` RENAME TO `TEST_TABLE_OLD`")
        db.execSQL(
            "CREATE TABLE `TEST_TABLE` (" + //
                    "`column0` TEXT NOT NULL," +
                    "`column1` TEXT NOT NULL," +
                    "`column2` TEXT" +
                    ")"
        )
        db.execSQL("INSERT INTO `TEST_TABLE` (`column0`, `column1`) SELECT `column0`, `column1` FROM `TEST_TABLE_OLD`")
        db.execSQL("DROP TABLE `TEST_TABLE_OLD`")
        db.update(
            "TEST_TABLE",
            CONFLICT_NONE,
            ContentValues().also { it.put("column2", "content2"); },
            null,
            null
        )
    }
}

这是我们已经发现的:查询被分派到

(Support)SQLiteDatabase
,匹配的
PreparedStatement
acquirePreparedStatement
返回,然后执行。
acquirePreparedStatement
要么创建一个新的 PS,然后将其缓存在
PreparedStatementCache mPreparedStatementCache
中,要么返回之前缓存的 PS。如果 PS 是由完全相同的 sql 文本创建的,则它匹配(添加反引号或注释将导致相应地返回新的 PS。)
针对查询的光标上的某些操作(例如
Cursor#getX
)将始终填充新数据 对于每次调用
query
,但某些操作(例如
getColumnCount
getColumnNames
)将仅在每个 PS 中填充 一次(或者更准确地说:它们似乎总是填充了 PS 创建时的数据。)

因此,如果调度并缓存查询然后将表更改为具有更多列然后调度具有相同sql文本的查询,则第二个查询产生的游标将在内部使用相同的PS并返回正确的新列内容值,但仍会报告相同的内容(现已过时!)

columnCounts
columnNames

Google 错误跟踪器中还存在一个问题,我们已注意到我们的发现,但尚未找到解决方法:https://issuetracker.google.com/issues/153521693#comment18

您遇到过这个问题并找到解决方法吗?

如果您想查看它的实际效果,我们创建了此示例项目:https://github.com/SailReal/Android-Issue-153521693

android sqlite android-room
1个回答
0
投票

在 SQLite 中更改数据库架构时,例如添加列或修改现有列,您还应该增加数据库版本。

当您增加版本号并在 SQLiteOpenHelper 子类的 onUpgrade() 方法中实现必要的更改时

import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper

class MyDBHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {

    companion object {
        private const val DATABASE_VERSION = 2
        private const val DATABASE_NAME = "MyDatabase.db"
    }

    override fun onCreate(db: SQLiteDatabase) {
        // Create your initial database schema here
        // This method is only called when the database is first created
        db.execSQL("CREATE TABLE my_table (id INTEGER PRIMARY KEY, name TEXT)")
    }

    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        // Handle database schema upgrades here
        if (oldVersion < 2) {
            // Add new columns or modify existing schema
            db.execSQL("ALTER TABLE my_table ADD COLUMN age INTEGER")
        }
        // Handle other version upgrades as necessary
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.