Room介绍
我们在很多项目中都会使用到数据库SQLite,我之前在项目中都是用的第三方框架GreenDao,知道前几天我才听说有个Google自己弄出来的框架Room,后来我研究了几天,简直吊炸天啊!代码量减少了很多,而且使用起来非常的简单。
Room 持久性库在 SQLite 的基础上提供了一个抽象层,让用户能够在充分利用 SQLite 的强大功能的同时,获享更强健的数据库访问机制。
处理大量结构化数据的应用可极大地受益于在本地保留这些数据。最常见的用例是缓存相关数据。这样,当设备无法访问网络时,用户仍可在离线状态下浏览相应内容。设备之后重新连接到网络后,用户发起的所有内容更改都会同步到服务器。
由于 Room 负责为您处理这些问题,因此我们强烈建议您使用 Room(而不是 SQLite)。
优点是:
1.大大的减少了代码量,是大大的!!!
2.层次清晰,上手简单,而且这是谷歌官方提供的,更加安全可靠。
Room 包含 3 个主要组件
应用使用 Room 数据库来获取与该数据库关联的数据访问对象 (DAO)。然后,应用使用每个 DAO 从数据库中获取实体,然后再将对这些实体的所有更改保存回数据库中。最后,应用使用实体来获取和设置与数据库中的表列相对应的值。
Room依赖
要添加 Room 的依赖项,您必须将 Google Maven 代码库添加到项目中。
在应用或模块的 build.gradle 文件中添加所需工件的依赖项:
java:
dependencies {
def room_version = "2.2.5"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
// optional - RxJava support for Room
implementation "androidx.room:room-rxjava2:$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"
}
kotlin:
注意:对于基于 Kotlin 的应用,请确保使用 kapt 而不是 annotationProcessor。您还应添加 kotlin-kapt 插件。
//kotlin-kapt插件Android的注解处理 java使用annotationProcessor 添加的依赖改为使用kapt
apply plugin: 'kotlin-kapt'
dependencies {
def room_version = "2.2.5"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
// optional - Test helpers
testImplementation "androidx.room:room-testing:$room_version"
}
配置编译器选项
Room 具有以下注释处理器选项:
- room.schemaLocation:配置并启用将数据库架构导出到给定目录中的 JSON 文件的功能。
迁移。 - room.incremental:启用 Gradle 增量注释处理器。
- room.expandProjection:配置 Room 以重新编写查询,使其顶部星形投影在展开后仅包含 DAO 方法返回类型中定义的列。
以下代码段举例说明了如何配置这些选项:
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [
"room.schemaLocation":"$projectDir/schemas".toString(),
"room.incremental":"true",
"room.expandProjection":"true"]
}
}
}
}
Room的使用
创建实体类,定义Entity
//默认情况下,Room 将类名称用作数据库表名称。如果您希望表具有不同的名称,请设置 @Entity 注释的 tableName 属性
//@Entity(tableName = "students")
@Entity
public class Student {
//每个实体必须将至少 1 个字段定义为主键。即使只有 1 个字段,您仍然需要为该字段添加 @PrimaryKey 注释
// @PrimaryKey //主键
// public int id;
@PrimaryKey(autoGenerate = true) //主键自增长
public int id;
//与 tableName 属性类似,Room 将字段名称用作数据库中的列名称。如果您希望列具有不同的名称,请将 @ColumnInfo 注释添加到字段
@ColumnInfo(name = "name")//设置数据库的列名,建议写上
public String name;
@ColumnInfo(name = "pwd")
private String password;
@ColumnInfo(name = "addressId")
private int addressId;
@Ignore //忽略字段,不加入数据库
@ColumnInfo(name = "desc")
public String desc;
public Student() {
}
public Student(String name, String password, int addressId, String desc) {
this.name = name;
this.password = password;
this.addressId = addressId;
this.desc = desc;
}
要保留某个字段,Room 必须拥有该字段的访问权限。您可以将某个字段设为公开字段,也可以为其提供 getter 和 setter。如果您使用 getter 和 setter 方法,则请注意,这些方法需遵循 Room 中的 JavaBeans 规范。
注意:实体可以具有空的构造函数(如果相应的 DAO 类可以访问保留的每个字段),也可以具有其参数包含的类型和名称与该实体中字段的类型和名称一致的构造函数。Room 还可以使用完整或部分构造函数,例如仅接收部分字段的构造函数。
使用主键
每个实体必须将至少 1 个字段定义为主键。即使只有 1 个字段,您仍然需要为该字段添加 @PrimaryKey 注释。此外,如果您想让 Room 为实体分配自动 ID,则可以设置 @PrimaryKey 的 autoGenerate 属性。
@Entity
public class Student {
//每个实体必须将至少 1 个字段定义为主键。即使只有 1 个字段,
//您仍然需要为该字段添加 @PrimaryKey 注释
// @PrimaryKey //主键
// public int id;
@PrimaryKey(autoGenerate = true) //主键自增长
public int id;
}
如果实体具有复合主键,您可以使用 @Entity 注释的 primaryKeys 属性,如以下代码段所示:
@Entity(primaryKeys = {"firstName", "lastName"})
public class User {
public String firstName;
public String lastName;
}
设置数据库的列名
与 tableName 属性类似,Room 将字段名称用作数据库中的列名称。如果您希望列具有不同的名称,请将 @ColumnInfo 注释添加到字段,如以下代码段所示:
//与 tableName 属性类似,Room 将字段名称用作数据库中的列名称。如果您希望列具有不同的名称,请将 @ColumnInfo 注释添加到字段
@ColumnInfo(name = "name")//设置数据库的列名,建议写上
private String name;
忽略字段
默认情况下,Room 会为在实体中定义的每个字段创建一个列。如果某个实体中有您不想保留的字段,则可以使用 @Ignore 为这些字段注释,如以下代码段所示:
@Ignore //忽略字段,不加入数据库
@ColumnInfo(name = "desc")
private String desc;
如果实体继承了父实体的字段,则使用 @Entity 属性的 ignoredColumns 属性通常会更容易:
@Entity(ignoredColumns = "picture")
public class RemoteUser extends User {
@PrimaryKey
public int id;
public boolean hasVpn;
}
设置表名
默认情况下,Room 将类名称用作数据库表名称。如果您希望表具有不同的名称,请设置 @Entity 注释的 tableName 属性,如以下代码段所示:
@Entity(tableName = "students")
public class Student{
// ...
}
注意:SQLite 中的表名称不区分大小写。
创建Dao,使用 Room DAO 访问数据
Dao主要是定义了增删改查的一系列操作,在开头记得注解@Dao。
使用“Room persistence library”访问应用程序的数据,可以使用数据访问对象或DAOs。这组DAO对象构成了Room的主要组件,因为每个DAO都包含了对应用程序数据库访问的抽象方法。
通过使用DAO类访问数据库而不是查询构造器或直接查询,可以分离数据库体系结构的不同组件。此外,DAOs允许您在测试应用程序时轻松模拟数据库访问。
注意:在向应用程序添加DAO类之前,将架构组件库添加到应用程序的build.gradle中。
DAO既可以是接口,也可以是抽象类。如果是抽象类,它可以有一个构造函数,它把RoomDatabase作为唯一的参数。Room在编译时创建每个DAO实现。
注意:除非在建造器上调用了allowMainThreadQueries(),否则Room不支持主线程上的数据库访问,因为它可能会长时间锁定UI。返回LiveData或Flowable实例的异步查询可免除此规则,因为它们在需要时异步地在后台线程上运行查询。
@Insert,@Delete,@Update,@Query,分别对应了增删改查四种操作
@Insert,@Delete,@Update,可以传入多种参数,可以以实体类的方式传入,也可以以List的方式传入
@Query,也可以返回不同的类型,可以返回一个实体类,也可以返回一个List,具体看实际应用
插入
- 当您创建一个DAO方法并用@Insert注解时,Room生成一个实现,在一个事务中将所有参数插入到数据库中。
- 如果@Insert方法只接收1个参数,则可以返回一个Long型的值,这是插入项的新rowId。如果参数是数组或集合,则应该返回long[]或者 List类型的值。
@Insert(onConflict = OnConflictStrategy.REPLACE)
public void insertStudent(Student... student);
@Insert
public void insertBothStudent(Student student1, Student student2);
@Insert
public void insertStudentAndFriends(Student student, List<Student> friends);
@Insert
void insert(List<Student> list);
修改
Update方法在数据库中用于修改一组实体的字段。它使用每个实体的主键来匹配查询。
下面的代码片段演示如何定义此方法:
//修改
//第一个要做的就是先找出那一项内容,就是根据用户提供的字段去查询那一项内容
//找到我们想找的那一项了,然后就是更改那一项的内容
@Update
void update(Student... student);
删除
Delete方法用于从数据库中删除给定参数的一系列实体,它使用主键匹配数据库中相应的行。
下面的代码片段演示如何定义此方法:
//删除某一项
@Delete
void delete(Student student);
//删全部
@Query("DELETE FROM Student")
void deleteAll();
虽然通常不需要,但可以使用此方法返回int值,以指示从数据库中删除的行数。
信息查询
@Query是DAO类中使用的主要注解。它允许您在数据库上执行读/写操作。每个@Query方法在编译时被验证,因此,如果存在查询问题,则会发生编译错误而不是运行时故障。
Room还验证查询的返回值,这样如果返回对象中字段的名称与查询响应中的相应列名不匹配,则Room将以以下两种方式之一提醒您:
- 如果只有一些字段名匹配,则发出警告。
- 如果没有字段名匹配,则会出错。
查询全部:
//查询全部
@Query("SELECT * FROM Student")
List<Student> queryAll();
这是一个极其简单的查询,可加载所有用户。在编译时,Room 知道它在查询用户表中的所有列。如果查询包含语法错误,或者数据库中没有用户表格,则 Room 会在您的应用编译时显示包含相应消息的错误。
将参数传递到查询中:
大多数情况下,需要将参数传递到查询中以执行筛选操作,例如只显示年龄大于某一年龄的用户。要完成此任务,请在您的Room注解中使用方法参数,如下面的代码片段所示:
@Dao
public interface StudentDao{
@Query("SELECT * FROM Student WHERE age > :minAge")
List<Student> loadAllStudentOlderThan(int minAge);
}
当在编译时处理此查询时,Room绑定参数与minAge方法参数匹配。Room使用参数名称执行匹配。如果存在错配,则在应用程序编译时发生错误。
还可以在查询中传递多个参数或多次引用它们,如下面的代码片段所示:
@Dao
public interface StudentDao{
@Query("SELECT * FROM Student WHERE age BETWEEN :minAge AND :maxAge")
public User[] loadAllUsersBetweenAges(int minAge, int maxAge);
@Query("SELECT * FROM Student WHERE first_name LIKE :search "
+ "OR last_name LIKE :search")
public List<User> findUserWithName(String search);
}
返回列的子集:
大多数时候,你只需要得到一个实体的几个字段。例如,只需要name和password,而不显示每一个细节。通过只获取需要的字段,可以节省宝贵的资源,并且查询完成得更快。
只要可以将结果列映射到返回的对象中,就可以让您从查询中返回任何基于Java的对象。例如,您可以创建以下普通的基于Java的对象(POJO)来获取用户的name和password:
public class StudentTuple {
@ColumnInfo(name = "name")
public String name;
@ColumnInfo(name="pwd")
public String password;
}
现在,您可以在查询方法中使用此POJO:
@Query("select name,pwd from Student")
public List<StudentTuple> getRecord();
Room知道查询返回namw和password列的值,并且这些值可以映射到StudentTuple类的字段中。因此,Room可以生成适当的代码。如果查询返回太多列,或一列在StudentTuple类中不存在,则Room将显示警告。
传递参数集合:
有些查询可能要求您传递一个可变数量的参数,其中参数的确切数目直到运行时才知道。例如,您可能希望从区域的子集检索有关所有用户的信息。当一个参数表示一个集合并在运行时根据所提供的参数的数量自动扩展它时,Room就可以理解。
@Dao
public interface MyDao {
@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
public List<NameTuple> loadUsersFromRegions(List<String> regions);
}
案例:
/**
* Cerated by xiaoyehai
* Create date : 2021/8/30 6:12
* description : 数据库增删改查操作相关接口
*/
@Dao
public interface StudentDao {
//增
@Insert
void insert(Student... student);
@Insert
void insert(List<Student> list);
//删除某一项
@Delete
void delete(Student student);
//删全部
@Query("DELETE FROM Student")
void deleteAll();
//改
//第一个要做的就是先找出那一项内容,就是根据用户提供的字段去查询那一项内容
//找到我们想找的那一项了,然后就是更改那一项的内容
@Update
void update(Student... student);
//查询全部
@Query("SELECT * FROM Student")
List<Student> queryAll();
//根据一组id查询多条数据
@Query("SELECT * FROM Student WHERE id IN (:userIds)")
List<Student> queryByIds(int[] userIds);
//根据字段查询
@Query("SELECT * FROM Student WHERE name= :name")
Student getUserByName(String name);
}
创建数据库Database
这里要用@Database来注解这个类并且添加了表名,数据库版本。
@Database(entities = {Student.class}, version = 1,exportSchema = false)
public abstract class StudentDataBase extends RoomDatabase {
private static final String DB_NAME = "StudentDataBase.db";
private static volatile StudentDataBase instance;
public static synchronized StudentDataBase getInstance(Context context) {
if (instance == null) {
instance = create(context);
}
return instance;
}
private static StudentDataBase create(final Context context) {
/**
* 默认操作必须放在io线程,不能放在ui线程中做
* 解决方式:
*
* 1、把操作放在io线程中(官方建议)
* 2、如果真要把操作放在ui线程中,就必须加个allowMainThreadQueries()方法,这样数据库的操作就可以在ui线程中使用了!
*/
return Room.databaseBuilder(context, StudentDataBase.class, DB_NAME)
.allowMainThreadQueries()
.build();
}
public abstract StudentDao userDao();
}
注意:如果您的应用在单个进程中运行,则在实例化 AppDatabase 对象时应遵循单例设计模式。每个 RoomDatabase 实例的成本相当高,而您几乎不需要在单个进程中访问多个实例。
如果您的应用在多个进程中运行,请在数据库构建器调用中包含 enableMultiInstanceInvalidation()。这样,如果您在每个进程中都有一个 AppDatabase 实例,就可以在一个进程中使共享数据库文件失效,并且这种失效会自动传播到其他进程中的 AppDatabase 实例。
return Room.databaseBuilder(context, StudentDataBase.class, DB_NAME)
.allowMainThreadQueries()
.enableMultiInstanceInvalidation()
.build();
注意:
在Activity中所有对数据库的操作都不可以在主线程中进行,除非在数据库的Builder上调用了allowMainThreadQueries(),或者所有的操作都在子线程中完成,否则程序会崩溃报以下错误:
java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
接下来就可以在Activity中对数据库进行操作了:
public class MainActivity extends AppCompatActivity {
private StudentDao mStudentDao;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mStudentDao = StudentDataBase.getInstance(this).userDao();
}
public void doClick(View view) {
switch (view.getId()) {
case R.id.btn_01: //插入
final List<Student> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Student student = new Student("xyh" + i, "123456", i, "描述1");
list.add(student);
}
mStudentDao.insert(list);
break;
case R.id.btn_02: //删除
mStudentDao.deleteAll();
break;
case R.id.btn_03: //修改
//先找出那一项内容,就是根据用户提供的字段去查询那一项内容
Student student = mStudentDao.getUserByName("xyh1");
student.setPassword("654321");
//找到我们想找的那一项了,然后就是更改那一项的内容
mStudentDao.update(student);
break;
case R.id.btn_04: //查询
List<Student> students = mStudentDao.queryAll();
for (Student stu : students) {
Log.e("xyh", "doClick: " + stu.toString());
}
break;
}
}
}
支持 RxJava 进行响应式查询
Room 为 RxJava2 类型的返回值提供了以下支持:
- @Query 方法:Room 支持 Publisher、Flowable 和 Observable 类型的返回值。
- @Insert、@Update 和 @Delete 方法:Room 2.1.0 及更高版本支持 Completable、Single 和 Maybe 类型的返回值。
要使用此功能,请在应用的 build.gradle 文件中添加最新版本的 rxjava2 工件:
dependencies {
def room_version = "2.1.0"
implementation 'androidx.room:room-rxjava2:$room_version'
}
以下代码段展示了几个如何使用这些返回类型的示例:
@Dao
public interface MyDao {
@Query("SELECT * from user where id = :id LIMIT 1")
public Flowable<User> loadUserById(int id);
// Emits the number of users added to the database.
@Insert
public Maybe<Integer> insertLargeNumberOfUsers(List<User> users);
// Makes sure that the operation finishes successfully.
@Insert
public Completable insertLargeNumberOfUsers(User... users);
/* Emits the number of users removed from the database. Always emits at
least one user. */
@Delete
public Single<Integer> deleteUsers(List<User> users);
}
返回Cursor访问
如果应用的逻辑需要直接访问返回行,您可以从查询返回 Cursor 对象,如以下代码段所示:
@Dao
public interface MyDao {
@Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
public Cursor loadRawUsersOlderThan(int minAge);
}
注意:强烈建议您不要使用 Cursor API,因为它无法保证行是否存在或者行包含哪些值。只有当您已具有需要光标且无法轻松重构的代码时,才使用此功能。
可观察的查询
当执行查询时,您经常希望应用程序的UI在数据更改时自动更新。要实现这一点,请在查询方法描述中使用类型LiveData的返回值。当数据库被更新时,Room生成所有必要的代码来更新LiveData。
@Dao
public interface MyDao {
@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
public LiveData<List<User>> loadUsersFromRegionsSync(List<String> regions);
}
注意:在版本1中,Room根据查询列表来决定是否更新LiveData的实例。
使用 Kotlin 协程编写异步方法
您可以将 suspend Kotlin 关键字添加到 DAO 方法,以使用 Kotlin 协程功能使这些方法成为异步方法。这样可确保不会在主线程上执行这些方法。
@Dao
interface MyDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUsers(vararg users: User)
@Update
suspend fun updateUsers(vararg users: User)
@Delete
suspend fun deleteUsers(vararg users: User)
@Query("SELECT * FROM user")
suspend fun loadAllUsers(): Array<User>
}
注意:要将 Room 与 Kotlin 协程一起使用,您需要使用 Room 2.1.0、Kotlin 1.3.0 和 Cordoines 1.0.0 或更高版本。如需了解详情,请参阅声明依赖项。
将特定列编入索引
@Entity(indices = {@Index("name"),
@Index(value = {"last_name", "address"})})
public class User {
@PrimaryKey
public int id;
public String firstName;
public String address;
@ColumnInfo(name = "last_name")
public String lastName;
@Ignore
Bitmap picture;
}
有时,数据库中的某些字段或字段组必须是唯一的。可以通过将@Index注解的唯一属性设置为true来强制执行此唯一性属性。下面的代码示例防止表中包含两个行,它们包含firstName和lastName列的相同值集:
@Entity(indices = {@Index(value = {"first_name", "last_name"},
unique = true)})
public class User {
@PrimaryKey
public int id;
@ColumnInfo(name = "first_name")
public String firstName;
@ColumnInfo(name = "last_name")
public String lastName;
@Ignore
Bitmap picture;
}
创建嵌套对象
有时,您可能希望在数据库逻辑中将某个实体或数据对象表示为一个紧密的整体,即使该对象包含多个字段也是如此。在这些情况下,您可以使用 @Embedded 注释表示要解构到表中其子字段的对象。然后,您可以像查询其他各个列一样查询嵌套字段。
例如,您的 User 类可以包含类型 Address 的字段,该类型表示一组分别名为 street、city、state 和 postCode 的字段。要在表中单独存储组成的列,请在 User 类(使用 @Embedded 注释)中添加 Address 字段,如以下代码段所示:
public class Address {
public String street;
public String state;
public String city;
@ColumnInfo(name = "post_code")
public int postCode;
}
@Entity
public class User {
@PrimaryKey
public int id;
public String firstName;
@Embedded
public Address address;
}
然后,表示 User 对象的表会包含具有以下名称的列:id、firstName、street、state、city 和 post_code。
注意:嵌套字段还可以包含其他嵌套字段。
如果某个实体具有同一类型的多个嵌套字段,您可以通过设置 prefix 属性确保每个列都独一无二。然后,Room 会将提供的值添加到嵌套对象中每个列名称的开头。
多表查询
由于 SQLite 是关系型数据库,因此您可以指定各个对象之间的关系。尽管大多数对象关系映射库都允许实体对象互相引用,但 Room 明确禁止这样做。
定义一对多关系
即使您不能使用直接关系,Room 仍允许您定义实体之间的外键约束。
外键:foreignKeys
@Entity(foreignKeys = @ForeignKey(entity = Address.class, parentColumns = "addressId", childColumns = "addressId"))
public class Student {
@ColumnInfo(name = "addressId")
private int addressId;
}
@Entity
public class Address {
@PrimaryKey(autoGenerate = true)
public int addressId;
@ColumnInfo(name = "addressName")
public String name;
}
@Database(entities = {Student.class, Address.class}, version = 1, exportSchema = false)
public abstract class StudentDataBase extends RoomDatabase {}
@Query("select x,x,x from where student.x==address.x")
由于零个或更多个 Student 实例可以通过 addressId外键关联到一个 Address 实例,因此这会在 Address 和 Student 之间构建一对多关系模型。
定义多对多关系
您通常希望在关系型数据库中构建的另一种关系模型是两个实体之间的多对多关系,其中每个实体都可以关联到另一个实体的零个或更多个实例。例如,假设有一个音乐在线播放应用,用户可以在该应用中将自己喜爱的歌曲整理到播放列表中。每个播放列表都可以包含任意数量的歌曲,每首歌曲都可以包含在任意数量的播放列表中。
要构建这种关系的模型,您需要创建下面三个对象:
- 播放列表的实体类。
- 歌曲的实体类。
- 用于保存每个播放列表中的歌曲相关信息的中间类。
@Entity
public class Playlist {
@PrimaryKey public int id;
public String name;
public String description;
}
@Entity
public class Song {
@PrimaryKey public int id;
public String songName;
public String artistName;
}
将中间类定义为包含对 Song 和 Playlist 的外键引用的实体:
@Entity(tableName = "playlist_song_join",
primaryKeys = { "playlistId", "songId" },
foreignKeys = {
@ForeignKey(entity = Playlist.class,
parentColumns = "id",
childColumns = "playlistId"),
@ForeignKey(entity = Song.class,
parentColumns = "id",
childColumns = "songId")
})
public class PlaylistSongJoin {
public int playlistId;
public int songId;
}
这会生成一个多对多关系模型。借助该模型,您可以使用 DAO 按歌曲查询播放列表和按播放列表查询歌曲:
@Dao
public interface PlaylistSongJoinDao {
@Insert
void insert(PlaylistSongJoin playlistSongJoin);
@Query("SELECT * FROM playlist " +
"INNER JOIN playlist_song_join " +
"ON playlist.id=playlist_song_join.playlistId " +
"WHERE playlist_song_join.songId=:songId")
List<Playlist> getPlaylistsForSong(final int songId);
@Query("SELECT * FROM song " +
"INNER JOIN playlist_song_join " +
"ON song.id=playlist_song_join.songId " +
"WHERE playlist_song_join.playlistId=:playlistId")
List<Song> getSongsForPlaylist(final int playlistId);
}
多表查询
您的部分查询可能需要访问多个表格才能计算出结果。借助 Room,您可以编写任何查询,因此您也可以联接表格。此外,如果响应是可观察数据类型(如 Flowable 或 LiveData),Room 会观察查询中引用的所有表格,以确定是否存在无效表格。
以下代码段展示了如何执行表格联接来整合两个表格的信息:一个表格包含当前借阅图书的用户,另一个表格包含当前处于已被借阅状态的图书的数据。
@Dao
public interface MyDao {
@Query("SELECT * FROM book " +
"INNER JOIN loan ON loan.book_id = book.id " +
"INNER JOIN user ON user.id = loan.user_id " +
"WHERE user.name LIKE :userName")
public List<Book> findBooksBorrowedByNameSync(String userName);
}
您还可以从这些查询中返回 POJO。例如,您可以编写一条加载某位用户及其宠物名字的查询,如下所示:
@Dao
public interface MyDao {
@Query("SELECT user.name AS userName, pet.name AS petName " +
"FROM user, pet " +
"WHERE user.id = pet.user_id")
public LiveData<List<UserPet>> loadUserAndPetNames();
// You can also define this class in a separate file, as long as you add the
// "public" access modifier.
static class UserPet {
public String userName;
public String petName;
}
}
数据库升级
方式1:强制升级
不建议使用,因为会丢失原来的数据。
private static StudentDataBase create(final Context context) {
return Room.databaseBuilder(context, StudentDataBase.class, DB_NAME)
.allowMainThreadQueries()
.fallbackToDestructiveMigration() //强制升级
.build();
}
方式2:迁移 Room 数据库
当您在应用中添加和更改功能时,需要修改实体类以反映这些更改。当用户更新到您应用的最新版本时,您不想让他们丢失所有现有数据,尤其是在您无法从远程服务器恢复数据时。
借助 Room 持久性库,您可以编写 Migration 类,以这种方式保留用户数据。每个 Migration 类均指定一个 startVersion 和 endVersion。在运行时,Room 会运行每个 Migration 类的 migrate() 方法,以按照正确的顺序将数据库迁移到更高版本。
private static StudentDataBase create(final Context context) {
return Room.databaseBuilder(context, StudentDataBase.class, DB_NAME)
.allowMainThreadQueries()
// .fallbackToDestructiveMigration() //强制升级
.addMigrations(MIGRATION_1_2)
.build();
}
//进行数据库升级,版本1升级到2,新增一列
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
//在这里用sql脚本完成数据变化 新增一列
database.execSQL("alter table student add column flag integer not null default 1");
}
};
//进行数据库升级,版本2升级到3,删除一列
static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
//创建一个临时表
database.execSQL("create table student_temp (id integer primary key not null,name text,pwd text,addressId)");
//把数据复制到临时表
database.execSQL("insert into student (id,name,pwd,addressid)" + " select uid,name,pwd,addressid from student");
//删除原来的表
database.execSQL("drop table student");
//把临时表该名为原来表的名字
database.execSQL("alter table student_temp rename to student");
}
};
注意:要使迁移逻辑正常工作,请使用完整查询(而不是引用表示查询的常量)。
迁移过程完成后,Room 会验证架构以确保迁移正确进行。如果 Room 发现问题,则会抛出包含不匹配信息的异常。
测试迁移
编写迁移非常重要,如果未正确编写,可能会导致应用陷入崩溃循环。为了保持应用的稳定性,您应事先测试迁移。Room 提供了一个测试 Maven 工件来协助完成此测试过程。不过,要使此工件正常工作,您需要导出数据库的架构。
导出schemas
编译后,Room将数据库的schemas信息导出到JSON文件中。若要导出schema,请在build.gradle文件中设置room.schemaLocation注解处理器属性,如下面的代码片段所示:
build.gradle
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation":
"$projectDir/schemas".toString()]
}
}
}
}
您应该在您的版本控制系统中存储表示数据库的schema历史的导出JSON文件,因为它允许Room创建用于测试目的的数据库的旧版本。
为了测试这些迁移,添加android.arch.persistence.room:testing 的Maven依赖,将并schema添加到asset文件夹,如下面的代码片段所示:
build.gradle
android {
...
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
}
您应该在您的版本控制系统中存储表示数据库的schema历史的导出JSON文件,因为它允许Room创建用于测试目的的数据库的旧版本。
为了测试这些迁移,添加android.arch.persistence.room:testing 的Maven依赖,将并schema添加到asset文件夹,如下面的代码片段所示:
build.gradle
android {
...
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
}
测试包提供了MigrationTestHelper类,可以读取这些模式文件。它还实现了JUnit4 TestRule接口,因此它可以管理创建的数据库。
在以下代码段中出现一个示例迁移测试:
@RunWith(AndroidJUnit4.class)
public class MigrationTest {
private static final String TEST_DB = "migration-test";
@Rule
public MigrationTestHelper helper;
public MigrationTest() {
helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
MigrationDb.class.getCanonicalName(),
new FrameworkSQLiteOpenHelperFactory());
}
@Test
public void migrate1To2() throws IOException {
SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);
// db has schema version 1. insert some data using SQL queries.
// You cannot use DAO classes because they expect the latest schema.
db.execSQL(...);
// Prepare for the next version.
db.close();
// Re-open the database with version 2 and provide
// MIGRATION_1_2 as the migration process.
db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2);
// MigrationTestHelper automatically verifies the schema changes,
// but you need to validate that the data was migrated properly.
}
}
在数据库中创建视图
2.1.0 及更高版本的 Room 持久性库为 SQLite 数据库视图提供了支持,从而允许您将查询封装到类中。Room 将这些查询支持的类称为视图,在 DAO 中使用时,它们的行为与简单数据对象的行为相同。
注意:与实体类似,您可以针对视图运行 SELECT 语句。不过,您无法针对视图运行 INSERT、UPDATE 或 DELETE 语句。
要创建视图,请将 @DatabaseView 注释添加到类中。将注释的值设为类应该表示的查询。
@DatabaseView("SELECT user.id, user.name, user.departmentId," +
"department.name AS departmentName FROM user " +
"INNER JOIN department ON user.departmentId = department.id")
public class UserDetail {
public long id;
public String name;
public long departmentId;
public String departmentName;
}
要将此视图添加为应用数据库的一部分,请在应用的 @Database 注释中添加 views 属性:
@Database(entities = {User.class}, views = {UserDetail.class},
version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
测试数据库
在使用Room库创建数据库时,验证应用程序的数据库和用户的数据的稳定性是很重要的。
测试数据库有2种方法:
- 在Android设备上。
- 在主机开发机器上(不推荐)。
注意:当为应用程序运行测试时,Room允许您创建DAO类的模拟实例。这样,如果不测试数据库本身,就不需要创建完整的数据库。此功能是可能的,因为您的DAOs不会泄漏数据库的任何细节。
Android设备测试
测试数据库实现的推荐方法是编写一个运行在Android设备上的JUnit测试。因为这些测试不需要创建一个activity,所以它们应该比UI测试更快执行。
在设置测试时,应创建数据库的in-memory版本,以使测试更加封闭,如以下示例所示:
@RunWith(AndroidJUnit4.class)
public class SimpleEntityReadWriteTest {
private UserDao mUserDao;
private TestDatabase mDb;
@Before
public void createDb() {
Context context = InstrumentationRegistry.getTargetContext();
mDb = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
mUserDao = mDb.getUserDao();
}
@After
public void closeDb() throws IOException {
mDb.close();
}
@Test
public void writeUserAndReadInList() throws Exception {
User user = TestUtil.createUser(3);
user.setName("george");
mUserDao.insert(user);
List<User> byName = mUserDao.findUsersByName("george");
assertThat(byName.get(0), equalTo(user));
}
}
主机测试
Room 使用SQLite支持库,它提供与Android框架类中的那些接口匹配的接口。此支持允许您通过支持库的自定义实现来测试数据库查询。
注意:即使这个设置允许您的测试运行得很快,但不推荐使用,因为在您的设备和用户设备上运行的SQLite版本可能与主机上的版本不匹配。