Android Room的详细介绍
Room是谷歌推出的做数据持久化保存的一个库。通过注释手段来实现一个抽象层,跟数据库打交道。官方推荐使用Room代替SQLite,当然如果你对SQLite情有独钟,也可以直接使用SQLite的APIs
先看下官方文档的定义:
Room provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite
Room提供一个关于SQLite的抽象层,以便在利用SQLite的全部功能的同时,能够流畅地访问数据库。
也就是说Room底层还是使用SQLite数据库,只是提供一套抽象层,更简洁,更优雅的方式去访问SQLite数据库。
Room有主要3个组件:
- DataBase:包含数据库持有者,并作为与应用程序持久的关系数据的底层连接的主要访问点。
用@database注释的类应该满足以下条件:
1. 作为一个扩展空间数据库的抽象类。
2. 在注释中包含与数据库相关联的实体列表。
3. 包含一个有0参数的抽象方法,并返回带有@dao注释的类。
- Entity:代表数据库中的表
- Dao:包含了访问数据库的各种方法
这些组件,以及它们与应用程序的其余部分的关系如图:
看一个简单的例子,下面代码段创建了一个简单的数据库配置了一个entity 和一个DAO
User.java
@Entity
public class User {
@PrimaryKey
private int uid;
@ColumnInfo(name = "first_name")
private String firstName;
@ColumnInfo(name = "last_name")
private String lastName;
// Getters and setters are ignored for brevity,
// but they're required for Room to work.
}
UserDao.java
@Dao
public interface UserDao {
@Query("SELECT * FROM user")
List<User> getAll();
@Query("SELECT * FROM user WHERE uid IN (:userIds)")
List<User> loadAllByIds(int[] userIds);
@Query("SELECT * FROM user WHERE first_name LIKE :first AND "
+ "last_name LIKE :last LIMIT 1")
User findByName(String first, String last);
@Insert
void insertAll(User... users);
@Delete
void delete(User user);
}
AppDatabase.java
@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
创建完这些文件后,使用下面代码可以创建一个数据库的实例。
AppDatabase db = Room.databaseBuilder(getApplicationContext(),
AppDatabase.class, "database-name").build();
笔记: 你应该使用一个单例模式当初始化一个AppDatabase对象。因为每个RoomDatabase实例都相当昂贵,而且你很少需要访问多个实例。
有了数据库实例后可以得到DAO对象,进行数据库操作。Room的基本使用大致就这样。下面详细介绍Room各个组件的的用法
引入Room的依赖
首先要添加Google Maven仓库,在project的在build.gradle添加
allprojects {
repositories {
jcenter()
google()
}
}
添加Room库的依赖,在module下的在build.gradle添加以下代码:
dependencies {
def room_version = "1.1.1"
implementation "android.arch.persistence.room:runtime:$room_version"
annotationProcessor "android.arch.persistence.room:compiler:$room_version"
// 可选 - Room的RxJava支持
implementation "android.arch.persistence.room:rxjava2:$room_version"
//可选 - Room的Guava支持,包含Optional and ListenableFuture
implementation "android.arch.persistence.room:guava:$room_version"
//测试帮助
testImplementation "android.arch.persistence.room:testing:$room_version"
}
Entity
Entity相当于对应数据库中的表,每一个Entity都会在数据库创建对应的表。默认时在Room中的Entity的每一个成员变量对应创建对应表中的一个列,如果某一成员不想做持久化保存,可以使用@Ignore注解它。同时你必须引用entity类通过在Database 类中的entities数据,如以上代码段中的AppDatabase.java。
以下代码段展示了如何声明一个entity
@Entity
public class User {
@PrimaryKey
public int id;
public String firstName;
public String lastName;
@Ignore
Bitmap picture;
}
为了保存一个成员,Room必须要要访问它,所以entity中的成员变量可以是public修饰,或者提供setter和getter方法。
笔记: entity可以有一个空的构造方法(要保证Dao类中有权限访问到每一个持久化的成员),也可以有一个参数的名字和类型跟持久成员匹配的构造方法。Room可以使用全部或者部分构造函数,比如一个构造函数只能接收受部分成员。
主键
每一个entity必须声明一个成员变量作为主键,使用@PrimaryKey注解成员来声明主键.
- 如果主键是自增id,可以使用@PrimaryKey的autoGenerate属性.
- 如果要声明复合主键,可以使用@Entity的primaryKeys属性。例如:
@Entity(primaryKeys = {"firstName", "lastName"})
public class User {
public String firstName;
public String lastName;
@Ignore
Bitmap picture;
}
表名和列名
默认情况下,Room使用类名作为表的名称。如果要使用其他名字作为表名,可以设置注解@Entity中的tableName的属性,如:
@Entity(tableName = "users")
public class User {
...
}
与表名类似,默认使用类成员的名称,表的列名可以使用 @ColumnInfo的name属性来设置
@Entity(tableName = "users")
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中的indices属性来建立索引,列出想要的字段作为索引或者复合索引。例如:
@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中的unique属性为true,如下:
@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是一个关系数据库,你可以指定对象之间的关系。尽管大多数对象-关系映射库允许实体对象相互参考,Room中明确禁止这个。原因可以看这个链接
即使你不能使用直接关系,Room允许定义外键来约束entity
比如有另外一个实体Book,可以使用@ForeignKey来声明与User之间的关系。如
@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;
}
外键是非常强大的,因为他们允许您指定时所引用的实体是什么更新。例如,你可以告诉SQLite删除用户所有的书如果相应的实例的用户包括onDelete = CASCADE在@ForeignKey注释
创建嵌套的对象
有时候,你想在entity中引用其他的对象,并且把该对象的成员作为自己的表的列,这时可以使用@Embedded来注解这个引用的对象。如:
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
笔记: Embedded 字段 也可以包含其他 embedded 字段
如果一个实体有多个相同类型的embedded 字段,你可以通过设置prefix 属性保持唯一的一列。
DAO(Data Access Object)
DAO是数据访问对象,包含了数据库访问的方法。访问数据库一般都会采用DAO类来代替直接查询的方式。
在Room中,一个DAO可以是一个接口或抽象类。如果它是一个抽象类,它可以有一个构造函数,该函数接收一个RoomDatabase作为其唯一的参数。每个DAO实现在编译时创建
笔记: Room不支持在主线程中访问数据库,除非你在builder中调用了allowMainThreadQueries()方法。因为数据库访问可能会阻塞UI线程一段时间,应该采用异步查询的方式
Insert
用@insert注解DAOd的方法,Room会自动生成实现的代码
@Dao
public interface MyDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
public void insertUsers(User... users);
@Insert
public void insertBothUsers(User user1, User user2);
@Insert
public void insertUsersAndFriends(User user, List<User> friends);
}
如果@insert注解的方法接受的参数只有一个,这个方法可以返回一个long类型的值,这个值是插入项的rowId,如果接受的是一个是数组或者集合,则返回long[]或者List
@Update
用update可以修改作为参数的entities的集合,在数据库中,它使用一个查询匹配对每个实体的主键。
例如:
@Dao
public interface MyDao {
@Update
public void updateUsers(User... users);
}
虽然通常没有必要,你可以有这个方法返回一个int值相反,表示在数据库中更新的行数
Delete
用delete可以移除作为参数的entitis的集合,在数据库中,它使用一个查询匹配对每个实体的主键。
@Dao
public interface MyDao {
@Delete
public void deleteUsers(User... users);
}
虽然通常没有必要,你可以有这个方法返回一个int值相反,表示在数据库中删除的行数
Query
@Query 是主要的注解使用在DAO类,允许你执行读写操作在一个数据库中。每一个@Query方法是在编译的时候验证的。所以如果查询有问题,编译错误就会发生而不是运行错误
Room 也可以验证查询的返回值例如返回字段的名字不能匹配相应的列名在一个查询的返回,Rooms就会警告你通过以下两种方式:
- 给了一个警告如果只有一些字段名称匹配
- 给了一个错误如果没有字段名称匹配
简单查询
@Dao
public interface MyDao {
@Query("SELECT * FROM user")
public User[] loadAllUsers();
}
这是一个非常简单的查询,加载所有用户。在编译时,Room知道查询用户表中的所有列。如果查询包含了一个语法错误,或者如果user表在数据库中不存在,Room会显示一个错误消息作为应用程序编译。
传递参数作为查询条件
大多数时候,需要传个参数作为查询条件,例如需要显示用户小于一个确定的年龄时,代码如下:
@Dao
public interface MyDao {
@Query("SELECT * FROM user WHERE age > :minAge")
public User[] loadAllUsersOlderThan(int minAge);
}
多个参数
@Dao
public interface MyDao {
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
public User[] loadAllUsersBetweenAges(int minAge, int maxAge);
@Query("SELECT * FROM user WHERE first_name LIKE :search "
+ "OR last_name LIKE :search")
public List<User> findUserWithName(String search);
}
返回列的子集
大多的时候,只需要entity中的几个字段。这样可以节省有效资源,还可以加快查询的速度。
例如显示User中的first_name和last_name,我们可以创建一个POJO(plain old Java-based object)来获取
first_name和last_name。
public class NameTuple {
@ColumnInfo(name="first_name")
public String firstName;
@ColumnInfo(name="last_name")
public String lastName;
}
然后可以使用这个POJO在查询方法中
@Dao
public interface MyDao {
@Query("SELECT first_name, last_name FROM user")
public List<NameTuple> loadFullName();
}
传递一个集合作为参数
可以将一个集合作为参数传到查询条件。比如
@Dao
public interface MyDao {
@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
public List<NameTuple> loadUsersFromRegions(List<String> regions);
}
可观察者的查询
当执行查询时,你可能想你的app’s UI自动更新当数据变化时。为了实现这个可以在返回类型使用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);
}
返回支持RxJava的查询
Room可以返回RxJava2的Publisher和Flowable对象作为查询条件,需要添加对应的依赖
@Dao
public interface MyDao {
@Query("SELECT * from user where id = :id LIMIT 1")
public Flowable<User> loadUserById(int id);
}
直接返回cursor
@Dao
public interface MyDao {
@Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
public Cursor loadRawUsersOlderThan(int minAge);
}
查询多个表
@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;
}
}
数据库的迁移
当你添加和改变功能时,你需要修改entities类,以反映这些变化。当一个用户更新到最新版本的应用程序,你不想让他们失去他们所有的现有数据,尤其是如果你不能恢复数据从远程服务器时。
Room允许你通过写Migration类来保护你的用户数据,每一个Migration类指定startVersion 和 endVersion, 运行时,Room运行每一个Migration 类中的 migrate() 方法,使用正确的顺序移值数据库到最新的版本。
警告: 如果不提供必要的migrations。Room将会重建数据库代替,意味着你将失去之前所有的数据库数据。
Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, "
+ "`name` TEXT, PRIMARY KEY(`id`))");
}
};
static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE Book "
+ " ADD COLUMN pub_year INTEGER");
}
};