7、Room数据库:从架构到实战,本地数据持久化
说实话,Room 是我在 Jetpack 组件里用得最频繁的一个。为什么?因为做 Android 开发,本地数据持久化几乎是躲不开的需求。SharedPreferences 存点简单配置还行,真要存个用户信息、商品列表、聊天记录,那玩意儿根本扛不住。SQLite 原生 API 又太啰嗦,写个 CRUD 能让你怀疑人生。
Room 就是 Google 给咱们准备的“SQLite 增强版”。它不是一个新数据库,而是对 SQLite 的封装。说白了,它让你用面向对象的方式操作数据库,不用写一堆 Cursor、ContentValues 那些反人类的代码。
7.1 Room 的整体架构
Room 的架构其实很清晰,就三层:
- Entity:数据实体,对应数据库里的表
- DAO:数据访问对象,定义增删改查的方法
- Database:数据库本身,连接 Entity 和 DAO
这三层的关系,我画了张图,你看一眼就明白了:
你想想看,以前用 SQLiteOpenHelper 的时候,你得自己管理数据库版本、写 SQL 语句、解析 Cursor。Room 把这些脏活累活全包了。我刚开始用 Room 的时候,最直观的感受就是:代码量至少减少了一半。
7.2 Entity 定义:把数据类变成数据库表
Entity 说白了就是一个普通的数据类,加上 @Entity 注解。每个 Entity 对应一张表,每个字段对应一列。
来看一个实际例子。假设我们要存用户信息:
@Entity(tableName = "users")
public class User {
@PrimaryKey(autoGenerate = true)
private long id;
@NonNull
@ColumnInfo(name = "user_name")
private String name;
@ColumnInfo(name = "age")
private int age;
@Ignore
private String tempField; // 这个字段不会被持久化
// 构造函数、getter/setter 省略
}
这里有几个关键点:
- @PrimaryKey:每个 Entity 必须有一个主键。我习惯用
autoGenerate = true让 Room 自动生成 ID。 - @ColumnInfo:可以自定义列名。如果不写,默认用字段名。
- @Ignore:标记不需要持久化的字段。比如临时计算用的变量。
- @NonNull:标记字段不能为 null。Room 默认允许 null,但加上这个注解更安全。
7.3 DAO 编写:CRUD 操作的核心
DAO 是接口或者抽象类,用 @Dao 注解标记。Room 会在编译时自动生成实现代码。
最基本的 DAO 长这样:
@Dao
public interface UserDao {
@Insert
void insert(User user);
@Update
void update(User user);
@Delete
void delete(User user);
@Query("SELECT * FROM users WHERE id = :userId")
User getUserById(long userId);
@Query("SELECT * FROM users ORDER BY id DESC")
List<User> getAllUsers();
}
你看,增删改查就这么简单。@Insert、@Update、@Delete 这些注解,Room 会自动生成对应的 SQL。查询用 @Query 写自定义 SQL。
不过,实际项目中我很少用同步方法。为什么?因为数据库操作不能在主线程执行,否则会 ANR。Room 默认也不允许在主线程调用 DAO 方法(除非你调用 allowMainThreadQueries(),但我不建议这么做)。
所以,更常见的写法是用 LiveData 或者 Kotlin 协程:
@Dao
public interface UserDao {
@Insert
void insert(User user);
@Query("SELECT * FROM users ORDER BY id DESC")
LiveData<List<User>> getAllUsersLiveData();
@Query("SELECT * FROM users WHERE id = :userId")
Flowable<User> getUserByIdFlowable(long userId);
}
用 LiveData 的好处是,数据变化时 UI 会自动更新。我在项目中经常这么用,省去了手动刷新数据的麻烦。
7.4 Database 创建:连接 Entity 和 DAO
Database 是一个抽象类,继承 RoomDatabase。它负责创建数据库实例,并提供 DAO 的访问方法。
@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,
"my_database.db"
).build();
}
}
}
return INSTANCE;
}
}
这里有几个要点:
- @Database:声明 Entity 列表和数据库版本。version 每次修改表结构都要加 1。
- exportSchema:设为 true 会导出数据库 schema 到指定目录,方便版本迁移。我一般开发时开,发布时关。
- 单例模式:数据库实例应该全局唯一,避免重复创建开销。
嗯,这里要注意:数据库版本升级是个大坑。如果你只是加个字段,用 fallbackToDestructiveMigration() 可以简单粗暴地重建数据库。但生产环境绝对不能这么干,用户数据会丢光的。正确的做法是用 Migration 对象写迁移逻辑。
7.5 CRUD 操作实战
理论说完了,咱们来点实际的。假设我们要做一个简单的笔记应用,存用户的笔记数据。
先定义 Entity:
@Entity(tableName = "notes")
public class Note {
@PrimaryKey(autoGenerate = true)
private long id;
@NonNull
private String title;
private String content;
private long createdAt;
// getter/setter 省略
}
再写 DAO:
@Dao
public interface NoteDao {
@Insert
long insert(Note note);
@Update
int update(Note note);
@Delete
int delete(Note note);
@Query("SELECT * FROM notes ORDER BY createdAt DESC")
LiveData<List<Note>> getAllNotes();
@Query("SELECT * FROM notes WHERE id = :noteId")
Note getNoteById(long noteId);
}
最后在 Activity 或 ViewModel 里调用:
// 插入笔记
Note note = new Note();
note.setTitle("我的第一条笔记");
note.setContent("这是笔记内容");
note.setCreatedAt(System.currentTimeMillis());
new Thread(() -> {
long id = AppDatabase.getInstance(context).noteDao().insert(note);
Log.d("RoomDemo", "插入成功,ID: " + id);
}).start();
// 查询所有笔记(自动更新)
AppDatabase.getInstance(context).noteDao().getAllNotes()
.observe(this, notes -> {
// UI 自动刷新
adapter.setNotes(notes);
});
你看,整个流程下来,代码非常简洁。没有 Cursor,没有 ContentValues,没有 SQLiteDatabase 的繁琐操作。
7.6 实战:本地数据持久化完整示例
最后,我分享一个我在项目中用过的完整方案。假设我们要缓存用户信息,包括基本资料和登录状态。
Entity 设计:
@Entity(tableName = "user_cache")
public class UserCache {
@PrimaryKey
private long userId;
@NonNull
private String nickname;
private String avatarUrl;
private int gender; // 0:未知 1:男 2:女
private long loginTime;
private boolean isVip;
// getter/setter 省略
}
DAO 设计:
@Dao
public interface UserCacheDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
void saveUser(UserCache user);
@Query("SELECT * FROM user_cache WHERE userId = :userId")
LiveData<UserCache> getUser(long userId);
@Query("DELETE FROM user_cache WHERE userId = :userId")
void deleteUser(long userId);
@Query("DELETE FROM user_cache")
void clearAll();
}
使用场景:
- 用户登录成功后,调用
saveUser()缓存数据。 - 打开个人中心时,通过
getUser()获取 LiveData,UI 自动展示。 - 用户退出登录时,调用
deleteUser()清除缓存。
这样做的好处是,即使用户断网,也能看到上次登录的信息。用户体验会好很多。
onConflict = OnConflictStrategy.REPLACE 时,发现主键冲突后数据被覆盖了。后来才意识到,如果主键相同,REPLACE 策略会先删后插。如果你只想更新部分字段,得用 @Update 或者写自定义 @Query。
好了,Room 的核心内容就这些。从 Entity 定义到 DAO 编写,再到 Database 创建和 CRUD 操作,每一步都不复杂。关键是理解它的设计思想:用面向对象的方式操作数据库。你只要把 Entity 当成普通的数据类,把 DAO 当成数据仓库,剩下的交给 Room 就行。
公众号:蓝海资料掘金营,微信deep3321