首页 / Android 入门教程 / SQLite 数据库

Android 入门教程

SQLite 数据库

本教程共 100 篇 · 第 67 篇 · 更新于 2026-07-28 · 约 9 分钟阅读

AndroidAndroid 入门教程SQLiteSQLiteOpenHelper数据库CRUD事务

67. SQLite 数据库

本节目标:学会用 SQLiteOpenHelper 创建和管理数据库,掌握增删改查(CRUD)操作和事务处理,为下一章学习 Room 打好基础。

SQLite 是什么

SQLite 是 Android 内置的轻量级关系型数据库。它不需要独立的数据库服务进程,整个数据库就是一个文件,适合移动设备存储结构化数据。

打个比方:如果 SharedPreferences 是便签纸,那 SQLite 就是 Excel 表格。你能建多张表、设主键、做查询、排序过滤,适合存大量有条理的数据。

Note
  • 官方现在推荐用 Room 持久化库替代直接使用 SQLite API(第 68 章会讲)。Room 是 SQLite 的封装,减少了样板代码、提供了编译期 SQL 检查和协程支持。但理解原生 SQLite 是学习 Room 的前提。

SQLiteOpenHelper

SQLiteOpenHelper 是管理数据库创建和升级的帮手类。继承它需要实现两个方法:

  • onCreate():数据库首次创建时调用,建表。
  • onUpgrade():数据库版本号升高时调用,做表结构迁移。
class AppDatabase(context: Context) :
    SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {

    companion object {
        const val DB_NAME = "app.db"
        const val DB_VERSION = 1
    }

    override fun onCreate(db: SQLiteDatabase) {
        // 建表
        db.execSQL("""
            CREATE TABLE users (
                _id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL,
                age INTEGER,
                email TEXT
            )
        """)
    }

    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        // 版本升级时执行(简单做法:删表重建,生产环境要写迁移逻辑)
        db.execSQL("DROP TABLE IF EXISTS users")
        onCreate(db)
    }
}

使用

val dbHelper = AppDatabase(context)
val db = dbHelper.writableDatabase  // 可读写的数据库对象
// 或
val db = dbHelper.readableDatabase  // 只读
Warning
  • onUpgrade 里直接 DROP TABLE 再重建会丢失数据!这只是演示。生产环境要写增量迁移,比如 ALTER TABLE users ADD COLUMN phone TEXT。每次升级只加改动部分。

增(Insert)

fun addUser(db: SQLiteDatabase, name: String, age: Int, email: String): Long {
    val values = ContentValues().apply {
        put("name", name)
        put("age", age)
        put("email", email)
    }
    // 返回新行的 id
    return db.insert("users", null, values)
}

// 使用
val dbHelper = AppDatabase(context)
val db = dbHelper.writableDatabase
val newId = addUser(db, "张三", 25, "zhangsan@example.com")
Note
  • ContentValues 是键值对容器,存放列名和值。insert() 第二个参数 nullColumnHack:当 values 为空时,插入一个 null 值的列名,防止 INSERT INTO users () VALUES () 这种非法 SQL。传 null 通常即可。

查(Query)

用 query 方法

fun queryUsers(db: SQLiteDatabase): List<User> {
    val cursor = db.query(
        "users",                     // 表名
        arrayOf("_id", "name", "age"), // 查询列
        "age > ?",                   // WHERE 条件
        arrayOf("20"),               // 条件参数
        null,                         // GROUP BY
        null,                         // HAVING
        "age DESC"                    // ORDER BY
    )

    val users = mutableListOf<User>()
    cursor.use {
        while (it.moveToNext()) {
            val id = it.getLong(it.getColumnIndexOrThrow("_id"))
            val name = it.getString(it.getColumnIndexOrThrow("name"))
            val age = it.getInt(it.getColumnIndexOrThrow("age"))
            users.add(User(id, name, age))
        }
    }
    return users
}

用 rawQuery 写 SQL

val cursor = db.rawQuery(
    "SELECT * FROM users WHERE age > ? ORDER BY age DESC",
    arrayOf("20")
)
Tip
  • 无论用 query 还是 rawQuery,条件值都用 ? 占位符配合参数数组,不要拼接字符串。拼接字符串有 SQL 注入风险,和第 38 章 ContentProvider 讲的一样。? 会被安全替换。

Cursor 的使用

cursor.use {  // use 自动关闭 cursor
    // 移动到第一行
    if (it.moveToFirst()) {
        do {
            val name = it.getString(it.getColumnIndexOrThrow("name"))
            // 处理每一行
        } while (it.moveToNext())
    }
}

// 便捷方法
val count = cursor.count       // 总行数
val position = cursor.position // 当前行位置
Warning
  • Cursor 用完必须关闭!用 cursor.use { } 自动关闭,或手动 cursor.close()。不关闭会导致内存泄漏和数据库锁定。getColumnIndexOrThrow 在列名不存在时抛异常,比 getColumnIndex 返回 -1 更容易发现问题。

改(Update)

fun updateUser(db: SQLiteDatabase, id: Long, newName: String): Int {
    val values = ContentValues().apply {
        put("name", newName)
    }
    return db.update(
        "users",
        values,
        "_id = ?",
        arrayOf(id.toString())
    )
    // 返回受影响的行数
}

删(Delete)

fun deleteUser(db: SQLiteDatabase, id: Long): Int {
    return db.delete("users", "_id = ?", arrayOf(id.toString()))
    // 返回删除的行数
}

事务

多个操作要么全成功要么全回滚,用事务:

fun transferData(db: SQLiteDatabase) {
    db.beginTransaction()
    try {
        db.execSQL("UPDATE users SET age = age + 1 WHERE _id = 1")
        db.execSQL("UPDATE users SET age = age - 1 WHERE _id = 2")

        db.setTransactionSuccessful()  // 标记成功
    } finally {
        db.endTransaction()  // 结束事务(没标记成功则回滚)
    }
}
Note
  • setTransactionSuccessful() 必须在 endTransaction() 之前调用,否则事务回滚。放在 try 块最后一行,endTransaction()finally 里保证一定执行。事务的好处:一是原子性(全成功或全回滚),二是性能(批量操作在一个事务里比逐条执行快很多)。

数据库升级

版本号升高时做增量迁移:

override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
    // 从 1 升到 2:加一列
    if (oldVersion < 2) {
        db.execSQL("ALTER TABLE users ADD COLUMN phone TEXT")
    }
    // 从 2 升到 3:再加一列
    if (oldVersion < 3) {
        db.execSQL("ALTER TABLE users ADD COLUMN avatar TEXT")
    }
    // 用 if 判断而非 switch,因为用户可能跨版本升级(从 1 直接升到 3)
}
Tip
  • if (oldVersion < N) 而非 switch,因为用户可能跳过中间版本(从 1 直接升到 3)。每个 if 独立执行,保证所有迁移都跑一遍。

在协程中使用

数据库操作必须在 IO 线程:

class UserRepository(private val dbHelper: AppDatabase) {

    suspend fun addUser(name: String, age: Int): Long = withContext(Dispatchers.IO) {
        val db = dbHelper.writableDatabase
        val values = ContentValues().apply {
            put("name", name)
            put("age", age)
        }
        db.insert("users", null, values)
    }

    suspend fun getAllUsers(): List<User> = withContext(Dispatchers.IO) {
        val db = dbHelper.readableDatabase
        val cursor = db.query("users", null, null, null, null, null, "name ASC")
        val users = mutableListOf<User>()
        cursor.use {
            while (it.moveToNext()) {
                users.add(User(
                    it.getLong(it.getColumnIndexOrThrow("_id")),
                    it.getString(it.getColumnIndexOrThrow("name")),
                    it.getInt(it.getColumnIndexOrThrow("age"))
                ))
            }
        }
        users
    }
}
Warning
  • 数据库读写不要在主线程执行,会触发 StrictMode 异常或卡 UI。用 withContext(Dispatchers.IO) 切到 IO 线程。SQLiteOpenHelper 本身不是线程安全的,多线程同时写要靠事务保证安全。

SQLite 的局限

原生 SQLite API 有不少痛点:

  1. 样板代码多:每次查询都要写 ContentValues、Cursor 遍历、列名转换。
  2. 没有编译期检查:SQL 写错了运行时才报。
  3. 类型不安全:列名是字符串,拼错不报错。
  4. 手动管理 Cursor:忘了关就泄漏。

这些问题正是 Room 要解决的。Room 在 SQLite 之上提供了:

  • 用注解定义表和查询
  • 编译期检查 SQL
  • 自动生成样板代码
  • 协程和 Flow 支持

小结

SQLiteOpenHelper 管理数据库的创建和升级。CRUD 用 insert/query/update/delete 方法或 execSQL/rawQuery 写 SQL。条件值用 ? 占位符防注入。事务用 beginTransaction + setTransactionSuccessful + endTransaction 保证原子性。数据库操作放 IO 线程。理解了原生 SQLite,下一章学 Room 就水到渠成了。