A
第 23 章JAVA50 分钟

数据库:Room 完整实战

从 Entity/Dao/Database 三层架构到 @Embedded/@Relation 关系、Migration 迁移与 LiveData/RxJava 集成,构建可演进的本地数据库。

学习目标

  • 理解 Room 三层架构与编译期校验机制
  • 掌握 @Entity/@PrimaryKey/@ColumnInfo/@Embedded 注解
  • 能够编写 @Dao 的 CRUD 与流式查询
  • 学会 @Relation 一对多关系与多表联查
  • 掌握数据库 Migration、回退与索引优化
  • 理解 Room 与 LiveData/RxJava 集成方式

学习目标

  • 理解 Room 的三层架构与编译期校验机制。
  • 掌握 @Entity/@PrimaryKey/@ColumnInfo/@Embedded 等注解。
  • 能够编写 @Dao 的 CRUD 与流式查询(返回 LiveData/Flowable)。
  • 学会 @Relation 处理一对多关系,以及 @Transaction 多表联查。
  • 掌握数据库 Migration 与回退策略、索引优化。
  • 理解 Room 与 LiveData/RxJava 的集成方式。

Room 完整实战

1. 引入依赖

Room 在 Java 项目中通过 annotationProcessor 处理注解。若使用 Kotlin 项目可改 kapt,本系列以 Java 为主。

android {
    compileSdk 34
    defaultConfig {
        javaCompileOptions {
            annotationProcessorOptions {
                arguments += ["room.schemaLocation": "$projectDir/schemas".toString(),
                              "room.incremental"   : "true",
                              "room.expandProjection": "true"]
            }
        }
    }
}

dependencies {
    def room_version = "2.6.1"
    implementation "androidx.room:room-runtime:$room_version"
    // Java 项目用 annotationProcessor;Kotlin 项目改 kapt
    annotationProcessor "androidx.room:room-compiler:$room_version"
    // 可选:RxJava3 支持
    implementation "androidx.room:room-rxjava3:$room_version"
    // 可选:Guava 支持
    implementation "androidx.room:room-guava:$room_version"
    // LiveData 用于响应式查询
    implementation "androidx.lifecycle:lifecycle-livedata:2.8.4"
    implementation "androidx.lifecycle:lifecycle-viewmodel:2.8.4"
}

room.schemaLocation 会在编译期生成 JSON schema 文件,是 Migration 测试与 CI 校验的依据,应纳入版本控制。

2. 三层架构总览

┌─────────────────────────────────────┐
│  @Database (AppDatabase)            │  顶层入口,单例
├─────────────────────────────────────┤
│  @Dao (UserDao)                     │  数据访问层,CRUD 与流式查询
├─────────────────────────────────────┤
│  @Entity (User)                     │  表模型,对应 SQLite 行
└─────────────────────────────────────┘
  • Entity:与 SQLite 表一一对应,字段 ↔ 列。
  • Dao:数据访问接口,Room 编译期生成实现。
  • Database:RoomDatabase 的子类,单例持有连接池。

3. Entity 定义

package com.example.db.entity;

import androidx.room.ColumnInfo;
import androidx.room.Embedded;
import androidx.room.Entity;
import androidx.room.Index;
import androidx.room.PrimaryKey;

@Entity(
    tableName = "users",
    indices = {
        @Index(value = {"email"}, unique = true),
        @Index(value = {"phone"})
    }
)
public class User {

    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "_id")
    public long id;

    @ColumnInfo(name = "name")
    public String name;

    // 数据库列名可与字段名不同
    @ColumnInfo(name = "email")
    public String email;

    @ColumnInfo(name = "phone", defaultValue = "")
    public String phone;

    // 嵌入式:把 Address 字段平铺到 users 表
    @Embedded(prefix = "addr_")
    public Address address;

    @ColumnInfo(name = "created_at")
    public long createdAt;

    public User() {}
}

// 嵌入式值对象,本身不需要 @Entity
public class Address {
    public String city;
    public String street;
    public String zip;
}

要点:

  • @PrimaryKey(autoGenerate = true) 要求类型为 long/Long 或 int/Integer,自增列从 1 开始。
  • @Index 在常用查询字段上加索引可显著提升查询速度,unique = true 表示唯一约束。
  • @Embedded(prefix = "addr_") 把内嵌字段平铺为 addr_city、addr_street,避免和外层列名冲突。
  • 多个 @Embedded 使用 prefix 区分列名。

4. Dao 接口

package com.example.db.dao;

import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import androidx.room.Transaction;
import androidx.room.Update;

import com.example.db.entity.User;

import java.util.List;

import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Single;

@Dao
public interface UserDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    long insert(User user);

    // 批量插入返回各行 id
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    List<Long> insertAll(List<User> users);

    @Update
    int update(User user);

    @Delete
    int delete(User user);

    // 按条件删除;返回受影响行数
    @Query("DELETE FROM users WHERE created_at < :before")
    int deleteBefore(long before);

    @Query("SELECT * FROM users WHERE _id = :id")
    User findById(long id);

    // 同步返回 List:一次性查询
    @Query("SELECT * FROM users ORDER BY created_at DESC")
    List<User> listAll();

    // LiveData 返回:表变化自动重查
    @Query("SELECT * FROM users ORDER BY created_at DESC")
    LiveData<List<User>> observeAll();

    // RxJava Flowable:表变化自动推送
    @Query("SELECT * FROM users ORDER BY created_at DESC")
    Flowable<List<User>> flowableAll();

    // Single 适合一次性异步
    @Query("SELECT * FROM users WHERE email = :email LIMIT 1")
    Single<User> findByEmailAsync(String email);

    // LIKE 模糊查询,防 SQL 注入
    @Query("SELECT * FROM users WHERE name LIKE '%' || :keyword || '%' ORDER BY name")
    List<User> searchByName(String keyword);

    // 聚合查询
    @Query("SELECT COUNT(*) FROM users")
    int count();

    // 多表联查(一对多),通过 POJO + @Relation 自动装配
    @Transaction
    @Query("SELECT * FROM users WHERE _id = :userId")
    LiveData<UserWithOrders> observeUserWithOrders(long userId);
}

参数绑定支持::param 直接拼接、IN(:ids) 使用 List<Long> 等;LIKE 应使用占位符避免注入。

5. 关系:一对多

package com.example.db.entity;

import androidx.room.Entity;
import androidx.room.ForeignKey;
import androidx.room.Index;
import androidx.room.PrimaryKey;

// ForeignKey 定义外键,约束子表数据存在性
@Entity(
    tableName = "orders",
    foreignKeys = @ForeignKey(
        entity = User.class,
        parentColumns = "_id",
        childColumns = "user_id",
        onDelete = ForeignKey.CASCADE,
        onUpdate = ForeignKey.CASCADE
    ),
    indices = {@Index("user_id")}
)
public class Order {
    @PrimaryKey(autoGenerate = true)
    public long id;

    @ColumnInfo(name = "user_id")
    public long userId;

    @ColumnInfo(name = "amount")
    public double amount;

    @ColumnInfo(name = "created_at")
    public long createdAt;
}

onDelete = CASCADE 表示父行删除时级联删除子行;务必给外键列加 @Index,否则查询性能差,且部分 Room 版本会校验失败。

POJO 装配关联:

package com.example.db.relation;

import androidx.room.Embedded;
import androidx.room.Relation;

import com.example.db.entity.Order;
import com.example.db.entity.User;

import java.util.List;

public class UserWithOrders {

    @Embedded
    public User user;

    // parentColumn 指向 User._id,entityColumn 指向 Order.user_id
    @Relation(parentColumn = "id", entityColumn = "user_id")
    public List<Order> orders;
}

查询时只需一行:

@Transaction
@Query("SELECT * FROM users WHERE _id = :userId")
UserWithOrders loadUserWithOrders(long userId);

@Transaction 必不可少:Room 通过两次查询完成关联装配,需在事务内保证一致性。

6. Database 单例

package com.example.db;

import android.content.Context;

import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import androidx.room.migration.Migration;
import androidx.sqlite.db.SupportSQLiteDatabase;

import com.example.db.dao.UserDao;
import com.example.db.dao.OrderDao;
import com.example.db.entity.User;
import com.example.db.entity.Order;

@Database(
    entities = {User.class, Order.class},
    version = 2,
    exportSchema = true
)
public abstract class AppDatabase extends RoomDatabase {

    public abstract UserDao userDao();
    public abstract OrderDao orderDao();

    private static volatile AppDatabase sInstance;

    public static AppDatabase get(Context context) {
        if (sInstance == null) {
            synchronized (AppDatabase.class) {
                if (sInstance == null) {
                    sInstance = Room.databaseBuilder(
                                    context.getApplicationContext(),
                                    AppDatabase.class,
                                    "app.db")
                            // 显式声明 Migration 路径,未定义版本触发 fallbackToDelete
                            .addMigrations(MIGRATION_1_2)
                            .fallbackToDestructiveMigration()
                            .addCallback(new Callback() {
                                @Override
                                public void onCreate(SupportSQLiteDatabase db) {
                                    // 初始化数据,例如内置用户
                                }
                            })
                            .build();
                }
            }
        }
        return sInstance;
    }

    // Migration 1 -> 2:新增 phone 列
    static final Migration MIGRATION_1_2 = new Migration(1, 2) {
        @Override
        public void migrate(SupportSQLiteDatabase db) {
            db.execSQL("ALTER TABLE users ADD COLUMN phone TEXT NOT NULL DEFAULT ''");
            db.execSQL("CREATE INDEX index_users_phone ON users(phone)");
        }
    };
}

要点:

  • Database 必须是 abstract class 继承 RoomDatabase,仅声明 abstract 方法返回 Dao。
  • exportSchema = true 时 schema JSON 会输出到 room.schemaLocation 指定目录,应纳入版本控制。
  • 主线程默认禁止访问数据库;如必须用,可加 .allowMainThreadQueries(),但不推荐。
  • fallbackToDestructiveMigration 仅限开发期使用,生产环境会丢数据。

7. Migration 增量升级

每次 schema 变化(加表、加列、改类型)必须提升 version 并补一段 Migration:

static final Migration MIGRATION_2_3 = new Migration(2, 3) {
    @Override
    public void migrate(SupportSQLiteDatabase db) {
        // 创建新表
        db.execSQL("CREATE TABLE IF NOT EXISTS settings (" +
                "_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " +
                "key TEXT, value TEXT)");
        // 修改列类型:SQLite 不支持直接 ALTER COLUMN,需建临时表 + 复制 + 重命名
        db.execSQL("CREATE TABLE users_new (_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " +
                "name TEXT, email TEXT, phone TEXT, age INTEGER NOT NULL DEFAULT 0, " +
                "created_at INTEGER NOT NULL DEFAULT 0)");
        db.execSQL("INSERT INTO users_new(_id, name, email, phone, created_at) " +
                "SELECT _id, name, email, phone, created_at FROM users");
        db.execSQL("DROP TABLE users");
        db.execSQL("ALTER TABLE users_new RENAME TO users");
    }
};

Migration 校验:Room 在运行期比对实际 schema 与生成 schema,不一致会抛 IllegalStateException。建议写单测覆盖 Migration:

@RunWith(AndroidJUnit4.class)
public class MigrationTest {

    @Rule
    public MigrationTestHelper helper =
            new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
                    AppDatabase.class.getCanonicalName());

    @Test
    public void migrate1To2() throws IOException {
        SupportSQLiteDatabase db = helper.createDatabase("app.db", 1);
        db.execSQL("INSERT INTO users(_id, name, email) VALUES (1, 'Tom', 'a@b.com')");
        db.close();

        SupportSQLiteDatabase newDb = helper.runMigrationsAndValidate("app.db", 2,
                true, AppDatabase.MIGRATION_1_2);

        Cursor c = newDb.query("SELECT phone FROM users WHERE _id = 1");
        assertTrue(c.moveToFirst());
        assertEquals("", c.getString(0));
        c.close();
    }
}

8. 与 LiveData / RxJava 集成

// ViewModel 中暴露 LiveData
public class UserViewModel extends AndroidViewModel {

    private final AppDatabase db;

    public UserViewModel(@NonNull Application app) {
        super(app);
        db = AppDatabase.get(app);
    }

    public LiveData<List<User>> observeUsers() {
        return db.userDao().observeAll();
    }

    public void addUser(User user) {
        // 必须子线程执行
        Executors.newSingleThreadExecutor().execute(() -> db.userDao().insert(user));
    }
}

// Activity / Fragment 观察并刷新 UI
viewModel.observeUsers().observe(this, users -> {
    adapter.submitList(users);
});

RxJava 风格:

db.userDao().flowableAll()
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(users -> adapter.submitList(users));

Room 默认禁止主线程访问,但返回 LiveData / Flowable / Single 等响应式类型的方法不受此限制,因为它们自带异步语义。

9. 事务与批量操作

// Dao 内事务:被 @Transaction 标注的默认方法
@Transaction
default void replaceUsers(List<User> users) {
    deleteAll();
    insertAll(users);
}

@Query("DELETE FROM users")
void deleteAll();

// 外部事务:通过 runInTransaction
db.runInTransaction(() -> {
    db.userDao().deleteAll();
    db.userDao().insertAll(users);
});

10. TypeConverter 自定义类型

SQLite 仅支持基本类型,自定义类型(如 Date、List<String>)需通过 TypeConverter 转换:

public class Converters {

    @TypeConverter
    public static Long fromDate(Date date) {
        return date == null ? null : date.getTime();
    }

    @TypeConverter
    public static Date toDate(Long ts) {
        return ts == null ? null : new Date(ts);
    }

    @TypeConverter
    public static String fromStringList(List<String> list) {
        return list == null ? null : new Gson().toJson(list);
    }

    @TypeConverter
    public static List<String> toStringList(String json) {
        if (json == null) return null;
        return new Gson().fromJson(json, new TypeToken<List<String>>() {}.getType());
    }
}

@TypeConverters(Converters.class)
public abstract class AppDatabase extends RoomDatabase { ... }

常见坑与最佳实践

  1. 主线程访问数据库会抛 IllegalStateException:除非加 .allowMainThreadQueries(),但仅在测试或一次性脚本使用。
  2. @Relation 不支持 @Transaction 之外的多对多:复杂关系用 @Embedded + 自定义 POJO,或拆成多次查询。
  3. Entity 字段修饰符:可以是 public 或 private + getter/setter,但 private 必须有完整 getter/setter 否则编译报错。
  4. 外键列必须加 @Index:否则查询性能差;Room 也会校验警告。
  5. fallbackToDestructiveMigration 会清表:仅在开发期使用,生产环境必须写 Migration。
  6. Migration 中的 SQL 必须严格对齐 schema:列名、类型、NOT NULL、DEFAULT 等任何差异都会触发运行期 IllegalStateException,建议用 schema 测试。
  7. 不要把 List<Long> 直接当参数传给 IN(:ids):建议改 Long[] 或 List<Long>,注意 Java 泛型擦除问题。
  8. RoomDatabase 实例应全局单例:每个实例持有自己的连接池,多实例会引发锁竞争与内存浪费。
  9. 大量数据插入用 @Insert 批量并包事务:避免逐条插入触发多次磁盘 IO。

章节小结

  • Room 通过 @Entity/@Dao/@Database 三层把 SQLite 包装成声明式 ORM,编译期校验 SQL。
  • @Embedded/@Relation/@ForeignKey 用来处理嵌套与一对多关系;多表联查必须 @Transaction。
  • Migration 是版本演进的保障,配合 MigrationTestHelper 可保证 schema 一致性。
  • 与 LiveData、Flowable 集成后,UI 可自动响应数据变化。

下一章预告

网络图片加载是 Android 的另一大场景。下一章《图片加载:Glide》将讲解 Glide 的基本用法、缓存策略、变换、RecyclerView 场景以及生命周期绑定,并简要对比 Coil。