前言
Google在没有推出Room组件之前,如果我们想要操作数据库,android是采用sqlite作为数据库存储。有使用过sqlite的读者应该十分清楚,sqlite的使用相当的复杂,而且代码写起来既繁琐又容易出错。因此,各大开源社区便推出了各种ORM库来简化sqlite的操作。而作为Google的亲儿子Room也是对sqlite的一层抽象。它使用了APT机制,通过注解的方式在编译阶段生成sqlite代码,使开发者不需要写冗杂的sqlite代码,提高开发效率。
基本使用
想要了解一个东西的用法,我们首先要会用它。sqlite的用法这里不多介绍了,如果不会使用的读者可以参考下面这边博客:android使用SQLite。sqlite的用法是繁琐了点,大家多写几遍就熟悉了。
我们重点看下room的用法。
从上面这幅图中可以清楚的看到,整个room的操作流程:room对象持有了dao对象的引用,dao对象则持有了entity对象的引用,entity对象则被ui(一般是activity、fragment)持有。说的通俗一点,就是Room类中有一个dao类型的成员变量,而dao类中则有一个entity类型的成员变量,ui中则有一个entity的成员变量。
那么room、dao、entity都是些什么东西呢?在代码中又是怎么表现出来的呢?
- Entity:一个Entity 对应于数据库中的一张表。Entity 类是 Sqlite 表结构
对 Java 类的映射,在Java 中可以被看作一个Model 类。也就是Entity就是一个java的model类,只不过这个类被加入了一个Entity注解。 - Dao:其实就是一个接口,只不过这个接口被Dao注解注释着而已,接口里面的方法则对应着sql中的增删改查等方法。
- Room:一个抽象类,被Database注解注释着,里面持有着真正的room对象,底层原理其实是初始化sqlite对象。
多说无益,我们直接上代码演示:
- 添加gradle依赖
dependencies {
def room_version = "2.4.1"//目前最新的稳定版本
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
}
- Entity层
package com.example.roomdemo01.simple1.entity;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
@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;
// 注意:升级的字段 1-2个升级开始
@ColumnInfo(name = "flag")
private int flag;
public int getFlag() {
return flag;
}
public void setFlag(int flag) {
this.flag = flag;
}
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 + '\'' +
'}';
}
}
package com.example.roomdemo01.simple1.entity;
import androidx.room.ColumnInfo;
// @Entity 不能加,加了就是一张表了
public class StudentTuple {
@ColumnInfo(name = "name")//需要添加ColumnInfo注解,这样才能知道数据库的字段映射到model的哪个属性
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标签用于将Student类与Room中的数据表对应起来。tableName属性可以为数据表设置表名,若不设置,则表名与类名相同,也就是说model类加了Entity标签就是一张表。
-
@PrimaryKey标签用于指定该字段作为表的主键。
-
@ColumnInfo标签可用于设置该字段存储在数据库表中的名字,并指定字段的类型。
-
@Ignore标签用来告诉Room忽略该字段或方法。Room不会持久化被@Ignore标签标记过的字段的数据
-
Dao层
package com.example.roomdemo01.simple1.dao;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
import com.example.roomdemo01.simple1.entity.Student;
import com.example.roomdemo01.simple1.entity.StudentTuple;
import java.util.List;
@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字段,如果给Student类接收,那么address字段就会为null;
//如果不想出现这种情况,我们可以新建一个普通的Model类,切记该类不能添加Entity注解,因为添加了就是一张表了
@Query("select name,pwd from Student")
StudentTuple getRecord();
}
- DataBase层
// 为了养成好习惯 规则 ,要写 exportSchema = false ,因为在升级过程中会记录所有的历史版本信息,,因为内部要记录升级的所有副本
//使用抽象类是为了能够在编译时期使用apt生成真正的类
@Database(entities = {Student.class}, version = 1, exportSchema = false)
public abstract class AppDataBase extends RoomDatabase {
private static final AppDataBase databaseInstance;
private static final String DATABASE_NAME = "my_db"
public static synchronized AppDataBase getInstance(Context context) {
if (databaseInstance == null) {
databaseInstance = Room.databaseBuilder(
context.getApplicationContext(),
AppDataBase.class, DATABASE_NAME)
//从assets/database目录下读取students.db
//.createFromAsset("databases/students.db")
// 可以设置强制主线程,默认是让你用子线程
// .allowMainThreadQueries()
.build();
}
return databaseInstance;
}
// 暴露dao
public abstract StudentDao userDao();
}
-
@Database标签用来告诉系统这是Room数据库对象。entity属性用于指定该数据库有哪些表,若需要建立多张表,则表名以逗号隔开。version属性用于指定数据库版本号,后面数据库的升级正是根据版本号进行判断的。
-
ui层
package com.example.roomdemo01.simple1.ui;
import android.os.Bundle;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
import androidx.room.Room;
import com.example.roomdemo01.R;
import com.example.roomdemo01.simple1.dao.StudentDao;
import com.example.roomdemo01.simple1.db.AppDataBase;
import com.example.roomdemo01.simple1.entity.Student;
import com.example.roomdemo01.simple1.entity.StudentTuple;
import java.util.List;
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();
}
// 测试我们刚刚写的 三个角色Room数据库 增删改查
public class DbTest extends Thread {
@Override
public void run() {
// 数据库的操作都在这里
AppDataBase MyDB = AppDataBase.getInstance(MainActivity.this);
StudentDao dao = MyDB.userDao();
dao.insert(new Student("Studnet1", "123", 1));
dao.insert(new Student("Studnet2", "456", 2));
dao.insert(new Student("Studnet3", "789", 3));
dao.insert(new Student("Studnet4", "111", 4));
// 查询全部数据
List<Student> all = dao.getAll();
Log.d("DbTest", "run: all.toString():" + all.toString());
Log.i("DeDbTest", "--------------------------");
// 查询名字为 Studnet3 的一条数据
Student stu = dao.findByName("Studnet3");
Log.d("DbTest", "run: stu.toString():" + stu.toString());
Log.i("DbTest", "--------------------------");
// 查询 2 3 4 uid的三条数据
List<Student> allID = dao.getAllId(new int[]{2, 3, 4});
Log.d("DbTest", "run: allID.toString():" + allID.toString());
Log.i("DbTest", "--------------------------");
// 查询student表里面的数据 到 StudentTuple里面去
StudentTuple record = dao.getRecord();
Log.d("DbTest", "run: record.toString():" + record.toString());
}
}
}
是不是很像后端中spring的开发方式,分层以及大量的注解。
运行结果如下图所示:
高级使用
现在,我们知道了room的基本用法了。这里又个疑问,Google为什么要大费周章的弄个room呢?如果仅仅只是为了对sqlite做一层封装那么个人感觉有点大材小用了。其实,Room组件最大的魅力在于能够于livedata组件一起使用。LiveData组件大家知道:它是整个jetpack的核心组件,只要数据有一点改变那么ui层就能立马得知,从而改变ui。这可就和Google推荐的开发模式:jetpack+kotlin+mvvm遥相呼应了。
下面,来演示下LiveData如何与Room结合使用,其实用法不难:
// 在dao层中使用 LiveData 关联 Room,这样从数据库中拿到的数据便可以映射到LiveData中
@Query("select * from Student order by uid")
LiveData<List<Student>> getAllLiveDataStudent();
然后,我们在ui层中对LiveData进行观察
package com.example.roomdemo02;
import android.os.Bundle;
import android.widget.ListView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProviders;
import java.util.List;
public class MainActivity extends AppCompatActivity {
ListView listView;
StudentViewModel studentViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
listView = findViewById(R.id.listView);
studentViewModel = ViewModelProviders.of(this).get(StudentViewModel.class);
// 观察者,只要页面在前台,只要数据改变,用户就可以肉眼看到这种变化
studentViewModel.getAllLiveDataStudent().observe(this, new Observer<List<Student>>() {
@Override
public void onChanged(List<Student> students) {
// 更新UI
listView.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("Student", "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();
}
studentViewModel.update(new Student(6, "Student" + i, "123", 1));
}
}
}.start();
}
}
注意:LiveData通常与ViewModel一起使用,ViewModel是用于存放数据的,因此可以将数据库放在ViewModel中进行实例化。但数据库的实例化需要用到Context,而ViewModel中最好不要传入Context,因此,我们不宜直接使用ViewModel,而应该用它的子类AndroidViewModel,并且在viemodel的构造函数中传入Application类型的上下文。
Room数据库版本升级
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import androidx.room.migration.Migration;
import androidx.sqlite.db.SupportSQLiteDatabase;
// exportSchema = false 尽量写,内部需要检测,如果没有写,会抛出异常,因为内部要记录升级的所有副本
@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
, "my_db")
// 可以强制在主线程运行数据库操作
.allowMainThreadQueries()
// 暴力升级 不管三七二十一 强制执行(数据会丢失)(慎用)
// .fallbackToDestructiveMigration()
// 稳定的方式升级
.addMigrations(MIGRATION_1_2)
.build();
}
return instance;
}
public abstract StudentDao studentDao();
// 下面是稳定升级的方式
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");
}
};
// ROOM 是不能降级的,我非要删除一个字段,却要保证数据的稳定性,这个是特殊情况
// 特殊手法降级
static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
// SQL 四步法
// 1.先建立临时表
// database.execSQL("create table student_temp (uid integer primary key not null,name text,pwd text,addressId)");
// 2.把之前表的数据(SQL语句的细节,同学们可以上网查询下)
// database.execSQL("insert into student_temp (uid,name,pwd,addressid) " + " select uid,name,pwd,addressid from student");
// 3.删除student 旧表
// database.execSQL("drop table student");
// 4.修改 临时表 为 新表 student
// database.execSQL("alter table student_temp rename to student");
}
};
}
源码分析
首先,我们需要明确我们的疑惑是什么?笔者在学习room时的疑惑有下面几点:
- room是如何封装sqlite?
- Dao层中哪些带注解的方法又是如何被调用的呢?
接下来,我们就带着这两个疑惑来看源码。首先,我们从Room的初始化方法开始看起。我们知道Room采用了建造者模式
public T build() {
//noinspection ConstantConditions
if (mContext == null) {
throw new IllegalArgumentException("Cannot provide null context for the database.");
}
//noinspection ConstantConditions
if (mDatabaseClass == null) {
throw new IllegalArgumentException("Must provide an abstract class that"
+ " extends RoomDatabase");
}
if (mQueryExecutor == null && mTransactionExecutor == null) {
mQueryExecutor = mTransactionExecutor = ArchTaskExecutor.getIOThreadExecutor();
} else if (mQueryExecutor != null && mTransactionExecutor == null) {
mTransactionExecutor = mQueryExecutor;
} else if (mQueryExecutor == null && mTransactionExecutor != null) {
mQueryExecutor = mTransactionExecutor;
}
if (mMigrationStartAndEndVersions != null && mMigrationsNotRequiredFrom != null) {
for (Integer version : mMigrationStartAndEndVersions) {
if (mMigrationsNotRequiredFrom.contains(version)) {
throw new IllegalArgumentException(
"Inconsistency detected. A Migration was supplied to "
+ "addMigration(Migration... migrations) that has a start "
+ "or end version equal to a start version supplied to "
+ "fallbackToDestructiveMigrationFrom(int... "
+ "startVersions). Start version: "
+ version);
}
}
}
if (mFactory == null) {
mFactory = new FrameworkSQLiteOpenHelperFactory();
}
if (mCopyFromAssetPath != null || mCopyFromFile != null) {
if (mName == null) {
throw new IllegalArgumentException("Cannot create from asset or file for an "
+ "in-memory database.");
}
if (mCopyFromAssetPath != null && mCopyFromFile != null) {
throw new IllegalArgumentException("Both createFromAsset() and "
+ "createFromFile() was called on this Builder but the database can "
+ "only be created using one of the two configurations.");
}
mFactory = new SQLiteCopyOpenHelperFactory(mCopyFromAssetPath, mCopyFromFile,
mFactory);
}
//上面这些都是些支线逻辑,我们不需要看
DatabaseConfiguration configuration =
new DatabaseConfiguration(
mContext,
mName,
mFactory,
mMigrationContainer,
mCallbacks,
mAllowMainThreadQueries,
mJournalMode.resolve(mContext),
mQueryExecutor,
mTransactionExecutor,
mMultiInstanceInvalidation,
mRequireMigration,
mAllowDestructiveMigrationOnDowngrade,
mMigrationsNotRequiredFrom,
mCopyFromAssetPath,
mCopyFromFile);
T db = Room.getGeneratedImplementation(mDatabaseClass, DB_IMPL_SUFFIX);
db.init(configuration);
return db;
}
上面的build方法中重点在于最后一句init方法,我们进入init方法看下究竟做了什么事情。
点击去之后,发现居然是个抽象方法,那么我们进入它是实现类看下,这个AppDatabase_Impl其实就是APT生成的类
看到这里大家应该都明白了吧!其实room底层也是调用了sqlite来创建数据库的。我们看下createAllTables方法是怎么被调的。
createAllTables被RoomOpenHelper的onCreate方法调用,而RoomOpenHelper的onCreate方法则是被OpenHelper类的onCreate方法调用,这个OpenHelper类则是在FrameworkSQLiteOpenHelper类的构造方法中被初始化,而FrameworkSQLiteOpenHelper则是在前面的build方法的时候被初始化的。
至此,整个room的初始化逻辑链路全部连接起来了。
至于,第二点则跟简单了,APT会生成一个Impl类,此例是StudentDao_Impl类,我们调用的其实就是该类的对应方法。
最后
感谢各位读者耐心读到最后,谢谢!!!