数据库: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 { ... }
常见坑与最佳实践
- 主线程访问数据库会抛
IllegalStateException:除非加.allowMainThreadQueries(),但仅在测试或一次性脚本使用。 @Relation不支持@Transaction之外的多对多:复杂关系用@Embedded+ 自定义 POJO,或拆成多次查询。- Entity 字段修饰符:可以是
public或private + getter/setter,但private必须有完整 getter/setter 否则编译报错。 - 外键列必须加
@Index:否则查询性能差;Room 也会校验警告。 fallbackToDestructiveMigration会清表:仅在开发期使用,生产环境必须写 Migration。Migration中的 SQL 必须严格对齐 schema:列名、类型、NOT NULL、DEFAULT 等任何差异都会触发运行期IllegalStateException,建议用 schema 测试。- 不要把
List<Long>直接当参数传给IN(:ids):建议改Long[]或List<Long>,注意 Java 泛型擦除问题。 - RoomDatabase 实例应全局单例:每个实例持有自己的连接池,多实例会引发锁竞争与内存浪费。
- 大量数据插入用
@Insert批量并包事务:避免逐条插入触发多次磁盘 IO。
章节小结
- Room 通过
@Entity/@Dao/@Database三层把 SQLite 包装成声明式 ORM,编译期校验 SQL。 @Embedded/@Relation/@ForeignKey用来处理嵌套与一对多关系;多表联查必须@Transaction。Migration是版本演进的保障,配合MigrationTestHelper可保证 schema 一致性。- 与
LiveData、Flowable集成后,UI 可自动响应数据变化。
下一章预告
网络图片加载是 Android 的另一大场景。下一章《图片加载:Glide》将讲解 Glide 的基本用法、缓存策略、变换、RecyclerView 场景以及生命周期绑定,并简要对比 Coil。