使用 SQLite 保存数据

将数据保存到数据库非常适合重复或结构化数据,例如联系信息。本页面假设您熟悉一般的 SQL 数据库,并帮助您开始使用 Android 上的 SQLite 数据库。您需要使用数据库的 Android API 位于 android.database.sqlite 包中。

注意:虽然这些 API 功能强大,但它们相当底层,需要大量的时间和精力才能使用

  • 对原始 SQL 查询没有编译时验证。当您的数据图发生变化时,您需要手动更新受影响的 SQL 查询。此过程可能很耗时且容易出错。
  • 您需要使用大量样板代码来在 SQL 查询和数据对象之间进行转换。

出于这些原因,我们 强烈建议 使用 Room 持久层库 作为访问应用 SQLite 数据库中信息的抽象层。

定义架构和合约

SQL 数据库的主要原则之一是架构:对数据库组织方式的正式声明。架构反映在您用于创建数据库的 SQL 语句中。您可能会发现创建称为合约类的配套类很有帮助,该类以系统且自文档化的方式明确指定架构的布局。

合约类是常量的容器,这些常量定义了 URI、表和列的名称。合约类允许您在同一包中的所有其他类中使用相同的常量。这使您可以在一个地方更改列名,并使其在整个代码中传播。

组织合约类的一种好方法是将对整个数据库通用的定义放在类的根级别。然后为每个表创建一个内部类。每个内部类都列出了相应表的列。

注意:通过实现 BaseColumns 接口,您的内部类可以继承一个名为 _ID 的主键字段,一些 Android 类(例如 CursorAdapter)希望它拥有该字段。这不是必需的,但这可以帮助您的数据库与 Android 框架和谐地工作。

例如,以下合约定义了表示 RSS 提要的单个表的表名和列名

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 的移动方法之一,在开始读取值之前您必须始终调用这些方法。由于游标从位置 -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() 方法。

更新表将 ContentValues 语法(与 insert() 相同)与 WHERE 语法(与 delete() 相同)结合在一起。

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() 在数据库关闭时调用成本很高,因此您应该尽可能长时间地保持数据库连接打开以访问它。通常,最佳做法是在调用活动的 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 命令