我们在 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
在 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
}
}