Android Architecture Components 之 Room 篇

Room,一个 SQLite 的 ORM 库,能够方便地将 Java 对象转成 SQLite 的表数据,不用再像传统方式那样写 SQLite API 的样板代码了。同时 Room 提供了 SQLite 语法的编译时检查,而且能够返回 RxJava,Flowable 和 LiveData observables。html

添加依赖

// Room (use 1.1.0-beta2 for latest beta)
    implementation "android.arch.persistence.room:runtime:1.0.0"
    annotationProcessor "android.arch.persistence.room:compiler:1.0.0"
    // Test helpers for Room
    testImplementation "android.arch.persistence.room:testing:1.0.0"
复制代码

基本使用

Room 主要包含三个组件:java

  • Database: 包含数据库持有者,做为与应用持久化相关数据的底层链接的主要接入点。这个类须要用 @Database 注解,并知足下面条件:
    • 必须是继承 RoomDatabase 的抽象类
    • 注解中包含该数据库相关的实体类列表
    • 包含的抽象方法不能有参数,且返回值必须是被 @Dao 注解的类
  • Entity: 表示了数据库中的一张表
  • DAO: 包含了访问数据库的一系列方法

它们与应用程序的关系如图所示:
android

room_architecture

@Entity(tableName = "products")
public class ProductEntity {

    @PrimaryKey
    private int id;
    private String name;
    private String description;
    ...
}
复制代码
@Dao
public interface ProductDao {

    @Query("select * from products")
    List<ProductEntity> getAllProducts();

    @Query("select * from products where id = :id")
    ProductEntity findProductById(int id);

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    void insertProduct(ProductEntity product);

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    void insertAllProducts(List<ProductEntity> products);

    @Delete
    void deleteProduct(ProductEntity product);
}
复制代码
@Database(entities = {ProductEntity.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
    public abstract ProductDao productDao();
}
复制代码
AppDatabase appDatabase = Room.databaseBuilder(this, AppDatabase.class, "product.db").build();
    ProductDao productDao = appDatabase.productDao();
    ...
    List<ProductEntity> allProducts = productDao.getAllProducts();
    ...
    productDao.insertProduct(productEntity);
复制代码

每一个 entity 都表明了一张表,其中的字段表明表中的一列。注解处理器会自动生成 AppDatabaseProductDao 对应的实现类 AppDatabase_ImplProductDao_Impl。能够经过调用Room.databaseBuilder()Room.inMemoryDatabaseBuilder()在运行时获取Database实例,但要注意,实例化 RoomDatabase 是至关昂贵的,最好按照单例模式只建立一个Database实例。git

定义 Entity

为了让 Room 能够访问 entity,entity 中的字段必须是 public 的,或者提供了getter/setter方法。默认状况下,Room 会将 entity 中的每一个字段做为数据库表中一列,若是你不想持久化某个字段,可使用 @Ignore 注解。默认数据库表名为 entity 类名,你能够经过 @Entity 注解的 tableName 属性 更改,默认列名是字段名,你能够经过 @ColumnInfo 注解更改。github

主键

每一个 entity 必须至少有一个字段做为主键(primary key),即便该 entity 只有一个字段。使用 @PrimaryKey 注解来指定主键,若是你但愿 SQLite 帮你自动生成这个惟一主键,须要将 @PrimaryKeyautoGenerate 属性设置成 true,不过须要改列是 INTEGER 类型的。若是字段类型是 longintInsert 方法会将 0 做为缺省值,若是字段类型是 IntegerLong 类型,Insert 方法会将 null 做为缺省值。
若是 entity 的主键是复合主键(composite primary key),你就须要使用 @Entity 注解的 primaryKeys 属性定义这个约束,如:数据库

@Entity(primaryKeys = {"firstName", "lastName"})
class User {
    public String firstName;
    public String lastName;
    public String avatar;
}
复制代码

索引

有些时候,咱们须要添加索引以加快查询速度,可使用 @Entity 注解的 indices 属性建立索引,若是某个字段或字段组是惟一的,能够将 @Index 注解的 unique 属性设置为 true 来强制这个惟一性,如:数组

@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 库容许实体类对象间相互引用,但 Room 明确禁止这样作。由于级联查询不能发生在 UI 线程,UI 线程只有 16 ms 时间计算和绘制布局,因此即便一个查询只花费 5 ms,你的应用仍可能所以绘制超时,形成明显的视觉问题。并且若是此时还有其余的数据库事务正在运行或者设备正在运行其余磁盘敏感任务,那么该查询将花费更多的时间。而若是你不使用懒加载,你的应用将不得不去获取比所须要的更多的数据,从而产生内存占用问题。
ORM 库一般把这个决定权交给开发者,以便开发者根据本身应用的状况采起措施,而开发者一般会决定在应用和 UI 之间共享 model,然而,这种解决方案并不能很好地扩展,由于随着UI的变化,共享 model 会产生一些难以让开发人员预测和调试的问题。
例如,UI 加载了 Book 对象列表,每一个 book 都有一个 Author 对象,你可能最开始想采用懒加载的方式获取 Book实例(使用getAuthor() 方法获取 author),第一次调用 getAuthor() 会调用数据库查询。过一会,你意识到你须要在 UI 上显示做者名,你写了下面这样的代码:服务器

authorNameTextView.setText(user.getAuthor().getName());
复制代码

这看似正常的变动会致使 Author 表在主线程中被查询。那提早查询好做者信息是否是就好了呢?明显不行,若是你再也不须要这些数据,就很难改变数据的加载方式了。例如,若是你的 UI 再也不须要显示做者信息了,你的应用仍然会加载这些不须要的数据,从而浪费昂贵的内存空间,若是 Author 又引用了其余表,那么应用的效率将会进一步下降。
因此为了让 Room 能同时引用多个 entity,你须要建立一个包含每一个 entity 的 POJO,而后编写一个链接相应表的查询。这个结构良好的 model,结合 Room 健壮的查询校验功能,就可以让你的应用花费更少的资源加载数据,提高应用的性能和用户体验。
虽然不能直接指定对象间关系,但能够指定外键(Foreign Key)约束。例如对于 Book entity 有一个做者的外键引用 User,能够经过 @ForeignKey 注解指定这个外键约束:app

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

    public String title;

    @ColumnInfo(name = "user_id")
    public int userId;
}
复制代码

能够经过 @ForeignKey注解的 onDeleteonUpdate 属性指定级联操做,如级联更新和级联删除:ide

@Entity(foreignKeys = @ForeignKey(entity = User.class,
                                  parentColumns = "id",
                                  childColumns = "user_id",
                                  onUpdate = ForeignKey.CASCADE,
                                  onDelete = ForeignKey.CASCADE))
复制代码

有时,一个包含嵌套对象的 entity 或 POJO 表示一个完整的数据库逻辑,可使用 @Embedded 注解将该嵌套对象的字段分解到该表中,如 User 表须要包含 Address相关字段,可使用 @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 表包含 idfirstNamestreetstatecity,和 post_code 列。
Embedded 字段也能包含其余 Embedded 字段。
若是有另外一个组合列也是 Address 类型的,可使用 @Embedded 注解的 prefix 属性添加列名前缀以保证列的惟一性。

使用 DAO

DAO(data access objects)是应用中操做数据库的最直接的接口,应用中对数据库的操做都表如今这个对象上,也就是说,应用不须要知道具体的数据库操做方法,只须要利用 DAO 完成数据库操做就好了,因此这一系列 Dao 对象也构成了 Room 的核心组件。DAO 能够是个接口,也能够是个抽象类,若是是个抽象类,那么它能够有个构造器,以 RoomDatabase 做为惟一参数,Room 会在编译时自动生成每一个 DAO 的实现类。

新增

定义一个用 @Insert 注解的 DAO 方法,Room 会自动生成一个在单个事务中将全部参数插入数据库的实现,若是方法只有一个参数,那么它能够返回 long 类型的 rowId,若是方法参数是数组或集合,那么它能够返回 long[]List<Long>:

@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 注解的方法能够更改一系列给定的 entity, 使用匹配的主键去查询更改这些 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 注解的方法可让你方便地读写数据库,Room 会在编译时验证这个方法,因此若是查询有问题编译时就会报错。Room 还会验证查询的返回值,若是查询响应的字段名和返回对象的字段名不匹配,若是有些字段不匹配,你会看到警告,若是全部字段都不匹配,你会看到 error。下面是一个简单的查询,查询全部的用户:

@Dao
public interface MyDao {
    @Query("SELECT * FROM user")
    public User[] loadAllUsers();
}
复制代码

若是你想要添加查询条件,可使用 :参数名 的方式获取参数值:

@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);
}
复制代码

固然,查询条件集合也是支持的:

@Dao
public interface MyDao {
    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
    public List<NameTuple> loadUsersFromRegions(List<String> regions);
}
复制代码

不少时候,咱们不须要查询表中的全部字段,咱们只用到了 UI 用到的那几列,为了节省资源,也为了加快查询速度,咱们就能够定义一个包含用到的字段的 POJO(这个 POJO 可使用 @Embedded 注解) ,查询方法可使用这个 POJO:

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();
}
复制代码

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;
   }
}
复制代码

查询方法的返回值能够是 LiveData 以便你能随着数据库的更新实时更新 UI,返回值也能够是 RxJava2PublisherFlowable(须要添加 android.arch.persistence.room:rxjava2 依赖),甚至能够是 Cursor(不建议直接使用 Cursor API )。

数据库的更新与迁移

随着应用功能的改变,你须要去更改 entity 和数据库,但不少时候,你不但愿所以丢失数据库中已存在的的数据,尤为是没法从远程服务器恢复这些数据时。也就是说,若是你不提供必要的迁移操做,Room 将会重建数据库,数据库中全部的数据都将丢失。
为此, Room 容许你写一些 Migration 类去保护用户数据,每一个 Migration 类指定一个 startVersionendVersion,在运行时,Room 会运行每一个 Migration 类的 migrate() 方法,以正确的顺序将数据库迁移到最新版本:

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");
    }
};
复制代码

注意,为了保证迁移逻辑按预期运行,应该使用完整的查询而不是引用表示查询的常量。

在迁移过程完成后,Room 会验证 schema 以确保迁移正确的完成了,若是 Room 发现了问题,会抛出一个包含不匹配信息的异常。
迁移数据库是很重要也是没法避免的操做,若是迁移出错可能会致使你的应用陷入崩溃循环,为了保持应用的稳定性,你必须提早测试好迁移的整的过程。为了更好地测试,你须要添加 android.arch.persistence.room:testing 依赖,而且你须要导出数据库的 schema。在编译时,Room 会将你数据库的 schema 信息导出为 JSON 文件。为了导出 schema,你须要在 build.gradle 文件中设置 注解处理器属性room.schemaLocation:

android {
    ...
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = ["room.schemaLocation":
                             "$projectDir/schemas".toString()]
            }
        }
    }
}
复制代码

你须要将这个导出的 JSON 文件保存在版本控制系统中,由于这个文件表明了数据库的 schema 历史记录。同时你须要添加 schema 位置做为 asset 文件夹:

android {
    ...
    sourceSets {
        androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
    }
}
复制代码

测试工具中的 MigrationTestHelper 类能够读这些 schema 文件,同时它也实现了 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.
    }
}
复制代码

数据库的测试

写 JUnit 测试一般比 UI 测试更快更直观,利用 Room.inMemoryDatabaseBuilder 构造 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));
    }
}
复制代码

高级用法与技巧

TypeConverter

有些时候,咱们须要把一些自定义数据类型存入数据库,或者在存入数据库前作一些类型转换,如咱们须要把 Date 类型的字段做为 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();
    }
}
复制代码

而后使用 @TypeConverters 注解那些须要使用转换器的元素。若是注解了 Database,那么数据库中全部的 DaoEntity 都能使用它。若是注解了 Dao,那么 Dao 中全部的方法都能使用它。若是注解了 Entity,那么 Entity 中全部的字段都能使用它。若是注解了 POJO,那么 POJO 中全部的字段都能使用它。若是注解了 Entity 字段,那么只有这个 Entity 字段能使用它。若是注解了 Dao 方法,那么该 Dao 方法中全部的参数都能使用它。若是注解了 Dao 方法参数,那么只有这个参数能使用它:

@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);
}
复制代码

Database 对象的建立

实例化 RoomDatabase 是至关昂贵的,最好使用 Dagger2 等依赖注入工具注入惟一的 Database 实例,如:

@Module(includes = ViewModelModule.class)
class AppModule {
    ...
    @Singleton @Provides
    GithubDb provideDb(Application app) {
        return Room.databaseBuilder(app, GithubDb.class,"github.db").build();
    }

    @Singleton @Provides
    UserDao provideUserDao(GithubDb db) {
        return db.userDao();
    }

    @Singleton @Provides
    RepoDao provideRepoDao(GithubDb db) {
        return db.repoDao();
    }
}
复制代码

即便不使用依赖注入,也应该采用单例的方式建立 Database:

@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {

    private static volatile AppDatabase INSTANCE;

    public abstract UserDao userDao();

    public static AppDatabase getInstance(Context context) {
        if (INSTANCE == null) {
            synchronized (AppDatabase.class) {
                if (INSTANCE == null) {
                    INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
                            AppDatabase.class, "sample.db")
                            .build();
                }
            }
        }
        return INSTANCE;
    }

}
复制代码

线程切换

操做数据库是个很是耗时操做,因此不能在主线程(UI线程)中查询或更改数据库,Room 也为此作了线程检查,若是你在主线程中操做了数据库会直接抛出异常。为了方便,Room 还容许你在查询操做中直接返回 LiveDataRxJavaPublisherFlowable

参考

相关文章
相关标签/搜索