翻译自android官网,可直接去官网观看
Jetpack系列之Room----入门(三)
- 迁移 Room 数据库
-
- 测试迁移
-
- 导出架构
- 测试单次迁移
-
- 测试所有迁移
- 妥善处理缺失的迁移路径
- 升级到 Room 2.2.0 时处理列默认值
- 测试和调试数据库
-
- 测试您的数据库
-
- 在 Android 设备上测试数据库
- 在主机上测试数据库
- 调试数据库
- 使用 Room 引用复杂数据
-
- 使用类型转换器
- 了解 Room 为何不允许对象引用
- 使用 SQLite 保存数据
-
- 定义架构和协定
- 使用 SQL 帮助程序创建数据库
- 将信息添加到数据库
- 从数据库中读取信息
- 从数据库中删除信息
- 更新数据库
- 保留数据库连接
- 调试数据库
迁移 Room 数据库
当您在应用中添加和更改功能时,需要修改 Room 实体类以反映这些更改。但是,如果应用更新更改了数据库架构,那么保留设备内置数据库中已有的用户数据就非常重要。
Room 持久性库支持通过 Migration 类进行增量迁移以满足此需求。每个 Migration 子类通过替换 Migration.migrate() 方法定义 startVersion 和 endVersion 之间的迁移路径。
当应用更新需要升级数据库版本时,Room 会从一个或多个 Migration 子类运行 migrate() 方法,以在运行时将数据库迁移到最新版本
:
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, "
+ "`name` TEXT, PRIMARY KEY(`id`))");
}
};
static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE Book "
+ " ADD COLUMN pub_year INTEGER");
}
};
Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();
注意:为使迁移逻辑正常工作,请使用完整查询,而不要引用表示查询的常量。
迁移过程完成后,Room 会验证架构以确保迁移成功完成。如果 Room 发现问题,就会抛出包含不匹配信息的异常。
如需了解详情,请参阅 GitHub 上的 Room 迁移示例。
测试迁移
迁移通常十分复杂,迁移定义错误可能会导致应用崩溃。为了保持应用的稳定性,您应测试迁移。Room 提供了一个 room-testing Maven 工件来协助完成此测试过程。不过,为使此工件正常工作,您必须先导出数据库的架构。
导出架构
Room 可以在编译时将数据库的架构信息导出为 JSON 文件。如需导出架构,请在 app/build.gradle 文件中设置 room.schemaLocation 注释处理器属性:
build.gradle
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments += ["room.schemaLocation":
"$projectDir/schemas".toString()]
}
}
}
}
导出的 JSON 文件代表数据库的架构历史记录。您应将这些文件存储在版本控制系统中,因为此系统允许 Room 出于测试目的创建较旧版本的数据库。
测试单次迁移
测试迁移之前,您必须先将 Room 中的 androidx.room:room-testing Maven 工件添加至测试依赖项中,并将导出的架构的位置添加为资源目录:
build.gradle
android {
...
sourceSets {
// Adds exported schema location as test app assets.
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
}
dependencies {
...
testImplementation "androidx.room:room-testing:2.2.5"
}
测试软件包提供了可读取导出的架构文件的 MigrationTestHelper 类。该软件包还实现了 JUnit4 TestRule 接口,因此可以管理创建的数据库。
以下示例演示了针对单次迁移的测试:
@RunWith(AndroidJUnit4.class)
public class MigrationTest {
private static final String TEST_DB = "migration-test";
@Rule
public MigrationTestHelper helper;
public MigrationTest() {
helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
MigrationDb.class.getCanonicalName(),
new FrameworkSQLiteOpenHelperFactory());
}
@Test
public void migrate1To2() throws IOException {
SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);
// db has schema version 1. insert some data using SQL queries.
// You cannot use DAO classes because they expect the latest schema.
db.execSQL(...);
// Prepare for the next version.
db.close();
// Re-open the database with version 2 and provide
// MIGRATION_1_2 as the migration process.
db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2);
// MigrationTestHelper automatically verifies the schema changes,
// but you need to validate that the data was migrated properly.
}
}
测试所有迁移
虽然可以测试单次增量迁移,但建议您添加一个测试,涵盖为应用的数据库定义的所有迁移。这可确保最近创建的数据库实例与遵循定义的迁移路径的旧实例之间不存在差异。
以下示例演示了针对所有定义的迁移的测试:
@RunWith(AndroidJUnit4.class)
public class MigrationTest {
private static final String TEST_DB = "migration-test";
@Rule
public MigrationTestHelper helper;
public MigrationTest() {
helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
AppDatabase.class.getCanonicalName(),
new FrameworkSQLiteOpenHelperFactory());
}
@Test
public void migrateAll() throws IOException {
// Create earliest version of the database.
SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);
db.close();
// Open latest version of the database. Room will validate the schema
// once all migrations execute.
AppDatabase appDb = Room.databaseBuilder(
InstrumentationRegistry.getInstrumentation().getTargetContext(),
AppDatabase.class,
TEST_DB)
.addMigrations(ALL_MIGRATIONS).build();
appDb.getOpenHelper().getWritableDatabase();
appDb.close();
}
// Array of all migrations
private static final Migration[] ALL_MIGRATIONS = new Migration[]{
MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4};
}
妥善处理缺失的迁移路径
如果 Room 无法找到将设备上的现有数据库升级到当前版本的迁移路径,就会发生 IllegalStateException。在迁移路径缺失的情况下,如果丢失现有数据可以接受,请在创建数据库时调用 fallbackToDestructiveMigration() 构建器方法:
Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name")
.fallbackToDestructiveMigration()
.build();
fallbackToDestructiveMigration此方法会指示 Room 在需要执行没有定义迁移路径的增量迁移时,破坏性地重新创建应用的数据库表。
警告:在应用的数据库构建器中设置此选项意味着 Room 在尝试执行没有定义迁移路径的迁移时会从数据库表中永久删除所有数据。
如果您只想让 Room 在特定情况下回退到破坏性重新创建,可以使用 fallbackToDestructiveMigration() 的一些替代选项:
- 如果特定版本的架构历史记录导致迁移路径出现无法解决的问题,请改用 fallbackToDestructiveMigrationFrom()。此方法表示您仅在从特定版本迁移时才希望 Room 回退到破坏性重新创建。
- 如果您仅在从较高数据库版本迁移到较低数据库版本时才希望 Room 回退到破坏性重新创建,请改用 fallbackToDestructiveMigrationOnDowngrade()。
注意:在 2.2.0 及更高版本中,Room 可以在某些回退迁移情况下使用预打包的数据库文件,而无需留空数据库。如需了解详情,请参阅预填充 Room 数据库。
升级到 Room 2.2.0 时处理列默认值
在 Room 2.2.0 及更高版本中,您可以使用注释 @ColumnInfo(defaultValue = “…”) 定义列的默认值。在
低于 2.2.0 的版本中,为列定义默认值的唯一方法是直接在执行的 SQL 语句中定义默认值,这样创建的默认值是 Room 不知道的值
。这意味着,如果数据库最初由版本低于 2.2.0 的 Room 创建,升级应用以使用 Room 2.2.0 就可能需要您为那些在未使用 Room API 的情况下定义的现有默认值提供特殊的迁移路径。
例如,假设数据库的版本 1 定义了一个 Song 实体:
// Song Entity, DB Version 1, Room 2.1.0
@Entity
public class Song {
@PrimaryKey
final long id;
final String title;
}
同时,假设同一数据库的版本 2 添加了新的 NOT NULL 列并定义了从版本 1 到版本 2 的迁移路径:
// Song Entity, DB Version 2, Room 2.1.0
@Entity
public class Song {
@PrimaryKey
final long id;
final String title;
@NonNull
final String tag; // Added in version 2.
}
// Migration from 1 to 2, Room 2.1.0
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL(
"ALTER TABLE Song ADD COLUMN tag TEXT NOT NULL DEFAULT ''");
}
};
这会导致更新的应用与全新安装的应用之间存在底层表差异。由于
tag 列的默认值仅在从版本 1 到版本 2 的迁移路径中进行了声明,因此从版本 2 开始安装该应用的用户的数据库架构中没有 tag 的默认值
。
在版本低于 2.2.0 的 Room 中,此差异不会产生任何不良后果。
但是,如果应用稍后升级以使用 Room 2.2.0 或更高版本,并使用 @ColumnInfo 注释更改 Song 实体类以包含 tag 的默认值,那么 Room 现在就会发现此差异。这会导致架构验证失败。
如需确保在早期迁移路径中声明列默认值的情况下在所有用户之间保持数据库架构的一致性,请在首次升级应用以使用 Room 2.2.0 或更高版本时执行以下操作:
- 使用 @ColumnInfo 注释在各自的实体类中声明列默认值。
- 将数据库版本号增加 1。
- 定义实现了删除并重新创建策略的新版本迁移路径,将必要的默认值添加到现有列。
注意:如果应用的数据库回退到破坏性迁移,或者如果没有迁移路径添加带有默认值的列,就不需要此过程。
以下示例演示了此过程:
// Migration from 2 to 3, Room 2.2.0
static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("CREATE TABLE new_Song (" +
"id INTEGER PRIMARY KEY NOT NULL," +
"name TEXT," +
"tag TEXT NOT NULL DEFAULT '')");
database.execSQL("INSERT INTO new_Song (id, name, tag) " +
"SELECT id, name, tag FROM Song");
database.execSQL("DROP TABLE Song");
database.execSQL("ALTER TABLE new_Song RENAME TO Song");
}
};
测试和调试数据库
在使用 Room 持久性库创建数据库时,请务必验证应用的数据库以及用户的数据是否稳定。本页介绍了如何测试数据库和执行调试步骤,以帮助您通过测试。
测试您的数据库
您可以通过以下两种方式测试数据库:
- 在 Android 设备上测试。
- 在主机开发计算机上测试(不推荐)。
如需了解专门针对数据库迁移的测试,请参阅测试迁移。
注意:在针对应用运行测试时,可以借助 Room 创建 DAO 类的模拟实例。这样一来,如果您不测试数据库本身,就无需创建完整的数据库。此功能是可行的,因为您的 DAO 不会泄露数据库的任何详细信息。
在 Android 设备上测试数据库
如需测试数据库实现,推荐的方法是编写在 Android 设备上运行的 JUnit 测试。由于执行这些测试不需要创建 Activity,因此它们的执行速度应该比界面测试速度快。
在设置测试时,您应创建内存版本的数据库以使测试更加封闭,如以下示例所示:
@RunWith(AndroidJUnit4.class)
public class SimpleEntityReadWriteTest {
private UserDao userDao;
private TestDatabase db;
@Before
public void createDb() {
Context context = ApplicationProvider.getApplicationContext();
db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
userDao = db.getUserDao();
}
@After
public void closeDb() throws IOException {
db.close();
}
@Test
public void writeUserAndReadInList() throws Exception {
User user = TestUtil.createUser(3);
user.setName("george");
userDao.insert(user);
List<User> byName = userDao.findUsersByName("george");
assertThat(byName.get(0), equalTo(user));
}
}
在主机上测试数据库
Room 使用 SQLite 支持库,该支持库提供了与 Android 框架类中的接口相对应的接口。通过此项支持,您可以传递该支持库的自定义实现来测试数据库查询。
注意:虽然此设置可让测试快速运行,但我们不建议这样做,因为您的设备以及用户设备上运行的 SQLite 版本可能与主机上的版本不一致。
调试数据库
Android SDK 包含一个 sqlite3 数据库工具,可用于检查应用的数据库。它包含用于输出表格内容的 .dump 以及用于输出现有表格的 SQL CREATE 语句的 .schema 等命令。
您也可以从命令行执行 SQLite 命令,如以下代码段所示:
adb -s emulator-5554 shell
sqlite3 /data/data/your-app-package/databases/rssitems.db
如需了解详情,请参阅 SQLite 网站上提供的 sqlite3 命令行文档。
使用 Room 引用复杂数据
Room 提供了在primitive 类型和boxed 类型之间进行转换的功能,但不允许在实体之间进行对象引用。本文档介绍了如何使用类型转换器,以及 Room 为何不支持对象引用。
使用类型转换器
有时,您的应用需要使用自定义数据类型,其中包含您想要存储到单个数据库列中的值。如需为自定义类型添加此类支持,您需要提供一个 TypeConverter,它可以在自定义类与 Room 可以保留的已知类型之间来回转换。
例如,如需保留 Date 的实例,可以编写以下 TypeConverter 将等效的 Unix 时间戳存储在数据库中:
public class Converters {
@TypeConverter
public static Date fromTimestamp(Long value) {
return value == null ? null : new Date(value);
}
@TypeConverter
public static Long dateToTimestamp(Date date) {
return date == null ? null : date.getTime();
}
}
前面的示例定义了 2 个函数,一个用于将 Date 对象转换为 Long 对象,另一个用于执行从 Long 到 Date 的反向转换。由于 Room 已经知道如何保留 Long 对象,因此可以使用此转换器保留 Date 类型的值。
接下来,将 @TypeConverters 注释添加到 AppDatabase 类中,以便 Room 可以使用您为该 AppDatabase 中的每个实体和 DAO 定义的转换器:
AppDatabase
@Database(entities = {User.class}, version = 1)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
通过使用这些转换器,您就可以在其他查询中使用自定义类型,就像使用primitive 类型一样,如以下代码段所示:
User
@Entity
public class User {
private Date birthday;
}
UserDao
@Dao
public interface UserDao {
@Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
List<User> findUsersBornBetweenDates(Date from, Date to);
}
您还可以将 @TypeConverters 限制为不同的范围,包括个别实体、DAO 和 DAO 方法。如需了解详情,请参阅 @TypeConverters 注释的参考文档。
了解 Room 为何不允许对象引用
要点:Room 不允许在实体类之间进行对象引用。因此,您必须明确请求应用所需的数据。
映射从数据库到相应对象模型之间的关系是一种常见做法,非常适合服务器端。即使程序在访问字段时加载字段,服务器仍然可以正常工作。
但在客户端,这种延迟加载是不可行的,因为它通常发生在界面线程上,并且在界面线程上查询磁盘上的信息会导致严重的性能问题。界面线程通常需要大约 16 毫秒来计算和绘制 Activity 的更新后的布局,因此,即使查询只用了 5 毫秒,您的应用仍然可能会用尽剩余的时间来绘制框架,从而导致明显的显示故障。如果有一个并行运行的单独事务,或者设备正在运行其他磁盘密集型任务,则查询可能需要更多时间才能完成。不过,如果您不使用延迟加载,则应用会抓取一些不必要的数据,从而导致内存消耗问题。
对象关系型映射通常将决定权留给开发者,以便他们可以针对自己的应用用例执行最合适的操作。开发者通常会决定在应用和界面之间共享模型。但是,这种解决方案不能很好地扩展,因为界面会不断发生变化,共享模型会出现开发者难以预测和调试的问题。
例如,假设界面加载了 Book 对象的列表,其中每本图书都有一个 Author 对象。您最初可能设计让查询使用延迟加载,从而让 Book 实例检索作者。对 author 字段的第一次检索会查询数据库。一段时间后,您会发现还需要在应用界面中显示作者姓名。您可以轻松访问此名称,如以下代码段所示:
但是,这种看似无害的更改会导致在主线程上查询 Author 表格。
如果您事先查询作者信息,则在您不再需要这些数据时,就会很难更改数据的加载方式。例如,如果应用界面不再需要显示 Author 信息,则应用会有效地加载不再显示的数据,从而浪费宝贵的内存空间。如果 Author 类引用其他表格(例如 Books),则应用的效率会进一步下降。
如需使用 Room 同时引用多个实体,请改为创建包含每个实体的 POJO,然后编写用于联接相应表格的查询。这种结构合理的模型与 Room 强大的查询验证功能相结合,可让您的应用在加载数据时消耗较少的资源,从而改善应用的性能和用户体验。
使用 SQLite 保存数据
对于重复数据或结构化数据(例如联系信息),将数据保存到数据库是理想选择。本页假设您已基本熟悉 SQL 数据库,并可帮助您开始在 Android 上使用 SQLite 数据库。android.database.sqlite 软件包中提供了在 Android 上使用数据库所需的 API。
注意:虽然这些 API 功能强大,但它们的级别较低,使用起来需要花费大量时间和精力:
- 没有针对原始 SQL 查询的编译时验证。
。随着数据图表不断发生变化,您需要手动更新受影响的 SQL 查询。此过程既耗时又容易出错
- 您需要使用大量样板代码,在 SQL 查询和数据对象之间进行转换。
出于这些原因,我们强烈建议将 Room 持久性库用作抽象层以访问应用的 SQLite 数据库中的信息。
定义架构和协定
SQL 数据库的主要原则之一是架构,即数据库组织方式的正式声明。架构反映在您用于创建数据库的 SQL 语句中。您可能会发现创建伴随类(称为协定类)很有用,该类以系统化、自记录的方式明确指定了架构的布局。
协定类是定义 URI、表和列名称的常量的容器。通过协定类,您可以在同一软件包的所有其他类中使用相同的常量。这样一来,您就可以在一个位置更改列名称并将其传播到整个代码中。
组织协定类的一种良好方法是将对整个数据库而言具有全局性的定义放入类的根级别。然后,为每个表创建一个内部类。每个内部类都枚举相应表的列。
注意:通过实现 BaseColumns 接口,您的内部类可以继承名为 _ID 的主键字段,CursorAdapter 等某些 Android 类需要内部类拥有该字段。
这并非强制要求,但这样做有助于您的数据库与 Android 框架和谐地工作
。
例如,以下协定定义了表示 RSS Feed 的单个表的表名称和列名称:
public final class FeedReaderContract {
// To prevent someone from accidentally instantiating the contract class,
// make the constructor private.
private FeedReaderContract() {}
/* Inner class that defines the table contents */
public static class FeedEntry implements BaseColumns {
public static final String TABLE_NAME = "entry";
public static final String COLUMN_NAME_TITLE = "title";
public static final String COLUMN_NAME_SUBTITLE = "subtitle";
}
}
使用 SQL 帮助程序创建数据库
定义了数据库的外观后,您应实现用于创建和维护数据库和表的方法。以下是用于创建和删除表的一些典型语句:
private static final String SQL_CREATE_ENTRIES =
"CREATE TABLE " + FeedEntry.TABLE_NAME + " (" +
FeedEntry._ID + " INTEGER PRIMARY KEY," +
FeedEntry.COLUMN_NAME_TITLE + " TEXT," +
FeedEntry.COLUMN_NAME_SUBTITLE + " TEXT)";
private static final String SQL_DELETE_ENTRIES =
"DROP TABLE IF EXISTS " + FeedEntry.TABLE_NAME;
就像您在设备的内部存储空间中保存文件一样,Android 会将您的数据库存储在您应用的私有文件夹中。您的数据安全无虞,因为在默认情况下,其他应用或用户无法访问此区域。
SQLiteOpenHelper 类包含一组用于管理数据库的实用 API。当您使用此类获取对数据库的引用时,系统仅在需要时才执行可能需要长时间运行的数据库创建和更新操作,而不是在应用启动期间执行。您仅需调用 getWritableDatabase() 或 getReadableDatabase() 即可。
注意:由于这些操作可能会长时间运行,因此请务必在后台线程中调用 getWritableDatabase() 或 getReadableDatabase()。如需了解详情,请参阅 Android 上的线程。
如需使用 SQLiteOpenHelper,请创建一个用于替换 onCreate() 和 onUpgrade() 回调方法的子类。您可能还需要实现 onDowngrade() 或 onOpen() 方法,但这些方法并非必需。
例如,下面展示了使用上述一些命令的 SQLiteOpenHelper 实现:
public class FeedReaderDbHelper extends SQLiteOpenHelper {
// If you change the database schema, you must increment the database version.
public static final int DATABASE_VERSION = 1;
public static final String DATABASE_NAME = "FeedReader.db";
public FeedReaderDbHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
public void onCreate(SQLiteDatabase db) {
db.execSQL(SQL_CREATE_ENTRIES);
}
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// This database is only a cache for online data, so its upgrade policy is
// to simply to discard the data and start over
db.execSQL(SQL_DELETE_ENTRIES);
onCreate(db);
}
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
onUpgrade(db, oldVersion, newVersion);
}
}
如需访问您的数据库,请实例化 SQLiteOpenHelper 的子类:
将信息添加到数据库
通过将 ContentValues 对象传递给 insert() 方法,将数据插入到数据库中:
// Gets the data repository in write mode
SQLiteDatabase db = dbHelper.getWritableDatabase();
// Create a new map of values, where column names are the keys
ContentValues values = new ContentValues();
values.put(FeedEntry.COLUMN_NAME_TITLE, title);
values.put(FeedEntry.COLUMN_NAME_SUBTITLE, subtitle);
// Insert the new row, returning the primary key value of the new row
long newRowId = db.insert(FeedEntry.TABLE_NAME, null, values);
insert() 的第一个参数就只是表名。
第二个参数将指示框架在 ContentValues 为空(即您没有 put 任何值)时应执行哪些操作。如果您指定列名称,框架会插入一行,并将该列的值设置为 null。如果您指定 null(如此代码示例所示),框架在没有值时不会插入行。
insert() 方法会返回新创建行的 ID;如果在插入数据时出错,会返回 -1。如果您的数据与数据库中已有的数据之间存在冲突,就会出现这种情况。
从数据库中读取信息
如需从数据库中读取信息,请使用 query() 方法,向其传递您的选择条件和所需的列。该方法合并了 insert() 和 update() 元素,不过列列表定义了要提取的数据(“预测值”),而不是要插入的数据。查询结果会包含在 Cursor 对象中返回给您。
SQLiteDatabase db = dbHelper.getReadableDatabase();
// Define a projection that specifies which columns from the database
// you will actually use after this query.
String[] projection = {
BaseColumns._ID,
FeedEntry.COLUMN_NAME_TITLE,
FeedEntry.COLUMN_NAME_SUBTITLE
};
// Filter results WHERE "title" = 'My Title'
String selection = FeedEntry.COLUMN_NAME_TITLE + " = ?";
String[] selectionArgs = { "My Title" };
// How you want the results sorted in the resulting Cursor
String sortOrder =
FeedEntry.COLUMN_NAME_SUBTITLE + " DESC";
Cursor cursor = db.query(
FeedEntry.TABLE_NAME, // The table to query
projection, // The array of columns to return (pass null to get all)
selection, // The columns for the WHERE clause
selectionArgs, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
sortOrder // The sort order
);
第三个参数和第四个参数(selection 和 selectionArgs)会合并起来创建一个 WHERE 子句。由于
这两个参数是与选择查询分开提供的,因此它们在合并之前会进行转义
。这样一来,您的选择语句就不受 SQL 注入的影响。如需详细了解所有参数,请参阅 query() 参考。
如需查看光标中的某一行,请使用 Cursor move 方法之一,您必须始终在开始读取值之前调用该方法。由于
光标从位置 -1 开始,因此调用 moveToNext() 会将“读取位置”置于结果的第一个条目上,并返回光标是否已经过结果集中的最后一个条目
。对于每一行,您都可以通过调用 Cursor get 方法之一(例如 getString() 或 getLong())读取列的值。对于每个 get 方法,您必须传递所需列的索引位置,您可以通过调用 getColumnIndex() 或 getColumnIndexOrThrow() 获取该位置。遍历结果之后,请对光标调用 close() 以释放其资源。例如,以下代码展示了如何获取存储在光标中的所有项目 ID 并将其添加到列表中:
List itemIds = new ArrayList<>();
while(cursor.moveToNext()) {
long itemId = cursor.getLong(
cursor.getColumnIndexOrThrow(FeedEntry._ID));
itemIds.add(itemId);
}
cursor.close();
从数据库中删除信息
如需从表中删除行,您需要提供选择条件,以标识 delete() 方法的目标行。该机制与 query() 方法的目标选择参数的工作方式相同。它将选择规范划分为选择子句和选择参数。子句定义要查看的列,并允许您合并列测试。参数是要测试的值,这些值绑定到子句中。由于结果的处理方式与常规 SQL 语句的处理方式不同,因此不受 SQL 注入的影响。
// Define 'where' part of query.
String selection = FeedEntry.COLUMN_NAME_TITLE + " LIKE ?";
// Specify arguments in placeholder order.
String[] selectionArgs = { "MyTitle" };
// Issue SQL statement.
int deletedRows = db.delete(FeedEntry.TABLE_NAME, selection, selectionArgs);
delete() 方法的返回值表示从数据库中删除的行数。
更新数据库
如果您需要修改数据库值的子集,请使用 update() 方法。
更新表可将 insert() 的 ContentValues 语法与 delete() 的 WHERE 语法相结合。
SQLiteDatabase db = dbHelper.getWritableDatabase();
// New value for one column
String title = "MyNewTitle";
ContentValues values = new ContentValues();
values.put(FeedEntry.COLUMN_NAME_TITLE, title);
// Which row to update, based on the title
String selection = FeedEntry.COLUMN_NAME_TITLE + " LIKE ?";
String[] selectionArgs = { "MyOldTitle" };
int count = db.update(
FeedReaderDbHelper.FeedEntry.TABLE_NAME,
values,
selection,
selectionArgs);
update() 方法的返回值是数据库中受影响的行数。
保留数据库连接
由于在数据库关闭时,调用 getWritableDatabase() 和 getReadableDatabase() 的成本比较高,因此只要您有可能需要访问数据库,就应保持数据库连接处于打开状态。通常情况下,最好在发出调用的 Activity 的 onDestroy() 中关闭数据库。
@Override
protected void onDestroy() {
dbHelper.close();
super.onDestroy();
}
调试数据库
Android SDK 提供一个 sqlite3 shell 工具,您可以使用该工具浏览表内容、运行 SQL 命令以及在 SQLite 数据库中执行其他实用操作。如需了解详情,请参阅如何发出 shell 命令。