11、数据存储之SQLite数据库:SQLiteOpenHelper的使用,增删改查操作,以及Room数据库框架入门

聊到Android本地存储,大家用得最多的可能是SharedPreferences或者文件。但一旦数据量上来,或者需要做复杂的查询、关联,那就得上数据库了。Android原生自带的是SQLite,轻量级、零配置,直接集成在系统里。今天我就带大家把SQLite这块彻底捋一遍,再聊聊Google官方推荐的Room框架——说白了,Room就是SQLite的“高级封装”,用起来更爽。

本章核心知识点

  • SQLiteOpenHelper的使用与生命周期
  • 增删改查(CRUD)的标准写法
  • Room框架的快速入门
  • 从SQLite迁移到Room的避坑指南
Android 数据存储:SQLite → Room 知识体系 SQLite 原生方案 • SQLiteOpenHelper 抽象类 • onCreate / onUpgrade 回调 • getWritableDatabase() • 原生 SQL 语句执行 • Cursor 手动遍历结果 • 手动管理数据库版本 • 易出错:SQL注入、内存泄漏 升级 Room 框架 • @Entity 注解实体类 • @Dao 注解数据访问对象 • @Database 注解数据库类 • 编译时校验 SQL 正确性 • 自动处理线程切换 • 与 LiveData / Flow 集成 • 支持迁移(Migration) 核心建议:新项目直接用 Room,老项目迁移也不难

11.1 SQLiteOpenHelper——数据库的“管家”

在Android里操作SQLite,官方推荐的方式是继承SQLiteOpenHelper这个抽象类。你想想看,如果每次打开数据库都要自己检查表存不存在、版本对不对,那得多麻烦?这个Helper就是帮我们搞定这些脏活累活的。

我个人习惯把Helper定义成一个单例,避免多次创建导致资源浪费。来看一个典型的实现:

public class MyDatabaseHelper extends SQLiteOpenHelper {

    private static final String DATABASE_NAME = "myapp.db";
    private static final int DATABASE_VERSION = 1;

    private static MyDatabaseHelper instance;

    // 单例模式,避免重复创建
    public static synchronized MyDatabaseHelper getInstance(Context context) {
        if (instance == null) {
            instance = new MyDatabaseHelper(context.getApplicationContext());
        }
        return instance;
    }

    private MyDatabaseHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        // 第一次创建数据库时调用
        String sql = "CREATE TABLE IF NOT EXISTS user (" +
                "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
                "name TEXT NOT NULL, " +
                "age INTEGER, " +
                "phone TEXT)";
        db.execSQL(sql);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // 数据库版本升级时调用
        // 实际项目中应该用 ALTER TABLE 或迁移策略,而不是直接删表
        db.execSQL("DROP TABLE IF EXISTS user");
        onCreate(db);
    }
}

💡 小技巧:onUpgrade里直接DROP表再重建,只适合开发阶段。线上版本一定要写迁移逻辑,否则用户数据就丢了。我曾经在一个小项目里偷懒这么干,结果被测试骂惨了……

11.2 增删改查——CRUD操作实战

有了Helper,接下来就是最核心的增删改查了。我习惯把数据库操作封装在一个单独的Repository或Dao类里,这样业务层和数据库层就解耦了。

11.2.1 插入数据

public long insertUser(User user) {
    SQLiteDatabase db = myDatabaseHelper.getWritableDatabase();
    ContentValues values = new ContentValues();
    values.put("name", user.getName());
    values.put("age", user.getAge());
    values.put("phone", user.getPhone());
    // 返回新插入行的ID,失败返回-1
    long id = db.insert("user", null, values);
    db.close();
    return id;
}

这里要注意,ContentValues的key必须和表字段名完全一致,大小写敏感。我见过有人把"name"写成"Name",结果插进去全是null……

11.2.2 查询数据

public List<User> queryUsersByName(String name) {
    List<User> userList = new ArrayList<>();
    SQLiteDatabase db = myDatabaseHelper.getReadableDatabase();
    // 使用 ? 占位符防止SQL注入
    Cursor cursor = db.query("user", null, "name LIKE ?",
            new String[]{"%" + name + "%"}, null, null, "age DESC");
    if (cursor != null) {
        while (cursor.moveToNext()) {
            User user = new User();
            user.setId(cursor.getLong(cursor.getColumnIndex("id")));
            user.setName(cursor.getString(cursor.getColumnIndex("name")));
            user.setAge(cursor.getInt(cursor.getColumnIndex("age")));
            user.setPhone(cursor.getString(cursor.getColumnIndex("phone")));
            userList.add(user);
        }
        cursor.close();
    }
    db.close();
    return userList;
}

⚠️ 避坑指南:Cursor用完后一定要close(),数据库也要close()。我曾经在一个循环里忘了关Cursor,结果内存直接飙到OOM。另外,getColumnIndex返回-1时如果不处理,会抛IllegalArgumentException。

11.2.3 更新与删除

// 更新:根据id修改用户信息
public int updateUser(User user) {
    SQLiteDatabase db = myDatabaseHelper.getWritableDatabase();
    ContentValues values = new ContentValues();
    values.put("name", user.getName());
    values.put("age", user.getAge());
    values.put("phone", user.getPhone());
    int rows = db.update("user", values, "id = ?", new String[]{String.valueOf(user.getId())});
    db.close();
    return rows; // 返回受影响的行数
}

// 删除:根据id删除用户
public int deleteUser(long userId) {
    SQLiteDatabase db = myDatabaseHelper.getWritableDatabase();
    int rows = db.delete("user", "id = ?", new String[]{String.valueOf(userId)});
    db.close();
    return rows;
}

嗯,这里要特别强调:永远不要拼接SQL字符串。用?占位符传参,这是防SQL注入的基本素养。你想想看,如果用户输入一个'; DROP TABLE user; --,直接拼接的话你的表就没了。

11.3 Room数据库框架——Google官方推荐

原生SQLite写多了你会发现,到处都是模板代码:定义SQL、解析Cursor、手动close……Room就是来解决这个痛点的。它本质上是对SQLite的ORM封装,但比JPA轻量得多。

我个人觉得Room最大的好处是:编译时校验SQL语法。写错表名或字段名,编译直接报错,不用等到运行时崩溃。这一点在团队协作时特别有用。

11.3.1 三步集成Room

第一步:定义实体类

@Entity(tableName = "user")
public class User {
    @PrimaryKey(autoGenerate = true)
    private long id;
    @ColumnInfo(name = "name")
    private String name;
    private int age;
    private String phone;

    // getter/setter 必须写,Room通过反射调用
    // 省略...
}

第二步:定义Dao接口

@Dao
public interface UserDao {
    @Insert
    long insert(User user);

    @Query("SELECT * FROM user WHERE name LIKE '%' || :name || '%' ORDER BY age DESC")
    List<User> queryByName(String name);

    @Update
    int update(User user);

    @Delete
    int delete(User user);
}

第三步:定义Database类

@Database(entities = {User.class}, version = 1, exportSchema = false)
public abstract class AppDatabase extends RoomDatabase {
    public abstract UserDao userDao();

    private static volatile AppDatabase INSTANCE;

    public static AppDatabase getInstance(Context context) {
        if (INSTANCE == null) {
            synchronized (AppDatabase.class) {
                if (INSTANCE == null) {
                    INSTANCE = Room.databaseBuilder(
                            context.getApplicationContext(),
                            AppDatabase.class,
                            "myapp_room.db"
                    ).build();
                }
            }
        }
        return INSTANCE;
    }
}

使用对比:

对比项原生SQLiteRoom
代码量多(需手动写SQL、解析Cursor)少(注解驱动)
SQL校验运行时编译时
线程安全需自己处理默认不允许主线程操作(可配置)
与LiveData集成需手动实现原生支持,返回LiveData自动监听变化
数据库迁移手动写onUpgrade提供Migration类,更安全

11.3.2 Room的迁移策略

数据库升级是绕不开的坑。Room提供了Migration类,可以精确控制每个版本的变更:

static final Migration MIGRATION_1_2 = new Migration(1, 2) {
    @Override
    public void migrate(@NonNull SupportSQLiteDatabase database) {
        // 从版本1升级到版本2:新增email字段
        database.execSQL("ALTER TABLE user ADD COLUMN email TEXT");
    }
};

// 构建时传入
Room.databaseBuilder(...)
    .addMigrations(MIGRATION_1_2)
    .build();

如果不提供Migration,Room会直接崩溃并抛出IllegalStateException。这其实是个好事——强迫开发者认真对待数据迁移,而不是偷偷丢数据。

💡 我的建议:新项目直接上Room,别犹豫。老项目如果SQLite代码已经写了很多,可以逐步迁移,Room和原生SQLite可以共存。我去年重构一个项目时就是这么干的——先让Room接管新功能,老功能慢慢改,稳得很。

11.4 本章小结

SQLite是Android本地存储的基石,SQLiteOpenHelper帮我们管理数据库生命周期。但说实话,原生写法太啰嗦了,Room才是现代Android开发的正确姿势。它用注解消灭了模板代码,用编译时校验避免了运行时崩溃,还天然支持LiveData和协程。

嗯,最后提醒一句:不管用哪种方式,数据库操作一定要放在子线程。Room默认禁止主线程操作,但原生SQLite可不会拦你——你自己要记得开线程或使用AsyncTask。别问我怎么知道的,线上ANR的教训还热乎着呢。