Room是一个对象关系映射(ORM)库。Room抽象了SQLite的使用,可以在充分利用SQLite的同时访问流畅的数据库。
Room官方文档介绍 https://developer.android.com/training/data-storage/room/
Room由三个重要的组件组成:Database、Entity、DAO。
1. 必须是abstract类而且的extends RoomDatabase。
2. 必须在类头的注释中包含与数据库关联的实体列表(Entity对应的类)。
3. 包含一个具有0个参数的抽象方法,并返回用@Dao注解的类。
在运行时,你可以通过Room.databaseBuilder() 或者 Room.inMemoryDatabaseBuilder()获取Database实例。
Entity:代表数据库中某个表的实体类。
DAO:包含用于访问数据库的方法。
Room的使用也是需要添加依赖的
dependencies {
...
// add for room
implementation "android.arch.persistence.room:runtime:1.1.1"
// room 配合 RxJava
implementation "android.arch.persistence.room:rxjava2:1.1.1"
annotationProcessor 'android.arch.persistence.room:compiler:1.1.1'
}
annotationProcessor 用于编译期间根据注解(Annotation)获取相关数据。
每个Entity代表数据库中某个表的实体类。默认情况下Room会把Entity里面所有的字段对应到表上的每一列。如果需要制定某个字段不作为表中的一列需要添加@Ignore注解。
@Entity
public class User {
@PrimaryKey
public int id;
public String firstName;
public String lastName;
@Ignore
Bitmap picture;
}
比如上面的picture字段代码因为使用了@Ignore所以该字段不会映射到User表中。
Entity的实体类都需要添加@Entity注解。而且Entity类中需要映射到表中的字段需要保证外部能访问到这些字段(你要么把字段什么为public、要不实现字段的getter和setter方法)。
@Entity注解包含的属性有:
默认情况下Entity类的名字就是表的名字(不区分大小写)。但是我们也可以通过@Entity的tableName属性来自定义表名字。如下代码所示users表对应的实体类。
@Entity(tableName = "users")
public class User {
...
}
默认情况下Entity类中字段的名字就是表中列的名字。我们也是可以通过@ColumnInfo注解来自定义表中列的名字。如下代码users表中first_name列对应firstName字段,last_name列对应lastName字段。
@Entity(tableName = "users")
public class User {
@PrimaryKey
public int id;
@ColumnInfo(name = "first_name")
public String firstName;
@ColumnInfo(name = "last_name")
public String lastName;
}
每个Entity都需要至少一个字段设置为主键。即使这个Entity只有一个字段也需要设置为主键。Entity设置主键的方式有两种
@Entity(primaryKeys = {"firstName",
"lastName"})
public class User {
public String firstName;
public String lastName;
}
@Entity
public class User {
@PrimaryKey
public String firstName;
@PrimaryKey
public String lastName;
}
如果你希望Room给entity设置一个自增的字段,可以设置@PrimaryKey的autoGenerate属性。
数据库索引用于提高数据库表的数据访问速度的。数据库里面的索引有单列索引和组合索引。Room里面可以通过@Entity的indices属性来给表格添加索引。
@Entity(indices = {@Index("firstName"),
@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的unique数学来设置是否唯一索引。
@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;
}
因为SQLite是关系形数据库,表和表之间是有关系的。这也就是我们数据库中常说的外键约束(FOREIGN KEY约束)。Room里面可以通过@Entity的foreignKeys属性来设置外键。我们用一个具体的例子来说明。
正常情况下,数据库里面的外键约束。子表外键于父表。当父表中某条记录子表有依赖的时候父表这条记录是不能删除的,删除会报错。一般大型的项目很少会采用外键的形式。一般都会通过程序依赖业务逻辑来保证的。
@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;
}
@Entity(foreignKeys = @ForeignKey(entity = User.class,
parentColumns = "id",
childColumns = "user_id"))
public class Book {
@PrimaryKey
public int bookId;
public String title;
@ColumnInfo(name = "user_id")
public int userId;
}
上述代码通过foreignKeys之后Book表中的userId来源于User表中的id。
@ForeignKey属性介绍:
onDelete:默认NO_ACTION,当parent里面有删除操作的时候,child表可以做的Action动作有:
1. NO_ACTION:当parent中的key有变化的时候child不做任何动作。
2. RESTRICT:当parent中的key有依赖的时候禁止对parent做动作,做动作就会报错。
3. SET_NULL:当paren中的key有变化的时候child中依赖的key会设置为NULL。
4. SET_DEFAULT:当parent中的key有变化的时候child中依赖的key会设置为默认值。
5. CASCADE:当parent中的key有变化的时候child中依赖的key会跟着变化。
onUpdate:默认NO_ACTION,当parent里面有更新操作的时候,child表需要做的动作。Action动作方式和onDelete是一样的。
deferred:默认值false,在事务完成之前,是否应该推迟外键约束。这个怎么理解,当我们启动一个事务插入很多数据的时候,事务还没完成之前。当parent引起key变化的时候。可以设置deferred为ture。让key立即改变。
有些情况下,你会需要将多个对象组合成一个对象。对象和对象之间是有嵌套关系的。Room中你就可以使用@Embedded注解来表示嵌入。然后,您可以像查看其他单个列一样查询嵌入字段。比如有一个这样的例子,User表包含的列有:id, firstName, street, state, city, and post_code。这个时候我们的嵌套关系可以用如下代码来表示。
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;
}
@Embedded注解属性:
这个组件代表了作为DAO的类或者接口。DAO是Room的主要组件,负责定义访问数据库的方法。Room使用过程中一般使用抽象DAO类来定义数据库的CRUD操作。DAO可以是一个接口也可以是一个抽象类。如果它是一个抽象类,它可以有一个构造函数,它将RoomDatabase作为其唯一参数。Room在编译时创建每个DAO实。
DAO里面所有的操作都是依赖方法来实现的。
当DAO里面的某个方法添加了@Insert注解。Room会生成一个实现,将所有参数插入到数据库中的一个单个事务。
@Insert注解可以设置一个属性:
1. OnConflictStrategy.REPLACE:冲突策略是取代旧数据同时继续事务。
2. OnConflictStrategy.ROLLBACK:冲突策略是回滚事务。
3. OnConflictStrategy.ABORT:冲突策略是终止事务。
4. OnConflictStrategy.FAIL:冲突策略是事务失败。
5. OnConflictStrategy.IGNORE:冲突策略是忽略冲突。
一个简单的实例如下:
@Dao
public interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertUsers(User... users);
}
当@Insert注解的方法只有一个参数的时候,这个方法也可以返回一个long,当@Insert注解的方法有多个参数的时候则可以返回long[]或者r List。long都是表示插入的rowId。
当DAO里面的某个方法添加了@Update注解。Room会把对应的参数信息更新到数据库里面去(会根据参数里面的primary key做更新操作)。
@Update和@Insert一样也是可以设置onConflict来表明冲突的时候的解决办法。
@Dao
public interface UserDao {
@Update(onConflict = OnConflictStrategy.REPLACE)
int updateUsers(User... users);
}
@Update注解的方法也可以返回int变量。表示更新了多少行。
当DAO里面的某个方法添加了@Delete注解。Room会把对应的参数信息指定的行删除掉(通过参数里面的primary key找到要删除的行)。
@Delete也是可以设置onConflict来表明冲突的时候的解决办法。
@Dao
public interface UserDao {
@Delete
void deleteUsers(User... users);
}
@Delete对应的方法也是可以设置int返回值来表示删除了多少行。
@Query注解是DAO类中使用的主要注释。它允许您对数据库执行读/写操作。@Query在编译的时候会验证准确性,所以如果查询出现问题在编译的时候就会报错。
Room还会验证查询的返回值,如果返回对象中的字段名称与查询响应中的相应列名称不匹配的时候,Room会通过以下两种方式之一提醒您:
@Query注解value参数:查询语句,这也是我们查询操作最关键的部分。
查询所有的信息。
@Dao
public interface UserDao {
@Query("SELECT * FROM user")
User[] loadAllUsers();
}
返回结果可以是数组,也可以是List。
大多数情况下我们都需要查询满足特定的查询条件的信息。
@Dao
public interface UserDao {
@Query("SELECT * FROM user WHERE firstName == :name")
User[] loadAllUsersByFirstName(String name);
}
查询需要多个参数的情况
@Dao
public interface UserDao {
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
User[] loadAllUsersBetweenAges(int minAge, int maxAge);
@Query("SELECT * FROM user WHERE firstName LIKE :search " + "OR lastName LIKE :search")
List findUserWithName(String search);
}
有的时候可能指向返回某些特定的列信息。
下来的例子只查询user表中的firstName和lastName信息。
@Entity
public class User {
@PrimaryKey
public String firstName;
@PrimaryKey
public String lastName;
public int age;
}
public class NameTuple {
private String firstName;
private String lastName;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
@Dao
public interface UserDao {
@Query("SELECT firstName, lastName FROM user")
List loadFullName();
}
在查询的时候您可能需要传递一组(数组或者List)参数进去。
@Dao
public interface UserDao {
@Query("SELECT firstName, lastName FROM user WHERE region IN (:regions)")
public List loadUsersFromRegions(List regions);
}
意思就是查询到结果的时候,UI能够自动更新。Room为了实现这一效果,查询的返回值的类型为LiveData。
@Dao
public interface UserDao {
@Query("SELECT firstName, lastName FROM user WHERE region IN (:regions)")
LiveData> loadUsersFromRegionsSync(List regions);
}
关于LiveData的具体用法,我们这里就不做过多的讨论了。
Room的查询也可以返回RxJava2的Publisher或者Flowable对象。当然了想要使用这一功能需要在build.gradle文件添加 implementation “android.arch.persistence.room:rxjava2:1.1.1” 依赖。
@Dao
public interface UserDao {
@Query("SELECT * from user")
Flowable> loadUser();
}
拿到Flowable
mAppDatabase.userDao()
.loadUser()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer>() {
@Override
public void accept(List entities) {
}
});
查询结果直接返回cursor。然后通过cursor去获取具体的结果信息。
@Dao
public interface UserDao {
@Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
Cursor loadRawUsersOlderThan(int minAge);
}
关于怎么从Cursor里面去获取结果,大家肯定都非常熟悉。
有的时候可能需要通过多个表才能获取查询结果。这个就涉及到数据的多表查询语句了。
@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 findBooksBorrowedByNameSync(String userName);
}
也可以查询指定的某些列。
@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> 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;
}
}
@Database注解可以用来创建数据库的持有者。该注解定义了实体列表,该类的内容定义了数据库中的DAO列表。这也是访问底层连接的主要入口点。注解类应该是抽象的并且扩展自RoomDatabase。
Database对应的对象(RoomDatabase)必须添加@Database注解,@Database包含的属性:
在运行时,你可以通过调用Room.databaseBuilder()或者Room.inMemoryDatabaseBuilder()获取实例。因为每次创建Database实例都会产生比较大的开销,所以应该将Database设计成单例的,或者直接放在Application中创建。
两种方式获取Database对象的区别:
- Room.databaseBuilder():生成Database对象,并且创建一个存在文件系统中的数据库。
- Room.inMemoryDatabaseBuilder():生成Database对象并且创建一个存在内存中的数据库。当应用退出的时候(应用进程关闭)数据库也消失。
我们用一个简单的实例来说明Database的创建。先定义一个abstract类AppDatabase继承RoomDatabase。
@Database(entities = {User.class, Book.class}, version = 3)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
public abstract BookDao bookDao();
}
创建RoomDatabase实例(AppDatabase)。这里我们把RoomDatabase实例的创建放在Application里面。
public class AppApplication extends Application {
private AppDatabase mAppDatabase;
@Override
public void onCreate() {
super.onCreate();
mAppDatabase = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "android_room_dev.db")
.allowMainThreadQueries()
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build();
}
public AppDatabase getAppDatabase() {
return mAppDatabase;
}
/**
* 数据库版本 1->2 user表格新增了age列
*/
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE User ADD COLUMN age integer");
}
};
/**
* 数据库版本 2->3 新增book表格
*/
static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL(
"CREATE TABLE IF NOT EXISTS `book` (`uid` INTEGER PRIMARY KEY autoincrement, `name` TEXT , `userId` INTEGER, 'time' INTEGER)");
}
};
}
创建RoomDatabase实例的时候,RoomDatabase.Builder类里面主要方法的介绍:
/**
* 默认值是FrameworkSQLiteOpenHelperFactory,设置数据库的factory。比如我们想改变数据库的存储路径可以通过这个函数来实现
*/
public RoomDatabase.Builder openHelperFactory(@Nullable SupportSQLiteOpenHelper.Factory factory);
/**
* 设置数据库升级(迁移)的逻辑
*/
public RoomDatabase.Builder addMigrations(@NonNull Migration... migrations);
/**
* 设置是否允许在主线程做查询操作
*/
public RoomDatabase.Builder allowMainThreadQueries();
/**
* 设置数据库的日志模式
*/
public RoomDatabase.Builder setJournalMode(@NonNull JournalMode journalMode);
/**
* 设置迁移数据库如果发生错误,将会重新创建数据库,而不是发生崩溃
*/
public RoomDatabase.Builder fallbackToDestructiveMigration();
/**
* 设置从某个版本开始迁移数据库如果发生错误,将会重新创建数据库,而不是发生崩溃
*/
public RoomDatabase.Builder fallbackToDestructiveMigrationFrom(int... startVersions);
/**
* 监听数据库,创建和打开的操作
*/
public RoomDatabase.Builder addCallback(@NonNull RoomDatabase.Callback callback);
RoomDatabase除了必须添加@Database注解也可以添加@TypeConverter注解。用于提供一个把自定义类转化为一个Room能够持久化的已知类型的,比如我们想持久化日期的实例,我们可以用如下代码写一个TypeConverter去存储相等的Unix时间戳在数据库中。
public class Converters {
@TypeConverter
public static Date fromTimestamp(Long value) {
return value == null ? null : new Date(value);
}
@TypeConverter
public static Long dateToTimestamp(Date date) {
return date == null ? null : date.getTime();
}
}
大部分情况下设计的数据库在版本的迭代过程中经常是会有变化的。比如突然某个表需要新加一个字段,需要新增一个表。这个时候我们又不想失去之前的数据。Room里面以Migration类的形式提供可一个简化SQLite迁移的抽象层。Migration提供了从一个版本到另一个版本迁移的时候应该执行的操作。
当数据库里面表有变化的时候(不管你是新增了表,还是改变了某个表)有如下几个场景。
mAppDatabase = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "android_room_dev.db")
.allowMainThreadQueries()
.fallbackToDestructiveMigration()
.build();
所以在数据库有变化的时候,我们任何时候都应该尽量提供Migrating。Migrating让我们可以自己去处理数据库从某个版本过渡到另一个版本的逻辑。我们用一个简单的实例来说明。有这么个情况,数据库开始设计的时候我们就一个user表(数据库版本 1),第一次变化来了我们需要给user表增加一个age的列(数据库版本 2),过了一段时间又有变化了我们需要新增加一个book表。三个过程版本1->2->3。
数据库版本为1的时候的代码
@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
public class AppApplication extends Application {
private AppDatabase mAppDatabase;
@Override
public void onCreate() {
super.onCreate();
mAppDatabase = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "android_room_dev.db")
.allowMainThreadQueries()
.build();
}
public AppDatabase getAppDatabase() {
return mAppDatabase;
}
}
数据库版本为2的时候的代码,User增加了age列
@Entity
public class User {
@PrimaryKey(autoGenerate = true)
private long uid;
private String name;
private String address;
private String phone;
private Integer age;
public long getUid() {
return uid;
}
public void setUid(long uid) {
this.uid = uid;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
@Database(entities = {User.class}, version = 2)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
public class AppApplication extends Application {
private AppDatabase mAppDatabase;
@Override
public void onCreate() {
super.onCreate();
mAppDatabase = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "android_room_dev.db")
.allowMainThreadQueries()
.addMigrations(MIGRATION_1_2)
.build();
}
public AppDatabase getAppDatabase() {
return mAppDatabase;
}
/**
* 数据库版本 1->2 user表格新增了age列
*/
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE user " + " ADD COLUMN age INTEGER");
}
};
}
数据库版本为3的时候的代码,新增了一个Book表
@Entity
public class Book {
@PrimaryKey(autoGenerate = true)
private Long uid;
private String name;
private Date time;
private Long userId;
public Long getUid() {
return uid;
}
public void setUid(Long uid) {
this.uid = uid;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getTime() {
return time;
}
public void setTime(Date time) {
this.time = time;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
}
@Database(entities = {User.class, Book.class}, version = 3)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
public abstract BookDao bookDao();
}
public class AppApplication extends Application {
private AppDatabase mAppDatabase;
@Override
public void onCreate() {
super.onCreate();
mAppDatabase = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "android_room_dev.db")
.allowMainThreadQueries()
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build();
}
public AppDatabase getAppDatabase() {
return mAppDatabase;
}
/**
* 数据库版本 1->2 user表格新增了age列
*/
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE User ADD COLUMN age integer");
}
};
/**
* 数据库版本 2->3 新增book表格
*/
static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL(
"CREATE TABLE IF NOT EXISTS `book` (`uid` INTEGER PRIMARY KEY autoincrement, `name` TEXT , `userId` INTEGER, 'time' INTEGER)");
}
};
}
Migrating使用过程中也有碰到一些坑,这里告诫大家一个经验Entity中能用Integer的时候不用int。
Room也允许你会将你数据库的表信息导出为一个json文件。你应该在版本控制系统中保存该文件,该文件代表了你的数据库表历史记录,这样允许Room创建旧版本的数据库用于测试。只需要在build.gradle文件中添加如下配置。编译的时候就会导出json文件。
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation":
"$projectDir/schemas".toString()]
}
}
}
// 用于测试
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
}
json文件文件会导出在工程目录下的schemas文件夹下面。里面会有各个版本数据库的信息。
本文涉及到的例子下载地址
以上对Room做了一个简单的介绍,如果大家使用Room的过程中有什么疑问,也可以留言。