Room数据库使用
Room数据库
简介:
-
Room是Google官方推出的Architecture Components中的一个持久性数据库,提供了基于SQLite的抽象层,可以在充分利用SQLite功能的前提下流畅的访问数据库。
-
Room是google官方开发的对象关系映射(ORM)库框架,采用注解的形式。它能缓存相关数据,使用户在断网的情况下浏览相应内容,重新连接网络时,用户发起的内容更改都会同步到服务器。能将本地数据保存到数据库中,它提供了一个SQLite的抽象层(封装),能够让我们更加稳健地访问数据库,能够提升数据库的性能。
优点
- 使用便捷。只需简单的三步少量的代码即可实现常规的CDUR;
- 支持LiveData;
- 与Rxjava结合可以实时监听数据变化。
- 针对 SQL 查询的编译时验证。
- 可最大限度减少重复和容易出错的样板代码的方便注解。
- 简化了数据库迁移路径。
Room的三个主要组件
- Entity: 表示Room数据库内的表(Table)。
- Database: 数据库的持有者,是与 App 持久关联数据的底层连接的主要访问点。是一个继承 RoomDatabase 的抽象类。在类前注解中包含与数据库相关联的实体列表。类中包含一个具有 0 个参数的抽象方法,并返回用 @Dao 注解的类。 用户可以通过调用 Room.databaseBuilder() 或 Room.inMemoryDatabaseBuilder() 获取 Database 实例。
- Dao(Data access object): Dao是用户来使用访问数据库的主要工具,它可以是一个接口,可以是一个抽象类。如果是一个抽象类,可以有一个构造函数,其只接收一个 RoomDatabase 参数。当系统编译时,Room会自动为Dao创造具体实现。
Room的相关依赖
dependencies {
def room_version = "2.4.1"
//Room数据库的依赖
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
// optional - RxJava2 support for Room
implementation "androidx.room:room-rxjava2:$room_version"
// optional - RxJava3 support for Room
implementation "androidx.room:room-rxjava3:$room_version"
// optional - Guava support for Room, including Optional and ListenableFuture
implementation "androidx.room:room-guava:$room_version"
// optional - Test helpers
testImplementation "androidx.room:room-testing:$room_version"
// optional - Paging 3 Integration
implementation "androidx.room:room-paging:2.4.1"
}
使用Room数据库
使用@Entity创建表
@Entity(tableName = "student") //设置表名
public class Student {
@NonNull //主键不能为null,必须添加这个注解
@PrimaryKey(autoGenerate = true) //主键是否自动增长,默认为false
private int id;
@ColumnInfo(name = "firstName") //可以通过设置name =xxx 的方式重新命名字段
private String name;
@Embedded
private Grade grade;
public Student() {
}
@Ignore //只允许有一个主构造方法,其他构造方法要使用@Ignore设置为忽略
public Student(int id, String name, Grade grade) {
this.id = id;
this.name = name;
this.grade = grade;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Grade getGrade() {
return grade;
}
public void setGrade(Grade grade) {
this.grade = grade;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", grade=" + grade.toString() +
'}';
}
}
上表中的嵌套类:
public class Grade {
private int math;
private int english;
private int chinese;
@Ignore
private int history; //这个字段将被忽略,不会被映射到表中
public Grade() {
}
@Ignore
public Grade(int math, int english, int chinese, int history) {
this.math = math;
this.english = english;
this.chinese = chinese;
this.history = history;
}
public int getHistory() {
return history;
}
public void setHistory(int history) {
this.history = history;
}
public int getMath() {
return math;
}
public void setMath(int math) {
this.math = math;
}
public int getEnglish() {
return english;
}
public void setEnglish(int english) {
this.english = english;
}
public int getChinese() {
return chinese;
}
public void setChinese(int chinese) {
this.chinese = chinese;
}
@Override
public String toString() {
return "Grade{" +
"math=" + math +
", english=" + english +
", chinese=" + chinese +
", history=" + history +
'}';
}
}
-
介绍: 上述过程即为Room数据库的表单的创建过程,通过注解我们可以在原有类上完成表的创建,与SQLite相比Room数据库去除了我们自己通过SQ语句创建表单的繁琐过程。
-
下面说一下类中出现的注解的意思:
-
@Entity
在Room中每个表对应一个具有 @Entity 注解的JavaBean,也就是说一个bean对应一张表;(通过此注解标注此类为表单)
-
@PrimaryKey
所有的CRUD操作都是根据主键进行操作的,所以每个表要求必须有一个主键,主键字段通过 @PrimaryKey 进行标识,因为主键不能为空,所以要求必须添加android.support.annotation注解库中的 @NonNull 注解 ,否则编译报错。主键默认是不自增,因为主键的类型并不限于long、int等类型,String等类型也是可以的;
-
@ColumnInfo
bean中每一个具有@ColumnInfo 注解的字段会被映射为表中的一个字段,映射字段可以更改,默认直接使用当前字段,注意如果字段为private修饰的则必须提供对应的getter、setter方法,public字段修饰则不用;
-
@Ignore
顾名思义,该注解是用于设置忽略的,每个表对应的JavaBean只能有一个主构造方法,若还有其它的构造方法则必须使用@Ignore注解进行标识,而且此注解还可标识字段,被标识的字段将不会被创建在表单中,在使用数据库对对象进行存储时此字段同理也将不会被存储。
-
@Embedded
这也是一个很方便实用的注解,这个注解可以将普通JavaBan中的字段也引入到对应的表中,查询结果会自动为对应的bean。使用此注解将实现嵌套类的直接存储。
-
使用@Dao,创建操作数据库的接口
- Dao在Room组件中是一个非常重要的组成部分。在这个Dao类中,定义了许多操作数据库的简便方法。
@Dao
public interface StudentDao {
//-------------------------插入--------------------------
/**
* 插入操作,若已存在相同的主键的数据则直接覆盖
* 返回值也可以设为void
*
* @param students
* @return 插入的 row id集合
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
List<Long> insert(Student... students);
/**
* 同上
*
* @param student
* @return 插入的 row id
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
Long insert(Student student);
/**
* 同上
*
* @param list
* @return 插入的 row id
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
List<Long> insert(List<Student> list);
//-------------------------更新--------------------------
/**
* 更新操作
*
* @param student
* @return 更新成功的数量
*/
@Update
int update(Student student);
/**
* 同上
*
* @param student
* @return 更新成功的数量
*/
@Update
int update(Student... student);
//-------------------------查询--------------------------
/**
* 查询指定名字的数据
* @param name
* @return
*/
@Query("select * from student where firstName =:name")
List<Student> getByName(String name);
/**
* 查询指定id的数据
* @param id
* @return
*/
@Query("SELECT * FROM student where id = :id")
Student getById(int id);
/**
* 查询所有数据
*
* @return
*/
@Query("select * from student")
List<Student> loadAll();
/**
* 查询所有数据
* 与Rxjava的Flowable结合,可以进行数据变化监听
* @return
*/
@Query("select * from student")
Flowable<List<Student>> getAll();
//-------------------------删除--------------------------------------------
/**
* 根据主键删除
* @param student
* @return 删除成功的数量
*/
@Delete
int deleteById(Student student);
/**
* 删除所有数据
* @return
*/
@Query("delete from student")
int deleteAll();
/**
* 删除指定名字的数据
*
* @param name
* @return
*/
@Query("delete from student where firstName = :name")
int deleteByName(String name);
}
-
下面介绍上面类中各注解:
-
@Dao
DAO是数据库访问对象,要变成一个数据库访问对象必须在类的声明上面添加 @Dao 注解,并且根据官方文档的要求,DAO必须是一个interface 或者 abstract class,然后在里面写操作数据库的相关接口,主要的是通过在方法上添加 @Insert、@Update、@Query、@Delete中的某一个注解实现对应的“插入、更新、查询、删除”操作。
注意:DAO类中这些操作都是阻塞的,使用要在异步线程中使用。在创建Database对象时可以选择是否允许在主线程中查询数据
-
@Insert
@Insert
方法的每个参数必须是带有@Entity
注解的 Room 数据实体类的实例或数据实体类实例的集合。调用@Insert
方法时,Room 会将每个传递的实体实例插入到相应的数据库表中。如果
@Insert
方法接收单个参数,则会返回long
值,这是插入项的新rowId
。如果参数是数组或集合,则该方法应改为返回由long
值组成的数组或集合,并且每个值都作为其中一个插入项的rowId
。返回值也可void
;插入操作可能出现冲突,所以我们通过@Insert(onConflict = OnConflictStrategy.REPLACE)设置在出现冲突时的应对策略,OnConflictStrategy接口中给我们提供了以下5个策略:
public @interface OnConflictStrategy { /** * OnConflict strategy constant to replace the old data and continue the transaction. * 取代旧数据同时继续事务 */ int REPLACE = 1; /** * OnConflict strategy constant to rollback the transaction. * 回滚事务 */ int ROLLBACK = 2; /** * OnConflict strategy constant to abort the transaction. * 终止事务 */ int ABORT = 3; /** * OnConflict strategy constant to fail the transaction. * 事务失败 */ int FAIL = 4; /** * OnConflict strategy constant to ignore the conflict. * 忽略冲突 */ int IGNORE = 5; }
-
@Update
Room 使用主键将传递的实体实例与数据库中的行进行匹配。如果没有具有相同主键的行,Room 不会进行任何更改。
@Update
方法可以选择性地返回int
值,该值指示成功更新的行数。 -
@Delete
Room 使用主键将传递的实体实例与数据库中的行进行匹配。如果没有具有相同主键的行,Room 不会进行任何更改。
@Delete
方法可以选择性地返回int
值,该值指示成功删除的行数。 -
@Query
使用
@Query
注解,**您可以编写 SQL 语句并将其作为 DAO 方法公开。使用这些查询方法从应用的数据库查询数据,或者需要执行更复杂的插入、更新和删除操作。**Room 会在编译时验证 SQL 查询。这意味着,如果查询出现问题,则会出现编译错误,而不是运行时失败。通过SQL语句可以实现复杂插入,更新等操作。-
查询字段的子集
有时候我们不需要查询返回所有的数据,我们只需要表中的某几条数据,这是我们要创建一个类,这个类中包含我们想得到的对象,然后在Query的返回参数中设置成这个类的对象就可以了。// An highlighted block public class NameTuple { @ColumnInfo(name="first_name") public String firstName; @ColumnInfo(name="last_name") public String lastName; } @Dao public interface MyDao { @Query("SELECT first_name, last_name FROM user") public List<NameTuple> loadFullName(); }
-
-
使用Room写SQL引用查询条件的格式是“字段名称=:引用值”(字段名称会有智能提示,会有亮色等提示),比如例子中的:
@Query("SELECT * FROM student where id = :id") //此冒号后的id即为此方法传入的参数 Student getById(int id);
使用@Database,创建数据库
//entities表示要包含哪些表;version为数据库的版本,数据库升级时更改;exportSchema是否导出数据库结构,默认为true @Database(entities = {Student.class}, version = 1, exportSchema = true) public abstract class AppDatabase extends RoomDatabase { public static AppDatabase sInstance; public static AppDatabase getInstance(Context context) { if(sInstance == null) { synchronized (AppDatabase.class) { if(sInstance == null) { //三个参数:第一个Context对象,第二个为数据库类,第三个为数据库名字 sInstance = Room.databaseBuilder(context,AppDatabase.class,"data") // 设置是否允许在主线程做查询操作 .allowMainThreadQueries() // setJournalMode(@NonNull JournalMode journalMode) 设置数据库的日志模式 // 设置迁移数据库如果发生错误,将会重新创建数据库,而不是发生崩溃 // .fallbackToDestructiveMigration() 会清理表中的数据 ,不建议这样做 //设置从某个版本开始迁移数据库如果发生错误,将会重新创建数据库,而不是发生崩溃 //.fallbackToDestructiveMigrationFrom(int... startVersions); /*.addCallback(new Callback() { 进行数据库的打开合创建的监听 @Override public void onCreate(@NonNull SupportSQLiteDatabase db) { super.onCreate(db); } @Override public void onOpen(@NonNull SupportSQLiteDatabase db) { super.onOpen(db); } }) */ .build(); } } } return sInstance; } public abstract StudentDao getStudentDao(); }
-
@Database
@Database用于创建数据库,需要继承RoomDatabase,若该注解后面不加“exportSchema = false”,该属性默认为true,则表示导出数据库的概要和记录,建议默认设置导出,方便查看历史更改记录,这个时候要在app/build.gradle下的defaultConfig中要增加如下配置,不然编译会报错
javaCompileOptions { annotationProcessorOptions { //room的数据库概要、记录 arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] } }
增加该配置后编译在Project视图下即可看到导出的数据库结构文件,文件是以json的格式记录。
然后再创建一个获取StudentDao 的抽象方法getStudentDao(),这一部分就算基本完成了。
如上:
public abstract StudentDao getStudentDao(); //此抽象方法会返回自己的Dao对象
使用:
由于数据库查询等操作为费时操作,所以需要放在子线程进行,所以建议学习Rxjava,通过Rxjava实现查询等的异步操作。这里简单展示:
findViewById(R.id.query).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Observable.create(new ObservableOnSubscribe<List<Student>>() { @Override public void subscribe(@NonNull ObservableEmitter<List<Student>> emitter) throws Exception { List<Student> list = AppDatabase.getInstance(getApplicationContext()).getStudentDao().getByName("王皓炜"); emitter.onNext(list); emitter.onComplete(); } }).subscribeOn(Schedulers.io()) //设置被观察者在子线程进行 .observeOn(AndroidSchedulers.mainThread()) //设置观察者在主线程 .subscribe(new Observer<List<Student>>() { @Override public void onSubscribe(@NonNull Disposable d) { } @Override public void onNext(@NonNull List<Student> list) { for(int i = 0; i < list.size(); i++) { Log.d("TAG", list.get(i).toString()); } } @Override public void onError(@NonNull Throwable e) { Log.d("TAG", "15"); } @Override public void onComplete() { } }); } });
我们通常可以提前创建一个管理类:
public class DbManger { private static Context context; private DbManger(){}; public static DbManger sInstance; public static DbManger getInstance(Context context) { DbManger.context = context; if (sInstance == null) { synchronized (DbManger.class) { if (sInstance == null) { sInstance = new DbManger(); } } } return sInstance; } /** * 插入一个数据 * @param student * @return */ public Observable<Long> insert (Student student) { return Observable.create(new ObservableOnSubscribe<Long>() { @Override public void subscribe(@NonNull ObservableEmitter<Long> emitter) throws Exception { Long l = AppDatabase.getInstance(context).getStudentDao().insert(student); emitter.onNext(l); } }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()); } }
然后插入数据使用此类:
findViewById(R.id.add).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { CompositeDisposable compositeDisposable = new CompositeDisposable(); Student student = new Student(); student.setName("王皓炜"); student.setGrade(new Grade(59, 59, 59, 59)); Disposable d = DbManger.getInstance(getApplicationContext()).insert(student).subscribe(); compositeDisposable.add(d); } });
这样代码将看着更加简洁。
-
数据库升级
Room中升级可以根据情景不同分为两种方式:
1.简单粗暴式
在创建Database对应类时设置fallbackToDestructiveMigration(),这样每次要升级时直接更改数据库版本号就可以了,不需要其它额外的操作,不过这样用户体验不大好。
sInstance = Room.databaseBuilder(context,AppDatabase.class,"data")
.fallbackToDestructiveMigration() //设置数据库升级的时候清除之前的所有数据
.build();
2.优雅友好式
Room中提供了专门用于升级的Migration类,通过它可以优雅的实现数据库升级,原来的数据不会丢失。
首先在自己的数据类中添加字段:
如我想在student表中增添sport字段,则首先在对应类中添加字段:(此字段只要在student表中即可,也可添加至嵌套类,如将sport添加至grade中)
@Entity(tableName = "student") //设置表名
public class Student {
@NonNull //主键不能为null,必须添加这个注解
@PrimaryKey(autoGenerate = true) //主键是否自动增长,默认为false
private int id;
@ColumnInfo(name = "firstName") //可以通过设置name =xxx 的方式重新命名字段
private String name;
@ColumnInfo(name = "sport") //新添加的字段
private Integer sport;
@Embedded
private Grade grade;
public Student() {
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", sport=" + sport +
", grade=" + grade.toString() +
'}';
}
@Ignore
public Student(int id, String name, Grade grade) {
this.id = id;
this.name = name;
this.grade = grade;
}
public Integer getSport() {
return sport;
}
public void setSport(Integer sport) {
this.sport = sport;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Grade getGrade() {
return grade;
}
public void setGrade(Grade grade) {
this.grade = grade;
}
}
然后在表中创建字段:
sInstance = Room.databaseBuilder(context,AppDatabase.class,"data")
.addMigrations(MIGRATION_1_2) //升级数据库
.build();
/**
* 数据库版本从1升至2,且在student表单中增添sport字段
*/
static final Migration MIGRATION_1_2 = new Migration(1,2) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
//向student表中添加一个sport字段
database.execSQL("ALTER TABLE student ADD COLUMN sport INTEGER"); //这种方式不支持一次添加多个字段,可以多写几条sql语句分别执行
}
};
然后需要将数据库版本修改至2,如下:
@Database(entities = {Student.class}, version = 2, exportSchema = true) //这里的版本从1修改至2,与上面对应
public abstract class AppDatabase extends RoomDatabase {
}
至此数据库升级完成,对比数据:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RQ6pkDtf-1644985406952)(C:\Users\过客\AppData\Roaming\Typora\typora-user-images\image-20220216114331762.png)]
由于此前没有sport字段,所以对于之前存储的数据sport字段为默认值,此后就为存储的数值了。
注意:
若你新添加的字段上添加有@NoNull注解,如:
@NonNull
@ColumnInfo(name = "sport")
private Integer sport;
那么在创建时也要与之对应:
database.execSQL("ALTER TABLE student ADD COLUMN sport INTEGER NOT NULL DEFAULT 1");
上面两者需要对应,否则会抛异常。
3.跳跃式升级
上面的升级方式都是渐进式的一级一级往上升,但实际情况下可能会出现:用户当前app的数据库版本是1,而当前最新版app的数据库版本是3,此时用户升级就会直接从版本1升级到版本3,这种跳跃式的升级又该怎么处理呢?这种时候就要先创建新表,复制旧表数据,然后重命名新表。
/**
* 数据库升级 version1 -> version3 物理
*/
static final Migration MIGRATION_1_3 = new Migration(1, 3) {
@Override
public void migrate(SupportSQLiteDatabase database) {
//创建新表
database.execSQL("CREATE TABLE IF NOT EXISTS student_new(id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
"name TEXT,chinese INTEGER,english INTEGER,math INTEGER,sport INTEGER,physical INTEGER)");
//备份数据
database.execSQL("INSERT INTO student_new(id,name,chinese,english,math) SELECT id,name,chinese,english,math FROM student");
//删除旧表
database.execSQL("DROP TABLE student");
//重命名表
database.execSQL("ALTER TABLE student_new RENAME TO student");
}
};