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