目录
[一、什么是 Room?](#一、什么是 Room?)
[3.1 创建实体类 (Entity)](#3.1 创建实体类 (Entity))
[3.2 创建 DAO 接口](#3.2 创建 DAO 接口)
[3.2.1 插入操作(@Insert)](#3.2.1 插入操作(@Insert))
[(1) 处理冲突策略 (OnConflictStrategy)](#(1) 处理冲突策略 (OnConflictStrategy))
[(2) 返回值类型](#(2) 返回值类型)
[3.2.2 更新操作 (@Update)](#3.2.2 更新操作 (@Update))
[3.2.3 删除操作 (@Delete)](#3.2.3 删除操作 (@Delete))
[3.2.4 万能查询 (@Query)](#3.2.4 万能查询 (@Query))
[(1) 基础查询](#(1) 基础查询)
[(2) 带参数查询 (使用冒号 :)](#(2) 带参数查询 (使用冒号 :))
[(3) 返回部分列 (Projection)](#(3) 返回部分列 (Projection))
[3.3 创建数据库类(RoomDatabase)](#3.3 创建数据库类(RoomDatabase))
[3.3.1 RoomDatabase](#3.3.1 RoomDatabase)
[3.3.2 @Database 注解详解](#3.3.2 @Database 注解详解)
[3.3.3 两种核心构建器 (Factory Methods)](#3.3.3 两种核心构建器 (Factory Methods))
[(1) Room.databaseBuilder (最常用)](#(1) Room.databaseBuilder (最常用))
[(2) Room.inMemoryDatabaseBuilder (内存数据库)](#(2) Room.inMemoryDatabaseBuilder (内存数据库))
[3.4 创建 Repository(推荐)](#3.4 创建 Repository(推荐))
[3.5 在 ViewModel 中使用](#3.5 在 ViewModel 中使用)
系列入口导航:Android Jetpack 概述
一、什么是 Room?
Room 是 Android Jetpack 组件库中的一个持久化库 ,它在 SQLite 之上提供了一层抽象,让数据库操作更加简单、安全。Room 在编译时会检查 SQL 语句的正确性,减少了运行时错误的风险。
Room 包含三个主要组件:
- Entity(实体):表示数据库中的表
- DAO(数据访问对象):定义数据库操作方法
- Database(数据库):数据库持有者,作为主要访问点
想了解关于SQLite 内容可以参考下面链接:
二、添加依赖
在 build.gradle (Module) 文件中添加以下依赖:
java
dependencies {
def room_version = "2.7.0" // 请根据当前实际版本调整
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
// 可选:对 RxJava 或 Guava 的支持
implementation "androidx.room:room-rxjava2:$room_version"
implementation "androidx.room:room-guava:$room_version"
}
三、完整代码示例
3.1 创建实体类 (Entity)
java
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.PrimaryKey;
@Entity(tableName = "users")
public class User {
@PrimaryKey(autoGenerate = true)
private int id;
@ColumnInfo(name = "user_name")
private String userName;
@ColumnInfo(name = "email")
private String email;
@ColumnInfo(name = "age")
private int age;
@ColumnInfo(name = "created_time")
private long createdTime;
// 使用 @Ignore 标记 Room 应该忽略的字段
@Ignore
private String tempData;
// 必须有一个无参构造方法
public User() {
}
// 有参构造方法(Room 会使用这个)
public User(String userName, String email, int age, long createdTime) {
this.userName = userName;
this.email = email;
this.age = age;
this.createdTime = createdTime;
}
// Getters 和 Setters
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public long getCreatedTime() {
return createdTime;
}
public void setCreatedTime(long createdTime) {
this.createdTime = createdTime;
}
public String getTempData() {
return tempData;
}
public void setTempData(String tempData) {
this.tempData = tempData;
}
}
上面我们创建了一个Entity 类,根据观察可以发现 Entity 类 就像是 Bean 类 + 注解的集合体。
为了更好的理解我们将 SQLite 对应的部分对比下:
java
// 创建数据库
@Override
public void onCreate(SQLiteDatabase db) {
// 创建 users 表(与 User 实体类对应)
String createTableSql = "CREATE TABLE users (" +
"id INTEGER PRIMARY KEY AUTOINCREMENT, " + // 对应 @PrimaryKey(autoGenerate = true)
"user_name TEXT, " + // 对应 @ColumnInfo(name = "user_name")
"email TEXT, " + // 对应 email 字段
"age INTEGER, " + // 对应 age 字段
"created_time INTEGER" + // 对应 created_time 字段(long 类型)
")";
db.execSQL(createTableSql);
}
| Room Entity 组件 | SQLite 对应部分 | 说明 |
|---|---|---|
@Entity(tableName = "users") |
CREATE TABLE users (...) |
创建名为 users 的表 |
@PrimaryKey |
PRIMARY KEY |
主键约束 |
@ColumnInfo |
列定义 | 列的属性信息 |
| 字段 (Field) | 列 (Column) | 每个字段对应一列 |
@Ignore |
- | 忽略该字段,不创建列 |
在 Room 出现之前,我们通常需要直接编写SQL语句来创建数据库表。这种方式虽然直观,但存在诸多问题。因为SQL语句是字符串,没有编译时检查,表名或字段名拼写错误只能在运行时发现。
那每列的类型是如何适配的呢?字段类型映射如下:
| Java 类型 | SQLite 类型 | 说明 |
|---|---|---|
int, long |
INTEGER |
整型 |
String |
TEXT |
文本类型 |
boolean |
INTEGER |
0 = false, 1 = true |
float, double |
REAL |
浮点数 |
byte[] |
BLOB |
二进制数据 |
Date |
INTEGER |
需要 TypeConverter |
3.2 创建 DAO 接口
如果说Entity定义了数据库的"骨骼",那么DAO 就是数据库的"灵魂"。DAO(Data Access Object)封装了所有数据库访问逻辑,是连接业务层和数据库层的桥梁。
DAO是一个接口或抽象类,通过注解来定义数据库操作。Room会在编译时自动生成实现类,你不需要写任何实现代码!
java
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
import java.util.List;
@Dao
public interface UserDao {
// 插入操作
@Insert
void insert(User user);
@Insert
void insertAll(User... users);
// 更新操作
@Update
void update(User user);
// 删除操作
@Delete
void delete(User user);
// 查询所有用户
@Query("SELECT * FROM users ORDER BY created_time DESC")
List<User> getAllUsers();
// 根据 ID 查询用户
@Query("SELECT * FROM users WHERE id = :userId")
User getUserById(int userId);
// 根据名称模糊查询
@Query("SELECT * FROM users WHERE user_name LIKE '%' || :name || '%'")
List<User> getUsersByName(String name);
// 查询年龄大于指定值的用户
@Query("SELECT * FROM users WHERE age > :minAge ORDER BY age DESC")
List<User> getUsersOlderThan(int minAge);
// 统计用户数量
@Query("SELECT COUNT(*) FROM users")
int getUserCount();
// 删除所有用户
@Query("DELETE FROM users")
void deleteAllUsers();
// 根据年龄范围查询
@Query("SELECT * FROM users WHERE age BETWEEN :minAge AND :maxAge")
List<User> getUsersByAgeRange(int minAge, int maxAge);
// 查询最新的 5 个用户
@Query("SELECT * FROM users ORDER BY created_time DESC LIMIT 5")
List<User> getLatestUsers();
}
3.2.1 插入操作(@Insert)
在 DAO(Data Access Object)接口中,你只需要在方法上标注 @Insert。Room 会自动为你生成底层 SQL 代码。
java
@Dao
public interface UserDao {
@Insert
long insertUser(User user);
@Insert
void insertUsers(List<User> users); // 支持集合
}
(1) 处理冲突策略 (OnConflictStrategy)
这是 @Insert 中最重要的参数。当你要插入的数据主键(Primary Key)与数据库中已有的记录冲突时,你可以决定如何处理。
java
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertOrUpdate(User user);
| 策略 | 行为 |
|---|---|
REPLACE |
替换旧数据。如果已存在,先删除旧行再插入新行(最常用)。 |
ABORT |
中止并回滚。这是默认值,如果冲突则报错并回滚事务。 |
IGNORE |
忽略冲突。保留旧数据,不插入新数据,也不报错。 |
(2) 返回值类型
@Insert 方法非常灵活,可以根据需要定义不同的返回值:
- void:不关心结果。
- long:返回插入行的 Row ID。如果插入的是集合,则返回 long[] 或 List<Long>。
- RxJava/LiveData/Flow:支持异步流返回插入结果。
3.2.2 更新操作 (@Update)
@Update根据 主键 (Primary Key) 来定位数据库中的行并更新非主键字段。
- 匹配机制:它会寻找Entity 对象中主键值对应的行。如果找不到,则不执行任何操作。
- 返回值:可以返回int,代表受影响的行数(成功更新了几条数据)。
java
@Update
public int updateUser(User user);
@Update
public void updateUsers(List<User> users);
3.2.3 删除操作 (@Delete)
@Delete同样通过主键 来匹配并删除记录。
- 返回值:可以返回 int,代表成功删除的行数。
- 注意:你必须传入一个带有正确主键的 Entity 对象,即使你只想删除这一行,也需要把对象传进去(或者至少是一个包含主键的对象)。
java
@Delete
public int deleteUser(User user);
@Delete
public int deleteUser(int id); // 不被允许的,错误的
3.2.4 万能查询 (@Query)
这是 Room 的核心。它允许你编写 原生 SQL 语句。查询操作在编译时就会进行语法检查,如果表名或列名写错了,编译器会直接报错。
(1) 基础查询
使用纯 SQL 语句。
java
@Query("SELECT * FROM user")
List<User> getAllUsers();
(2) 带参数查询 (使用冒号 :)
你可以直接在 SQL 中引用方法参数:
java
@Query("SELECT * FROM user WHERE age > :minAge")
List<User> loadUsersOlderThan(int minAge);
@Query("SELECT * FROM user WHERE name LIKE :search OR last_name LIKE :search")
List<User> findUserByName(String search);
(3) 返回部分列 (Projection)
如果你只需要某些字段,可以定义一个简单的 POJO 类:
java
public class NameTuple {
public String name;
public String lastName;
}
@Query("SELECT name, last_name FROM user")
List<NameTuple> loadNames();
| 操作 | 依赖项 | 常见返回值 | 特点 |
|---|---|---|---|
@Insert |
整个对象 | long (RowID) |
处理新数据进入。 |
@Update |
主键匹配 | int (影响行数) |
修改已有记录。 |
@Delete |
主键匹配 | int (删除行数) |
移除记录。 |
@Query |
原生 SQL | 任意对象/List | 最灵活,支持多表联查 (JOIN)。 |
3.3 创建数据库类(RoomDatabase)
java
// AppDatabase.java
import android.content.Context;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
@Database(entities = {User.class}, version = 1, exportSchema = false)
public abstract class AppDatabase extends RoomDatabase {
private static volatile AppDatabase instance;
public abstract UserDao userDao();
// 单例模式获取数据库实例
public static AppDatabase getInstance(Context context) {
if (instance == null) {
synchronized (AppDatabase.class) {
if (instance == null) {
instance = Room.databaseBuilder(
context.getApplicationContext(),
AppDatabase.class,
"app_database"
)
.allowMainThreadQueries() // 允许主线程操作(仅用于测试)
.build();
}
}
}
return instance;
}
}
采用了 单例模式 (Singleton) 来确保整个 App 运行期间只有一个数据库实例。
3.3.1 RoomDatabase
RoomDatabase 是 Room 持久化库的底层底座 ,它在架构中起到了"中央枢纽"的作用。
- 接入层:它持有了所有的 DAO(Data Access Objects)。
- 配置层:通过 @Database 注解定义表结构(Entities)和版本号。
- 管理层:负责数据库连接的创建、缓存以及底层的 SQLite 状态管理。
3.3.2 @Database 注解详解
参数配置:
- entities:声明数据库中包含的所有表。必须是标注了**@Entity 的类**。
- version:数据库版本。每当修改表结构(增减列、修改类型),此版本必须递增。
- exportSchema:如果设为 true,Room 会在编译时导出一个描述表结构的 JSON 文件。
3.3.3 两种核心构建器 (Factory Methods)
在 Room 框架中,Room.databaseBuilder 是最常用的方法,但并不是唯一的方法。
(1) Room.databaseBuilder (最常用)
这是代码中使用的,用于持久化存储。
- 行为:在设备的磁盘上(/data/data/包名/databases/)创建一个真正的数据库文件。
- 场景:正式的业务开发,数据需要永久保存,即使 App 进程杀掉或手机重启,数据依然存在。
java
instance = Room.databaseBuilder(
context.getApplicationContext(),
AppDatabase.class,
"db_name" // 数据库文件名
).build();
(2) Room.inMemoryDatabaseBuilder (内存数据库)
这是一个非常实用的方法,用于非持久化存储。
-
行为 :数据存储在内存中。一旦 App 进程被杀掉,所有数据都会消失。
-
场景:
-
单元测试 (Unit Testing):测试完即焚,不污染手机存储,速度极快。
-
临时缓存:只需要在 App 运行期间暂存,不希望占用磁盘空间的数据。
-
| 方法名称 | 存储位置 | 数据持久性 | 典型场景 |
|---|---|---|---|
databaseBuilder |
磁盘文件 | 永久保存 | 绝大多数正式业务 |
inMemoryDatabaseBuilder |
RAM 内存 | 进程结束即消失 | 单元测试、临时缓存 |
createFromAsset |
磁盘 (从 Asset 拷贝) | 永久保存 | 携带初始数据的 App |
3.4 创建 Repository(推荐)
这个UserRepository 类扮演的是 "中介中心" 的角色。
它是典型的 Repository 模式(仓库模式) 的实现。简单来说,它的存在是为了把 "数据从哪来" 和 "界面怎么显示" 这两件事彻底解耦。
java
// UserRepository.java
import android.os.AsyncTask;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class UserRepository {
private final UserDao userDao;
private final ExecutorService executorService;
public UserRepository(AppDatabase database) {
this.userDao = database.userDao();
this.executorService = Executors.newSingleThreadExecutor();
}
// 插入用户(异步)
public void insert(User user) {
executorService.execute(() -> userDao.insert(user));
}
// 更新用户(异步)
public void update(User user) {
executorService.execute(() -> userDao.update(user));
}
// 删除用户(异步)
public void delete(User user) {
executorService.execute(() -> userDao.delete(user));
}
// 获取所有用户(同步,用于 LiveData 或协程)
public LiveData<List<User>> getAllUsers() {
return userDao.getAllUsersLiveData();
}
}
初始化的时候 this.userDao = database.userDao(); 其中userDao是个抽象方法啊,在哪实现的?
你自己不需要实现它 ,Room 在编译代码时会通过"注解处理器(Annotation Processor)"帮你把实现类写好。这个在后面讲源码的时候会讲解的。
| 方法类型 | 是否使用 Executor | 行为特征 | 为什么这么设计? |
|---|---|---|---|
| 增/删/改 | 是 | 异步、无返回值 | 保护主线程,且通常不需要立刻看结果。 |
查 (Sync) |
否 | 同步、有返回值 | 保持灵活性,让外部(ViewModel/协程)来决定什么时候查。 |
Room 框架有一个硬性安全检查:如果你尝试在主线程(UI 线程)执行任何数据库查询,Room 会直接抛出 IllegalStateException 。虽然我在之前 build 的时候 使用了allowMainThreadQueries(),但是正式开发的时候还是要避免在主线程运行。
3.5 在 ViewModel 中使用
java
import android.app.Application;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import java.util.List;
public class UserViewModel extends AndroidViewModel {
private final UserRepository repository;
// 直接观察数据库的 LiveData
private final LiveData<List<User>> allUsers;
public UserViewModel(Application application) {
super(application);
AppDatabase database = AppDatabase.getInstance(application);
repository = new UserRepository(database);
// 关键点:直接从 repository 拿到"活"的数据流
// 这样只要数据库变了,allUsers 会自动更新,不需要手动 loadUsers()
allUsers = repository.getAllUsers();
}
// 供 Activity/Fragment 观察
public LiveData<List<User>> getUsers() {
return allUsers;
}
public void addUser(String name, String email, int age) {
User user = new User(name, email, age, System.currentTimeMillis());
repository.insert(user);
// 注意:这里不需要再手动调用 loadUsers()!
// 因为 LiveData 会感应到数据库变化并自动推送新数据。
}
public void updateUser(User user) {
repository.update(user);
// 同样,LiveData 会自动刷新
}
public void deleteUser(User user) {
repository.delete(user);
// 同样,LiveData 会自动刷新
}
}
四、数据迁移
当数据库版本升级时,需要处理迁移:
java
// 数据库版本从 1 升级到 2
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
// 添加新列
database.execSQL("ALTER TABLE users ADD COLUMN phone TEXT DEFAULT ''");
}
};
// 在构建数据库时添加迁移
instance = Room.databaseBuilder(context, AppDatabase.class, "app_database")
.addMigrations(MIGRATION_1_2)
.build();
//既然你增加了 phone 字段,记得同步更新你的 User 实体类!!!!!
-
Migration(1, 2):明确告诉 Room,这段代码负责把数据库从 版本 1 升级到 版本 2。 -
database.execSQL(...):这里写的是原生 SQL。Room 在迁移时不支持注解,必须手动操作数据库。 -
注意 :这里的 SQL 语法必须非常精确(例如
TEXT类型、DEFAULT值等),要与你在User实体类中定义的新字段属性完全一致。