Android 架構之美 - Room(上)
Room
是 Google 官方出品的 ORM(Object-relational mapping) 框架,當然市面上也有很多 ORM 框架,例如GreenDao
、OrmLite
、Litepal
等。個人並沒有瞭解過其它框架,因此也無法比較其優缺點,相對而言,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
簡單來說可以分為以下幾個部分:
- Model
- DAO(Data Access Object)
- DataBase 類
- 入口類 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 的大部分東西都隱藏起來了,但如果我們想寫出更為準確和高效的東西,我們依舊需要繼續升入,看看我們寫的每一行程式碼具體都做了些什麼,這個我們下一篇博文再詳細介紹。