11、数据存储之SQLite数据库:SQLiteOpenHelper的使用,增删改查操作,以及Room数据库框架入门
聊到Android本地存储,大家用得最多的可能是SharedPreferences或者文件。但一旦数据量上来,或者需要做复杂的查询、关联,那就得上数据库了。Android原生自带的是SQLite,轻量级、零配置,直接集成在系统里。今天我就带大家把SQLite这块彻底捋一遍,再聊聊Google官方推荐的Room框架——说白了,Room就是SQLite的“高级封装”,用起来更爽。
本章核心知识点
- SQLiteOpenHelper的使用与生命周期
- 增删改查(CRUD)的标准写法
- Room框架的快速入门
- 从SQLite迁移到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;
}
}
使用对比:
| 对比项 | 原生SQLite | Room |
|---|---|---|
| 代码量 | 多(需手动写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的教训还热乎着呢。