Room
是一个持久化工具,和ormlite
、greenDao
类似,都是ORM
工具。在开发中我们可以利用Room
来操作sqlite
数据库。
使用原始的SQLite
可以提供这样的功能,但是有以下两个缺点:
-
没有编译时
SQL
语句的检查。尤其是当你的数据库表发生变化时,需要手动的更新相关代码,这会花费相当多的时间并且容易出错。 -
编写大量
SQL
语句和Java
对象之间相互转化的代码。 针对以上的缺点,Google
提供了Room
来解决这些问题。Room
包含以下三个重要组成部分: -
Database
:使用注解申明一个类,注解中包含若干个Entity
类,这个Database
类主要负责创建数据库以及获取数据对象的。 -
Entities
:表示每个数据库的总的一个表结构,同样也是使用注解表示,类中的每个字段都对应表中的一列。 -
DAO
:Data Access Object
的缩写,表示从从代码中直接访问数据库,屏蔽sql
语句。
其实这和传统写数据库创建访问的代码大概形式差不多的。以存储User
信息为例,看一下下面的代码:
@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.
//Getters和setters为了简单起见就省略了,但是对Room来说是必须的
}
@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);
}
@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();
下面详细介绍提到的各个部分:
@Entity
如果上面的User
类中包含一个字段是不希望存放到数据库中的,那么可以用@Ignore
注解这个字段:
@Entity
class User {
@PrimaryKey
public int id;
public String firstName;
public String lastName;
//不需要被存放到数据库中
@Ignore
Bitmap picture;
}
Room
持久化一个类的field
必须要求这个field
是可以访问的。可以把这个field
设为public
或者设置setter
和getter
。
每个Entity
都必须定义一个field
为主键,即使是这个Entity
只有一个field
。如果想要Room
生成自动的primary key
,可以使用@PrimaryKey
的autoGenerate
属性。如果Entity
的primary key
是多个Field
的复合Key
,可以向下面这样设置:
@Entity(primaryKeys = {"firstName", "lastName"})
class User {
public String firstName;
public String lastName;
@Ignore
Bitmap picture;
}
在默认情况下Room
使用类名作为数据库表的名称。如果想要设置不同的名称,可以参考下面的代码,设置表名tableName
为users
:
@Entity(tableName = "users")
class User {
...
}
和设置tableName
相似,Room
默认使用field
的名称作为表的列名。如果想要使用不同的名称,可以通过@ColumnInfo(name = "first_name")
设置,代码如下:
@Entity(tableName = "users")
class User {
@PrimaryKey
public int id;
@ColumnInfo(name = "first_name")
public String firstName;
@ColumnInfo(name = "last_name")
public String lastName;
@Ignore
Bitmap picture;
}
根据访问数据库的方式,你可能想对特定的field
建立索引来加速你的访问。下面这段代码展示了如何在Entity
中添加索引或者复合索引:
@Entity(indices = {@Index("name"),
@Index(value = {"last_name", "address"})})
class User {
@PrimaryKey
public int id;
public String firstName;
public String address;
@ColumnInfo(name = "last_name")
public String lastName;
@Ignore
Bitmap picture;
}
下面的代码展示了对数据库中特定field
设置唯一性(这个表中的firstName
和lastName
不能同时相同):
@Entity(indices = {@Index(value = {"first_name", "last_name"},
unique = true)})
class User {
@PrimaryKey
public int id;
@ColumnInfo(name = "first_name")
public String firstName;
@ColumnInfo(name = "last_name")
public String lastName;
@Ignore
Bitmap picture;
}
SQLite
是关系型数据库,那么就可以在两个对象之间建立联系。大多数ORM
库允许Entity
对象互相引用,但Room
明确禁止了这样做。原因
既然不允许建立直接的关系,Room
提供以外键的方式在两个Entity
之间建立联系。
例如,有一个Pet
类需要和User
类建立关系,可以通过@ForeignKey
来达到这个目的,代码如下:
@Entity(foreignKeys = @ForeignKey(entity = User.class,
parentColumns = "id",
childColumns = "user_id"))
class Pet {
@PrimaryKey
public int petId;
public String name;
@ColumnInfo(name = "user_id")
public int userId;
}
外键可以允许你定义被引用的Entity
更新时发生的行为。例如你可以定义当删除User
时对应的Pet
类也被删除。可以在@ForeignKey
中添加onDelete = CASCADE
实现。
@Insert(OnConflict = REPLACE)
定义了REMOVE和REPLACE而不是简单的UPDATE操作。这样产生的后果会影响外键定义的约束行为,详细的信息可以参考 SQLite documentation。
Entity
之间可能也有一对多之间的关系。比如一个User
有多个Pet
,通过一次查询获取多个关联的Pet
。
public class UserAndAllPets {
@Embedded
public User user;
@Relation(parentColumn = "id", entityColumn = "user_id")
public List<Pet> pets;
}
@Dao
public interface UserPetDao {
@Query("SELECT * from User")
public List<UserAndAllPets> loadUserAndPets();
}
使用 @Relation 注解的field必须是一个List或者一个Set。通常情况下, Entity 的类型是从返回类型中推断出来的,可以通过定义 entity()来定义特定的返回类型。 用 @Relation 注解的field必须是public或者有public的setter。这是因为加载数据是分为两步的:1. 父Entity被查询 2. 触发用 @Relation 注解的entity的查询。所以,在上面UserAndAllPets例子中,首先User所在的数据库被查询,然后触发查询Pets的查询。即Room首先出创建一个空的对象,然后设置父Entity和一个空的list。在第二次查询后,Room将会填充这个list。
有时候需要在类里面把另一个类作为field
,这时就需要使用@Embedded
。这样就可以像查询其他列一样查询这个field
。
例如,User
类可以包含一个field Address
,代表User
的地址包括所在街道、城市、州和邮编。代码如下:
class Address {
public String street;
public String state;
public String city;
@ColumnInfo(name = "post_code")
public int postCode;
}
@Entity
class User {
@PrimaryKey
public int id;
public String firstName;
@Embedded
public Address address;
}
在存放User
的表中,包含的列名如下:id,firstName,street,state,city,post_code
。
Embedded
的field
中也可以包含其他Embedded
的field
。
如果多个Embedded
的field
是类型相同的,可以通过设置prefix
来保证列的唯一性。
DAOs
是数据库访问的抽象层。
Dao
可以是一个接口也可以是一个抽象类。如果是抽象类,那么它可以接受一个RoomDatabase
作为构造器的唯一参数。
Room
不允许在主线程中防伪数据库,除非在builder
里面调用allowMainThreadQueries()
。因为访问数据库是耗时的,可能阻塞主线程,引起UI
卡顿。
添加方便使用的方法:
Insert
:使用@Insert
注解的方法,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
@Dao
public interface MyDao {
@Update
public void updateUsers(User... users);
}
也可以让update
方法返回一个int
型的整数,代表被update
的行号。
Delete
@Dao
public interface MyDao {
@Delete
public void deleteUsers(User... users);
}
和update
方法一样,也可以返回一个int
型的整数,代表被delete
的行号。
@Query
注解的方法在编译时就会被检查,如果有任何查询的问题,都会抛出编译异常,而不是等到运行以后才会发现异常。
Room
也会检查查询返回值的类型,如果返回类型的字段和数据路列名存在不一致,会收到警告。如果两者完全不一致,就会产生错误。
@Dao
public interface MyDao {
@Query("SELECT * FROM user")
public User[] loadAllUsers();
}
下面的代码显示了如何根据年龄条件查询User
信息:
@Dao
public interface MyDao {
@Query("SELECT * FROM user WHERE age > :minAge")
public User[] loadAllUsersOlderThan(int minAge);
}
同理,这里也会在编译时做类型检查,如果表中没有age
这个列,那么就会抛出错误。
也可以穿入多个参数或一个参数作为多个约束条件查询用户:
@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
的几个field
,例如只需要获取User
的姓名就行了。通过只获取这两列的数据不仅能够节省宝贵的资源,还能加快查询速度。
Room
也提供了这样的功能。
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();
}
通过和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);
}
Room
也可以返回RxJava2
中Publisher
和Flowable
格式的数据。如果需要使用这项功能,需要在Gradle
中添加android.arch.persistence.room:rxjava2
。
@Dao
public interface MyDao {
@Query("SELECT * from user where id = :id LIMIT 1")
public Flowable<User> loadUserById(int id);
}
@Dao
public interface MyDao {
@Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
public Cursor loadRawUsersOlderThan(int minAge);
}
有时可能需要查询多个表来获取结果,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;
}
}
如果想要在数据库中存储Date
,可以存储等价的Unix
时间戳。通过TypeConverter
可以很方便的做到这一点:
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();
}
}
这里定义了两个方法,将Date
和Unix
时间戳相互转换。Room
支持存储Long
类型的对象,这样就可以通过这种方法存储Date
。
接下来将TypeConverter
添加到AppDatabase
中,这样Room
就能识别这种转换:
@Database(entities = {User.class}, version = 1)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
接下来就可以像使用基本类型一样使用自定义类型的查询,比如:
@Database(entities = {User.class}, version = 1)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
@Dao
public interface UserDao {
...
@Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
List<User> findUsersBornBetweenDates(Date from, Date to);
}
随着业务的扩展有时候需要对数据库调整一些字段。当数据库升级时,需要保存已有的数据。
Room
使用Migration
来实现数据库的迁移。每个Migration
都指定了startVersion
和endVersion
。在运行的时候Room
运行每个Migration
的migrate()
方法,按正确的顺序来迁移数据库到下个版本。如果没有提供足够的迁移信息,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");
}
};
- 邮箱 :charon.chui@gmail.com
- Good Luck! `