使用 SQLite 保存数据

将数据保存到数据库是处理重复性或结构化数据(例如联系信息)的理想方法。本页假设您熟悉 SQL 数据库,并帮助您开始在 Android 上使用 SQLite 数据库。在 Android 上使用数据库所需的 API 位于 android.database.sqlite 包中。

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

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

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

定义模式和契约类

SQL 数据库的主要原则之一是模式 (schema):它是对数据库组织的正式声明。模式体现在您用于创建数据库的 SQL 语句中。您可能会发现创建一个配套类(称为契约类)很有帮助,它可以系统地、自文档地明确指定模式的布局。

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

组织契约类的一个好方法是将整个数据库的全局定义放在类的根级别。然后为每个表创建一个内部类。每个内部类列举了相应表的列。

注意:通过实现 BaseColumns 接口,您的内部类可以继承一个名为 _ID 的主键字段,一些 Android 类(例如 CursorAdapter)期望它具有此字段。这不是必需的,但这可以帮助您的数据库与 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 helper 创建数据库

定义好数据库的样子后,您应该实现创建和维护数据库和表的方法。以下是创建和删除表的一些典型语句

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 中的一行,请使用 Cursor 的移动方法之一,在开始读取值之前必须始终调用这些方法。由于 cursor 从位置 -1 开始,调用 moveToNext() 会将“读取位置”置于结果集中的第一个条目上,并返回 cursor 是否已超出结果集中的最后一个条目。对于每一行,您可以通过调用 Cursor 的 get 方法之一(例如 getString()getLong())来读取列的值。对于每个 get 方法,您都必须传递您想要的列的索引位置,您可以通过调用 getColumnIndex()getColumnIndexOrThrow() 来获取此位置。完成结果迭代后,请在 cursor 上调用 close() 以释放其资源。例如,以下代码展示了如何获取存储在 cursor 中的所有项 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() 方法。

更新表结合了 ContentValuesinsert() 语法和 WHEREdelete() 语法。

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 命令