使用 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 的单个表的表名称和列名称:

Kotlin

object FeedReaderContract {
    // Table contents are grouped together in an anonymous object.
    object FeedEntry : BaseColumns {
        const val TABLE_NAME = "entry"
        const val COLUMN_NAME_TITLE = "title"
        const val COLUMN_NAME_SUBTITLE = "subtitle"
    }
}

Java

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 帮助类创建数据库

定义了数据库的外观后,您应实现用于创建和维护数据库和表的方法。以下是用于创建和删除表的一些典型语句:

Kotlin

private const val SQL_CREATE_ENTRIES =
        "CREATE TABLE ${FeedEntry.TABLE_NAME} (" +
                "${BaseColumns._ID} INTEGER PRIMARY KEY," +
                "${FeedEntry.COLUMN_NAME_TITLE} TEXT," +
                "${FeedEntry.COLUMN_NAME_SUBTITLE} TEXT)"

private const val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS ${FeedEntry.TABLE_NAME}"

Java

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 实现:

Kotlin

class FeedReaderDbHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
    override fun onCreate(db: SQLiteDatabase) {
        db.execSQL(SQL_CREATE_ENTRIES)
    }
    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        // 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)
    }
    override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        onUpgrade(db, oldVersion, newVersion)
    }
    companion object {
        // If you change the database schema, you must increment the database version.
        const val DATABASE_VERSION = 1
        const val DATABASE_NAME = "FeedReader.db"
    }
}

Java

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 的子类:

Kotlin

val dbHelper = FeedReaderDbHelper(context)

Java

FeedReaderDbHelper dbHelper = new FeedReaderDbHelper(getContext());

将信息添加到数据库

通过将 ContentValues 对象传递给 insert() 方法,将数据插入到数据库中:

Kotlin

// Gets the data repository in write mode
val db = dbHelper.writableDatabase

// Create a new map of values, where column names are the keys
val values = ContentValues().apply {
    put(FeedEntry.COLUMN_NAME_TITLE, title)
    put(FeedEntry.COLUMN_NAME_SUBTITLE, subtitle)
}

// Insert the new row, returning the primary key value of the new row
val newRowId = db?.insert(FeedEntry.TABLE_NAME, null, values)

Java

// 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 对象中返回给您。

Kotlin

val db = dbHelper.readableDatabase

// Define a projection that specifies which columns from the database
// you will actually use after this query.
val projection = arrayOf(BaseColumns._ID, FeedEntry.COLUMN_NAME_TITLE, FeedEntry.COLUMN_NAME_SUBTITLE)

// Filter results WHERE "title" = 'My Title'
val selection = "${FeedEntry.COLUMN_NAME_TITLE} = ?"
val selectionArgs = arrayOf("My Title")

// How you want the results sorted in the resulting Cursor
val sortOrder = "${FeedEntry.COLUMN_NAME_SUBTITLE} DESC"

val 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
)

Java

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
    );

第三个参数和第四个参数(selectionselectionArgs)会合并起来创建一个 WHERE 子句。由于这两个参数是与选择查询分开提供的,因此它们在合并之前会进行转义。这样一来,您的选择语句就不受 SQL 注入的影响。如需详细了解所有参数,请参阅 query() 参考文档。

如需查看光标中的某一行,请使用 Cursor move 方法之一,您必须始终在开始读取值之前调用该方法。由于光标从位置 -1 开始,因此调用 moveToNext() 会将“读取位置”置于结果的第一个条目上,并返回光标是否已经过结果集中的最后一个条目。对于每一行,您都可以通过调用 Cursor get 方法之一(例如 getString()getLong())来读取列的值。对于每个 get 方法,您必须传递所需列的索引位置,您可以通过调用 getColumnIndex()getColumnIndexOrThrow() 来获取该位置。遍历结果之后,请对光标调用 close() 以释放其资源。例如,以下代码展示了如何获取存储在光标中的所有项目 ID 并将其添加到列表中:

Kotlin

val itemIds = mutableListOf<Long>()
with(cursor) {
    while (moveToNext()) {
        val itemId = getLong(getColumnIndexOrThrow(BaseColumns._ID))
        itemIds.add(itemId)
    }
}
cursor.close()

Java

List itemIds = new ArrayList<>();
while(cursor.moveToNext()) {
  long itemId = cursor.getLong(
      cursor.getColumnIndexOrThrow(FeedEntry._ID));
  itemIds.add(itemId);
}
cursor.close();

从数据库中删除信息

如需从表中删除行,您需要提供选择条件,以标识 delete() 方法的目标行。该机制与 query() 方法的目标选择参数的工作方式相同。它将选择规范划分为选择子句和选择参数。子句定义要查看的列,并允许您合并列测试。参数是要测试的值,这些值绑定到子句中。由于结果的处理方式与常规 SQL 语句的处理方式不同,因此不受 SQL 注入的影响。

Kotlin

// Define 'where' part of query.
val selection = "${FeedEntry.COLUMN_NAME_TITLE} LIKE ?"
// Specify arguments in placeholder order.
val selectionArgs = arrayOf("MyTitle")
// Issue SQL statement.
val deletedRows = db.delete(FeedEntry.TABLE_NAME, selection, selectionArgs)

Java

// 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 语法相结合。

Kotlin

val db = dbHelper.writableDatabase

// New value for one column
val title = "MyNewTitle"
val values = ContentValues().apply {
    put(FeedEntry.COLUMN_NAME_TITLE, title)
}

// Which row to update, based on the title
val selection = "${FeedEntry.COLUMN_NAME_TITLE} LIKE ?"
val selectionArgs = arrayOf("MyOldTitle")
val count = db.update(
        FeedEntry.TABLE_NAME,
        values,
        selection,
        selectionArgs)

Java

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() 中关闭数据库。

Kotlin

override fun onDestroy() {
    dbHelper.close()
    super.onDestroy()
}

Java

@Override
protected void onDestroy() {
    dbHelper.close();
    super.onDestroy();
}

调试数据库

Android SDK 提供一个 sqlite3 shell 工具,您可以使用该工具来浏览表内容、运行 SQL 命令以及在 SQLite 数据库中执行其他实用操作。如需了解详情,请参阅如何发出 shell 命令