前言

Android Jetpack 的出现统一了 Android 开发生态,各种三方库逐渐被官方组件所取代。 Room 也同样如此,逐渐取代竞品成为最主流的数据库 ORM 框架。 这当然不仅仅因为其官方身份,更是因为其良好的开发体验,大大降低了 SQLite 的使用门槛。
相对于SQLiteOpenHelper等传统方法,使用Room操作SQLite有以下优势:
1.编译期的SQL语法检查
2.开发高效,避免大量模板代码
3.API设计友好,容易理解
4.可与 LiveData 关联,具备 LiveData Lifecycle 的所有魅力
Room 的使用,主要涉及三个类: Entity、DataBase、DAO
- Database: 访问底层数据库的入口;
- Entity: 代表数据库中的表(table),一般用注解;
- Data Access Object (DAO): 数据库访问者;
这三个组件的概念也出现在其他 ORM 框架中,有过使用经验的同学理解起来并不困难:通过 Database 获取 DAO,然后通过 DAO 查询并获取 entities,最终通过 entities 对数据库 table 中数据进行读写;

用法篇
使用方式我们介绍下基础使用和结合 MVVM 的使用
基础使用
导入依赖
kotlin
// 下面是 ROOM依赖相关的代码
def room_version = "xxx"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version" // use kapt for Kotlin
// optional - Guava support for Room, including Optional and ListenableFuture
implementation "androidx.room:room-guava:$room_version"
我们先来了解下它的简单使用,遵循 Entity、DAO、DataBase
我们先来声明 Entity
typescript
@Entity
public class Student {
// 主键 SQL 唯一的 autoGenerate 自增长
@PrimaryKey(autoGenerate = true)
private int uid;
@ColumnInfo(name = "name")
private String name;
@ColumnInfo(name = "pwd")
private String password;
@ColumnInfo(name = "address")
private int address;
public Student(String name, String password, int address) {
this.name = name;
this.password = password;
this.address = address;
}
public int getUid() {
return uid;
}
public void setUid(int uid) {
this.uid = uid;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getAddress() {
return address;
}
public void setAddress(int address) {
this.address = address;
}
@Override
public String toString() {
return "Student{" +
"uid=" + uid +
", name='" + name + '\'' +
", password='" + password + '\'' +
", address='" + address + '\'' +
'}';
}
}
这样 Entity 我们就声明好了;@ColumnInfo 注解可有可无,没有就会使用我们声明的属性来创建列名,声明了就用我们声明的来创建列名;
接下来,我们来声明 Dao,用来做增、删、改、查操作;
less
@Dao
public interface StudentDao {
@Insert
void insert(Student... students);
@Delete
void delete(Student student);
@Update
void update(Student student);
@Query("select * from Student")
List<Student> getAll();
// 查询一条记录
@Query("select * from Student where name like:name")
Student findByName(String name);
// 数组查询 多个记录
@Query("select * from Student where uid in(:userIds)")
List<Student> getAllId(int[] userIds);
// 就是只查询 name pwd 给 StudentTuple 类接收
@Query("select name,pwd from Student")
StudentTuple getRecord();
}
PS:Dao 的声明必须要用
interface
来声明;
如果我们只想查询数据库表中的 某x个字段,那么我们需要声明一个类来接收相关字段,切这个类中声明的字段要和 Entity 中保持一致;
typescript
public class StudentTuple {
@ColumnInfo(name = "name")
public String name;
@ColumnInfo(name="pwd")
public String password;
public StudentTuple(String name, String password) {
this.name = name;
this.password = password;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "StudentTuple{" +
"name='" + name + '\'' +
", password='" + password + '\'' +
'}';
}
}
这个类 @Entity 不能加,加了就是一张表了;
接下来,我们来声明 Database
scala
@Database(entities = { Student.class }, version = 1, exportSchema = false)
public abstract class AppDataBase extends RoomDatabase {
// 暴露dao
public abstract StudentDao userDao();
}
必须要用抽象类
必须要继承 RoomDatabase
要写 exportSchema = false 导出模式,防止后续升级的异常
通过 APT 技术帮我们生成具体的实现类,所以 Dao 和 Database 都需要抽象的类;
接下来,我们在 Activity 中调用;
scala
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 数据库的操作应该是在子线程
DbTest t = new DbTest();
t.start();
}
public class DbTest extends Thread {
@Override
public void run() {
// 核心逻辑1 数据库的操作都在这里
AppDataBase marsDB = Room.databaseBuilder(getApplicationContext(),
AppDataBase.class,
"MarsDB")
// 可以设置强制主线程,默认是让你用子线程
// .allowMainThreadQueries()
.build();
// 执行增、删、改、查操作
StudentDao dao = marsDB.userDao();
// 增
dao.insert(new Student("Mars0", "123", 1));
dao.insert(new Student("Mars1", "456", 2));
dao.insert(new Student("Mars2", "789", 3));
dao.insert(new Student("Mars3", "111", 4));
// 查询全部数据
List<Student> all = dao.getAll();
Log.d("Mars", "run: all.toString(): " + all.toString());
Log.i("Mars", "--------------------------");
// 查询名字为 Mars3 的一条数据
Student mars3 = dao.findByName("Mars3");
Log.d("Mars", "run: mars.toString(): " + mars.toString());
Log.i("Mars", "--------------------------");
// 查询 2 3 4 uid的三条数据
List<Student> allID = dao.getAllId(new int[]{2, 3, 4});
Log.d("Mars", "run: allID.toString(): " + allID.toString());
Log.i("Mars", "--------------------------");
// 查询student表里面的数据 到 StudentTuple里面去
StudentTuple record = dao.getRecord();
Log.d("Derry", "run: record.toString(): " + record.toString());
// 更新
dao.update(new Student("Mars3", "112", 4))
// 删除
dao.delete(new Student("Mars3", "112", 4))
}
}
}
增、删、改、查的基础使用就是这些,相对我们直接操作 SQLite 要方便很多;
Jetpack Room 结合 MVVM 使用

我们先来看下 MVVM 的标准架构图,可以看到 Room 通过 Repository 连接到 Model 层中的 Room,我们就按照这个架构图来看下如何结合 MVVM 使用;
老三件,Entity、Dao、Database,我们可以直接复用前面基础使用中创建的;Database 我们稍微改造下,不再 Activity 中进行数据库的创建了,我们可以改成单例的模式,以及可以在主线程中创建数据库;
scala
@Database(entities = {Student.class}, version = 2, exportSchema = false)
public abstract class AppDatabase extends RoomDatabase {
/** 单例属性 */
private static AppDatabase instance;
// 对外暴露唯一实例
public static synchronized AppDatabase getInstance(Context context) {
if (instance == null) {
instance = Room.databaseBuilder(context.getApplicationContext(),
AppDatabase.class
, "MarsDB")
// 可以强制在主线程运行数据库操作
.allowMainThreadQueries()
.build();
}
return instance;
}
public abstract StudentDao studentDao();
}
支持 LiveData
Dao 中也需要改造下,支持 LiveData,主要是查询操作;
less
@Dao
public interface StudentDao {
@Insert
void insert(Student... students);
@Delete
void delete(Student student);
@Update
void update(Student student);
@Query("select * from Student order by uid")
LiveData<List<Student>> getAll();
// 查询一条记录
@Query("select * from Student where name like:name")
LiveData<Student> findByName(String name);
// 数组查询 多个记录
@Query("select * from Student where uid in(:userIds)")
LiveData<List<Student>> getAllId(int[] userIds);
// 就是只查询 name pwd 给 StudentTuple 类接收
@Query("select name,pwd from Student")
LiveData<StudentTuple> getRecord();
}
针对查询操作,只需要将返回值改成 LiveData<> 进行包裹,就可以完美支持 LiveData 了;
支持 Repository
我们可以简单的来支持下 Repository 层,商业环境中,它应该是一个独立的模块,支持各个业务的 DAO 和 Net 的操作,内部根据业务判断 dao 和 net 的逻辑;本期我们为了体验 MVVM 的结合,设计的比较简单
scss
public class StudentRepository {
private StudentDao studentDao; // 用户操作 只面向DAO
public StudentRepository(Context context) {
AppDatabase database = AppDatabase.getInstance(context);
studentDao = database.studentDao();
}
// 下面代码是:提供一些API给ViewModel使用
// 增
void insert(Student... students) {
studentDao.insert(students);
}
// 删
void delete(Student student) {
studentDao.delete(student);
}
// 改
void update(Student student) {
studentDao.update(student);
}
// 查 关联 LiveData 暴露环节
LiveData<List<Student>> getAllLiveDataStudent() {
return studentDao.getAll();
}
}
结合 ViewModel
接下来我们来声明 ViewModel,由架构图可以看到,ViewModel 操作的是 Repository;
scss
public class StudentViewModel extends AndroidViewModel {
private StudentRepository studentRepository; // 定义仓库 ViewModel 只操作仓库
private LiveData<List<Student>> liveDataAllStudent; // 触发改变的 LiveData 的数据
public StudentViewModel(Application application) {
super(application);
studentRepository = new StudentRepository(application);
if (liveDataAllStudent == null) {
liveDataAllStudent = studentRepository.getAllLiveDataStudent(); // 使用 LiveData 关联 Room
}
}
// 增
void insert(Student... students) {
studentRepository.insert(students);
}
// 删
void delete(Student student) {
studentRepository.delete(student);
}
// 改
void update(Student student) {
studentRepository.update(student);
}
// 查 关联 LiveData
LiveData<List<Student>> getAllLiveDataStudent() {
if (liveDataAllStudent == null) {
liveDataAllStudent = studentRepository.getAllLiveDataStudent(); // 使用 LiveData 关联 Room
}
return liveDataAllStudent;
}
}
接下来,我们声明 Activity 来创建 ViewModel
java
public class MainActivity extends AppCompatActivity {
private RecyclerView recyclerView;
private StudentViewModel studentViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 创建 RV
recyclerView = findViewById(R.id.recyclerView);
// 创建 ViewModel
studentViewModel = new ViewModelProvider(getViewModelStore(), new ViewModelProvider.AndroidViewModelFactory(getApplication())).get(StudentViewModel.class);
// 观察者,如果使用了 DataBinding 以及 BindingAdapter 之后,这个 observe 的逻辑就可以删掉了
studentViewModel.getAllLiveDataStudent().observe(this, new Observer<List<Student>>() {
@Override
public void onChanged(List<Student> students) {
// 更新UI
recyclerView.setAdapter(new GoodsAdapter(MainActivity.this, students));
}
});
// 模拟 仓库
new Thread() {
@Override
public void run() {
super.run();
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 默认给数据库 ROOM 增加数据
for (int i = 0; i < 50; i++) {
studentViewModel.insert(new Student("Mars", "123", 1));
}
}
}.start();
// 模拟仓库 数据库数据被修改了,一旦数据库被修改,那么数据会驱动 UI 发生改变
new Thread() {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
// 每隔一秒、更新一次,内部会 setValue,就会回调到 onChange 方法;
studentViewModel.update(new Student(6, "Mars" + i, "123", 1));
}
}
}.start();
}
}
运行之后,可以看到的效果就是,界面每隔 1s 更新一下;
源码篇
本质上是通过 APT 技术,帮我们生成了 Dao 和 Database 的具体实现;

为什么,使用了 LiveData 之后,就能自动的更新 UI 了,我们进入源码看下:

当我们执行编译之后,可以看到 Dao 有了实现,我们点击这个可以进入到实现层;
java
public LiveData<List<Student>> getAllLiveDataStudent() {
final String _sql = "select * from Student order by uid";
final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
return new ComputableLiveData<List<Student>>(__db.getQueryExecutor()) {
private Observer _observer;
@Override
protected List<Student> compute() {
if (_observer == null) {
_observer = new Observer("Student") {
@Override
public void onInvalidated(@NonNull Set<String> tables) {
// 核心逻辑就在这里 Oberver 监听,Room 会把改变的数据发送过来,收到变化之后会进行刷新实现
invalidate();
}
};
__db.getInvalidationTracker().addWeakObserver(_observer);
}
final Cursor _cursor = __db.query(_statement);
try {
final List<Student> _result = new ArrayList<Student>(_cursor.getCount());
while(_cursor.moveToNext()) {
final Student _item;
_item = __entityCursorConverter_comExampleRoomdemo02Student(_cursor);
_result.add(_item);
}
return _result;
} finally {
_cursor.close();
}
}
@Override
protected void finalize() {
_statement.release();
}
}.getLiveData();
}
核心逻辑就在这里 Oberver 监听,Room 会把改变的数据发送过来,收到变化之后会进行刷新实现,我们进入这个 invalidate 看下:
java
final Runnable mInvalidationRunnable = new Runnable() {
@MainThread
@Override
public void run() {
boolean isActive = mLiveData.hasActiveObservers();
if (mInvalid.compareAndSet(false, true)) {
if (isActive) {
// 核心逻辑,最终执行了 mRefreshRunnable 这个 runnable
mExecutor.execute(mRefreshRunnable);
}
}
}
};
public void invalidate() {
ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
}
核心逻辑,最终执行了 mRefreshRunnable 这个 runnable,我们进入这个 Runnable 看下:
java
final Runnable mRefreshRunnable = new Runnable() {
@WorkerThread
@Override
public void run() {
boolean computed;
do {
computed = false;
if (mComputing.compareAndSet(false, true)) {
try {
T value = null;
while (mInvalid.compareAndSet(true, false)) {
computed = true;
value = compute();
}
if (computed) {
// 核心逻辑,直接调用了 postValue
mLiveData.postValue(value);
}
} finally {
mComputing.set(false);
}
}
} while (computed && mInvalid.get());
}
};
最终调用了 LiveData 的 postValue 发送数据;
升级篇
Room 的数据库升级相对比较复杂;
暴力升级
scss
public static synchronized AppDatabase getInstance(Context context) {
if (instance == null) {
instance = Room.databaseBuilder(context.getApplicationContext(),
AppDatabase.class
, "MarsDB")
// 可以强制在主线程运行数据库操作
.allowMainThreadQueries()
// 暴力升级,强制执行(数据会丢失)(慎用)
.fallbackToDestructiveMigration()
.build();
}
return instance;
}
fallbackToDestructiveMigration() 会强制升级数据库,但是可能会导致数据丢失,谨慎使用吧;
稳定升级
Room 提供了稳定升级的接口 .addMigrations()
这个接口需要我们实现 Migration,并实现具体的 migrate 逻辑;
java
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
// 在这里用 SQL 脚本完成数据的变化
database.execSQL("alter table student add column flag integer not null default 1");
}
};
然后将 MIGRATION_1_2 传入到 addMigrations() 这个方法中;
数据库一般是不建议降级的,这块大家在使用过程中也尽量不要降级处理吧;
好了,room 就先写到这里吧~
欢迎三连
来都来了,点个关注,点个赞吧,你的支持是我最大的动力~