7、Room数据库:从架构到实战,本地数据持久化

说实话,Room 是我在 Jetpack 组件里用得最频繁的一个。为什么?因为做 Android 开发,本地数据持久化几乎是躲不开的需求。SharedPreferences 存点简单配置还行,真要存个用户信息、商品列表、聊天记录,那玩意儿根本扛不住。SQLite 原生 API 又太啰嗦,写个 CRUD 能让你怀疑人生。

Room 就是 Google 给咱们准备的“SQLite 增强版”。它不是一个新数据库,而是对 SQLite 的封装。说白了,它让你用面向对象的方式操作数据库,不用写一堆 Cursor、ContentValues 那些反人类的代码。

7.1 Room 的整体架构

Room 的架构其实很清晰,就三层:

  • Entity:数据实体,对应数据库里的表
  • DAO:数据访问对象,定义增删改查的方法
  • Database:数据库本身,连接 Entity 和 DAO

这三层的关系,我画了张图,你看一眼就明白了:

Room 三层架构 RoomDatabase 数据库实例,持有 DAO 持有 DAO 数据访问对象,定义 CRUD 操作 Entity 数据实体,对应数据库表 核心特点 • 编译时 SQL 校验 • 自动生成实现代码 • 与 LiveData/Flow 无缝集成 • 支持 RxJava/协程 • 数据库版本迁移 • 外键约束支持 • 类型转换器 • 预填充数据库

你想想看,以前用 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,但加上这个注解更安全。
我的习惯:Entity 里尽量用基本类型包装类(Integer、Long 等),而不是基本类型(int、long)。因为数据库里可能存 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 会自动更新。我在项目中经常这么用,省去了手动刷新数据的麻烦。

注意:@Update 和 @Delete 是根据主键来匹配的。如果你只传了一个部分字段的对象,Room 会把其他字段更新为默认值。我曾经因为这个 bug 排查了半天,后来养成了习惯:更新前先查一遍完整数据。

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 的繁琐操作。

核心要点:Room 的 CRUD 操作,本质上就是操作 Java/Kotlin 对象。你只需要关注业务逻辑,数据库的细节 Room 帮你处理了。

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