小松的技术博客

六和敬

若今生迷局深陷,射影含沙。便许你来世袖手天下,一幕繁华。 你可愿转身落座,掌间朱砂,共我温酒煮茶。

Android 架构之美 - Room(上)

Room 是 Google 官方出品的 ORM(Object-relational mapping) 框架,当然市面上也有很多 ORM 框架,例如 GreenDaoOrmLiteLitepal 等。个人并没有了解过其它框架,因此也无法比较其优缺点,相对而言,Room 毕竟是官方出品,能够更好的与 LiveData 等框架结合使用,如果是新项目的话,建议使用。

引入

// 这里以 androidx 最新版为例
implementation 'androidx.room:room-runtime:2.1.0-alpha01'  
kapt 'androidx.room:room-compiler:2.1.0-alpha01'  

简单使用

Room 在 Google 的另一个框架 WorkManager 中得到使用,所以这里我就简单的以它为例来简单介绍下 Room 的使用。

Room 简单来说可以分为以下几个部分:

  1. Model
  2. DAO(Data Access Object)
  3. DataBase 类
  4. 入口类 Room

首先我们需要建立 Model 对象, 添加 @Entity 注解

// 用 @Index 来标示索引
@Entity(indices = {@Index(value {"schedule_requested_at"})})
public class WorkSpec {  
    // 用 @ColumnInfo 来标明数据库表的列名, 用 @PrimaryKey 来标示 主键
    @ColumnInfo(name = "id")
    @PrimaryKey
    @NonNull
    public String id;

    @ColumnInfo(name = "state")
    @NonNull
    public State state = ENQUEUED;

    @ColumnInfo(name = "worker_class_name")
    @NonNull
    public String workerClassName;

    // ...

    // 用 @Embedded 来聚合字段,这里 Constraints 的多个字段,在 数据库表里与 workerClassName 等字段平级
    @Embedded
    @NonNull
    public Constraints constraints = Constraints.NONE;

    //...

    public WorkSpec(@NonNull String id, @NonNull String workerClassName) {
        //...
    }

    public WorkSpec(@NonNull WorkSpec other) {
        //...
    }
}

// 通过 @ForeignKey 来指明外键, 以及在父 Model delete 与 update 时的行为(NO_ACTION, RESTRICT, SET_NULL, SET_DEFAULT, CASCADE)
@Entity(foreignKeys = {
        @ForeignKey(
                entity = WorkSpec.class,
                parentColumns = "id",
                childColumns = "work_spec_id",
                onDelete = ForeignKey.CASCADE,
                onUpdate = ForeignKey.CASCADE)},
        primaryKeys = {"tag", "work_spec_id"},
        indices = {@Index(value = {"work_spec_id"})})
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class WorkTag {

    @NonNull
    @ColumnInfo(name = "tag")
    public final String tag;

    @NonNull
    @ColumnInfo(name = "work_spec_id")
    public final String workSpecId;

    public WorkTag(@NonNull String tag, @NonNull String workSpecId) {
        this.tag = tag;
        this.workSpecId = workSpecId;
    }
}

一般而言,每个 Model 都会有其对应的 DAO 类,集成了所有对这个 Model 的操作,如 WorkSpec 对应的 WorkSpecDao, Room 的 DAO 类一般都声明为 interface, 然后加上 @Dao 注解,具体的实现类则由代码自动生成。

@Dao
public interface WorkSpecDao {  
    @Insert(onConflict = IGNORE)
    void insertWorkSpec(WorkSpec workSpec);

    // @Query 并不是指查询数据库,而是执行数据库语句
    @Query("DELETE FROM workspec WHERE id=:id")
    void delete(String id);

    @Query("SELECT * FROM workspec WHERE id=:id")
    WorkSpec getWorkSpec(String id);

    //...

    // 使用 @Transaction 标示使用 transition
    @Transaction
    @Query("SELECT id, state, output FROM workspec WHERE id=:id")
    WorkSpec.WorkStatusPojo getWorkStatusPojoForId(String id);

    // 可以返回 LiveData, 当数据变动后,重新执行查询,获取新数据
    @Transaction
    @Query("SELECT id, state, output FROM workspec WHERE id IN (:ids)")
    LiveData<List<WorkSpec.WorkStatusPojo>> getWorkStatusPojoLiveDataForIds(List<String> ids);
}

当准备好所有的 Model 和 DAO 后,我们就需要把它放入 DataBase 的管理中:

// 我们需要把所有的 model 对象 全都方式 @Database 的 entities 中,增删改 model 后,我们应该更新 version
// sqlite 只支持 NULL、INTEGER、REAL、TEXT、BLOB 这些类型,如果是 Date 或者自定义的枚举等类型,则需要声明 @TypeConverters 来做类型转换了
@Database(entities = {
        Dependency.class,
        WorkSpec.class,
        WorkTag.class,
        SystemIdInfo.class,
        WorkName.class},
        version = 4)
@TypeConverters(value = {Data.class, WorkTypeConverters.class})
public abstract class WorkDatabase extends RoomDatabase {

    // 获取 WorkSpecDao
    public abstract WorkSpecDao workSpecDao();

   // ...
}

剩下的就是如何使用这个 DataBase 类了,它是一个抽象类,我们真正需要的是由代码生成的子类,那如何获取呢?这个时候 Room 这个类就该出场了。也不得不感叹下,通过注解来做代码生成真好,一堆复杂可重复的东西都被隐藏在水下了。

Room 构造 DataBase 实例是通过 Builder 的方式来构建的,我们来看看 WorkDatabase 的构建:

public static WorkDatabase create(Context context, boolean useTestDatabase) {  
    RoomDatabase.Builder<WorkDatabase> builder;
    if (useTestDatabase) {
        // 可以通过 inMemoryDatabaseBuilder 来构建内存Db,可用于测试
        builder = Room.inMemoryDatabaseBuilder(context, WorkDatabase.class)
                .allowMainThreadQueries();
    } else {
        builder = Room.databaseBuilder(context, WorkDatabase.class, DB_NAME);
    }

    return builder.addCallback(generateCleanupCallback())
            .addMigrations(WorkDatabaseMigrations.MIGRATION_1_2)
            .addMigrations(
                    new WorkDatabaseMigrations.WorkMigration(context, VERSION_2, VERSION_3))
            .addMigrations(MIGRATION_3_4)
            .fallbackToDestructiveMigration()
            .build();
}

通过 builder, 我们可以添加 Callback,可以添加每个版本的升级降级策略, 可以启用 WAL 模式等。一般应用构建好 DataBase 应该以单例的形式存在于应用中。

DataBase 的实例化

实现我们看看 RoomDataBase$Builder 的 build 方法:

public static class Builder<T extends RoomDatabase> {

    public T build() {

        //...

        if (mQueryExecutor == null) {
            // 如果使用者没有提供 Executor,则使用框架默认的 IOThreadExecutor, 所以默认所有通过 DAO 执行的操作都会在子线程执行
            mQueryExecutor = ArchTaskExecutor.getIOThreadExecutor();
        }

        // Migration 相关

        if (mFactory == null) {
            mFactory = new FrameworkSQLiteOpenHelperFactory();
        }
        DatabaseConfiguration configuration =
        new DatabaseConfiguration(mContext, mName, mFactory, mMigrationContainer,
        mCallbacks, mAllowMainThreadQueries, mJournalMode.resolve(mContext),
        mQueryExecutor,
        mMultiInstanceInvalidation,
        mRequireMigration,
        mAllowDestructiveMigrationOnDowngrade, mMigrationsNotRequiredFrom);
        // 真正构造 DataBase 实例
        T db = Room.getGeneratedImplementation(mDatabaseClass, DB_IMPL_SUFFIX);
        // 初始化 DataBase
        db.init(configuration);
        return db;
    }
}

注解生成器会为我们生成一个带 _Impl 后缀的类,我们的 DB 名为 WorkDatabase,那么生成类就为 WorkDatabase_Impl。 所以真正构造实例时通过反射去构造的。

static <T, C> T getGeneratedImplementation(Class<C> klass, String suffix) {  
    final String fullPackage = klass.getPackage().getName();
    String name = klass.getCanonicalName();
    final String postPackageName = fullPackage.isEmpty()
    ? name
    : (name.substring(fullPackage.length() + 1));
    final String implName = postPackageName.replace('.', '_') + suffix;
    try {
        final Class<T> aClass = (Class<T>) Class.forName(
                fullPackage.isEmpty() ? implName : fullPackage + "." + implName);
        return aClass.newInstance();
    } catch (Exception e) {
        // 各种 rethrow
    }
}

至此,对于业务开发者而言,了解到此已经足够了,Room 已经将 sqlite 的大部分东西都隐藏起来了,但如果我们想写出更为准确和高效的东西,我们依旧需要继续升入,看看我们写的每一行代码具体都做了些什么,这个我们下一篇博文再详细介绍。

←微信← →支付宝 →