Android Architecture Components Room

Room

一个给 SQLite 用的 ORM。

在 android.arch.persistence 包及其子包中。

项目的 build.gradle ,注意这个新加的 maven 仓库地址。一定概率需要爱国爬墙。

allprojects {
    repositories {
        jcenter()
        maven { url 'https://maven.google.com' }
    }
}

app 模块的 build.gradle

// For Room, add:
implementation "android.arch.persistence.room:runtime:1.0.0"
annotationProcessor "android.arch.persistence.room:compiler:1.0.0"
// For testing Room migrations, add:
testImplementation "android.arch.persistence.room:testing:1.0.0"
// For Room RxJava support, add:
implementation "android.arch.persistence.room:rxjava2:1.0.0"

Entity 注解

@Entity
class User {
    @PrimaryKey
    private int id;
    @ColumnInfo(name = "name")
    private String name;
    private String gender;
    // getters and setters for fields
}

当一个类用 @Entity注解并且被 @Database 注解中的 entities 属性所引用,Room 就会在数据库中为那个 entity 创建一张表。

默认 Room 会为 entity 中定义的每一个 field 都创建一个列,使用 @Ignore 注解可以不被作为数据库的表的列

要持久化一个 field,Room 必须有获取它的渠道。要么把 field 写成 public,要么提供一个setter和getter。

Primary key

每个 entity 必须至少定义一个 field 作为主键(primary key)。即使是只有一个 field 时。

@PrimaryKey 的 autoGenerate 属性可以让 Room 为 entity 设置自增 ID 。

@Entity
public class User {
    @PrimaryKey(autoGenerate = true)
    private Integer id;
}

如果是组合主键,你可以使用 @Entity 注解的 primaryKeys 属性

@Entity(primaryKeys = {"firstName", "lastName"})
class User {
    public String firstName;
    public String lastName;
}

表名

Room 默认把类名作为数据库的表名。如果你想用其它的名称,使用 @Entity 注解的 tableName 属性 注意 SQLite 中的表名是大小写敏感的。

@Entity(tableName = "users")
class User {
}

列名

Room 默认把 field 名称作为数据库表的 column 名。如果你想让 column 使用其他名称,为 field 添加 @ColumnInfo 注解并使用 name 属性

@Entity
class User {
    @ColumnInfo(name = "first_name")
    public String firstName;
    ...
}

索引

要为一个 entity 添加索引,在 @Entity 注解中添加 indices 属性,列出你想放在索引或者 组合索引 中的字段。

@Entity(indices = {@Index("id"), @Index("firstName", "lastName")})
class User {
    @PrimaryKey
    public int id;
    public String firstName;
    public String lastName;
}

UNIQUE 约束

通过把 @Index 注解的 unique 属性设置为 true 来实现唯一性。 下面的代码防止了一个表中的两行数据出现firstName和lastName字段的值相同的情况:

@Entity(indices = {
        @Index(
            value = {"firstName", "lastName"},
            unique = true)
        })
class User {
    @PrimaryKey
    public int id;
    public String firstName;
    public String lastName;
}

关系

Room 禁止 entity 对象相互引用,但是允许定义 entity 之间的外键(Foreign Key)约束。

外键可以指定当被关联的 entity 更新时做什么操作。 例如,通过在 @ForeignKey 注解中包含 Delete = CASCADE 时,如果相应的 User 实例被删除,那么删除这个 User 下的所有 book。

@Entity(foreignKeys = @ForeignKey(entity = User.class,
                                  parentColumns = "id",
                                  childColumns = "userId"))
class Book {
    @PrimaryKey
    public int bookId;
    public String title;
    public int userId;
}

嵌套对象

使用 @Embedded 注解,把一个 Entity 中某个 对象 field 的变量分解为表的字段。 比如,我们的 User 类可以包含一个类型为 Address 的 field,Address 代表 street,city,state, 和 postCode 字段的组合。为了让这些组合的字段单独存放在这个表中,对 User 类中的 Address 字段使用 @Embedded 注解, 那么现在代表一个User对象的表就有了如下的字段::id,firstName,street,state,city, 以及post_code。

嵌套字段也可以包含其它的嵌套字段。 如果一个 entity 有多个嵌套字段是相同名字,你可以设置 prefix 属性保持每个字段的唯一性。Room 就会在嵌套对象中的每个字段名的前面添加上这个值。

class Address {
    public String city;
    @ColumnInfo(name = "post_code")
    public int postCode;
}
 
@Entity
class User {
    @PrimaryKey
    public int id;
    public String firstName;
    @Embedded
    public Address address;
}

Dao 注解

一个定义 Dao 操作的接口。Room 会在编译时生成这个类的实现。

@Dao
public interface UserDao {
    @Insert(onConflict = REPLACE)
    void save(User user);
    @Query("SELECT * FROM user WHERE id = :userId")
    LiveData<User> load(String userId);
}

Insert

将所有的参数(数据)在一次事务中插入数据库

如果 @Insert 方法只有一个参数,它可以返回一个 long,代表新插入元素的 rowId,如果参数是一个数组或者集合,那么应该返回 long[] 或者 List

@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);
}

Update

Update 是一个更新一系列 entity 的简便方法。它根据参数的主键作为更新的依据。 可以让这个方法返回一个 int 类型的值,表示更新影响的行数。

@Dao
public interface MyDao {
    @Update
    public void updateUsers(User... users);
}

Delete

删除一系列 entity。它使用参数的主键找到要删除的 entity。 可以让这个方法返回一个int类型的值,表示从数据库中被删除的行数。

@Dao
public interface MyDao {
    @Delete
    public void deleteUsers(User... users);
}

@Query

@Query 用来执行自定义的读写操作。每个 @Query 方法的 SQL 和返回值类型都会在编译时被检查。

  • 简单的查询

一个简单的查询,加载所有的 user。

@Dao
public interface MyDao {
    @Query("SELECT * FROM user")
    public User[] loadAllUsers();
}
  • 向 query 传递参数

:minAge 用方法的参数 minAge 匹配。

@Dao
public interface MyDao {
    @Query("SELECT * FROM user WHERE age > :minAge")
    public User[] loadAllUsersOlderThan(int minAge);
}
  • 传入参数集合
@Dao
public interface MyDao {
    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
    public List<NameTuple> loadUsersFromRegions(List<String> regions);
}
  • 返回字段的子集

如果只需要一个 entity 的部分字段。只要结果的字段可以和返回的对象匹配就可以开工。

public class NameTuple {
    @ColumnInfo(name="first_name")
    public String firstName;
    public String lastName;
}

在 query 方法中使用这个 POJO,这些值可以被映射到 NameTuple 类的 field 中。因此 Room 可以生成正确的代码。这些 POJO 也可以使用 @Embedded 注解。

@Dao
public interface MyDao {
    @Query("SELECT first_name, lastName FROM user")
    public List<NameTuple> loadFullName();
}
  • 可观察的查询

可以在 query 方法中使用 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);
}
  • RxJava

Room 可以让查询返回 RxJava2 的 Publisher 和 Flowable 对象。要使用这个功能,在 Gradle dependencies中添加android.arch.persistence.room:rxjava2。

@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);
}
  • 联表查询

支持联表查询,并且址可观察特性的数据类型,比如 Flowable 或者 LiveData。

@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();
  
   static class UserPet {
       public String userName;
       public String petName;
   }
}

Database 注解和 RoomDatabase

Database 注解的这个类是一个继承 RoomDatabase 的抽象类。通过声明返回值是 Dao 的方法,提供这些 Dao 的使用。 Room 根据它自动提供一个实现。 在运行时,可以通过调用 Room.databaseBuilder() 或者 Room.inMemoryDatabaseBuilder() 来得到它的实例。

@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
  public abstract UserDao userDao();
}

获取实例

AppDatabase db = Room.databaseBuilder(getApplicationContext(), MyDatabase.class, "database-name").build();

类型转换器

使把一个自定义的数据类型,转换成数据库表的 字段

例如 一个Date的实例 转换成 Unix timestamp,通过编写如下的 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();
    }
}

然后将 @TypeConverters 注解添加到 AppDatabase 类中, Room 会自动转换这些自定义的类型,也可以限制 @TypeConverter 的作用范围。

@Database(entities = {User.java}, version = 1)
@TypeConverters({Converter.class})
public abstract class AppDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}

数据库迁移

随着 app 功能更新,你需要修改 entity 类来反应这些变化。通过定义 Migration 类来完成升级。当用户更新了 app 的最新版本之后,保证数据可用。

每个 Migration 类指定 from 和 to 版本。运行时 Room 运行每个 Migration 类的 migrate() 方法。

如果没有提供必要的 migration,Room 将清空和重建数据库。

为了让迁移的逻辑是可预知的,使用完整的查询而不用引用代表查询的 constant。

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");
    }
};