Android Jetpack架構組件之 Room(使用、源碼篇)

1、前言

最近簡單看了下google推出的框架Jetpack,感覺此框架的內容可以對平時的開發有很大的幫助,也可以解決很多開發中的問題,對代碼的邏輯和UI界面實現深層解耦,打造數據驅動型UI界面。

Android Architecture組件是Android Jetpack的一部分,它們是一組庫,旨在幫助開發者設計健壯、可測試和可維護的應用程序,包含一下組件:

上述時Android Architecture所提供的架構組件,本文主要從使用和源碼的角度分析Room組件。

2、Room 簡介

Room是Google提供的一個ORM庫。Room提供了三個主要的組件:

  1. @Database:@Database用來註解類,並且註解的類必須是繼承自RoomDatabase的抽象類。該類主要作用是創建數據庫和創建Daos(data access objects,數據訪問對象)。
  2. @Entity:@Entity用來註解實體類,@Database通過entities屬性引用被@Entity註解的類,並利用該類的所有字段作爲表的列名來創建表。
  3. @Dao:@Dao用來註解一個接口或者抽象方法,該類的作用是提供訪問數據庫的方法。在使用@Database註解的類中必須定一個不帶參數的方法,這個方法返回使用@Dao註解的類

3、Room數據庫使用

數據庫的創建

  • 包含數據庫持有者,並作爲應用程序持久關係數據的基礎連接的主要訪問點,使用@Database註解,註解類應滿足以下條件:
  1. 數據庫必須是一個抽象類 RoomDatabase的擴展類
  2. 在註釋中包括與數據庫關聯的實體列表
  3. 必須包含一個具有0個參數且返回帶@Dao註釋的類的抽象方法
  4. 通過調用 Room.databaseBuilder()或 獲取實例Room.inMemoryDatabaseBuilder()創建數據庫實例
  5. 使用單例實例化數據庫對象
@Database(entities = {User.class}, version = 1)  // 註釋
public abstract class AppDatabase extends RoomDatabase {
    public abstract UserDao userDao();  // 抽象方法
}
  • 以單例形式對外提供RoomDataBase實例
public static UserDataBase getInstance(Context context) {
    if (userDataBase == null) {
        synchronized (UserDataBase.class) {
            if (userDataBase == null) {
                userDataBase = Room.databaseBuilder(context.getApplicationContext()
                        , UserDataBase.class, "user_data").build();
            }
        }
    }
    return userDataBase;
}

定義實體數據:表示數據庫中的表

  • @Entity
  1. 使用@Entity註解實體類,Room會爲實體中定義的每個字段創建一列,如果想避免使用@Ignore註解
  2. Room默認使用類名作爲數據庫表名,要修改表名使用 @Entity 的 tableName屬性
  • 主鍵
  1.  @PrimaryKey :至少定義一個字段作爲主鍵
  2. 如果自增長ID 使用設置@PrimaryKey的 autoGenerate 屬性
  3. 使用組合主鍵 使用@Entity 的@primaryKeys屬性
  4. Room 默認使用字段名成作爲列名,要修改使用 @ColumnInfo(name = "***") 
@Entity(tableName = "userDataBase")
class User {
    @PrimaryKey(autoGenerate = true)    // 單個主鍵設置爲自增長
    public var id = 0
    @ColumnInfo(name = "nameUser")  // 定義列名
    public var name: String? = null
}

@Entity(primaryKeys = ["id", "name"])  // 組合組件
  • 添加索引@Entity
  1. 使用 @Entity 的indices 屬性,列出要包含在索引或複合索引中的列的名稱
@Entity(indices = [Index("nameUser"), Index(value = ["name"])])  // 創建索引
@Entity(indices = [Index("nameUser"), Index(value = ["name"] ,unique = true)]) //唯一索引
  • 外鍵約束@ForeignKey
  1. 使用@ForeignKey 註釋定義其與實體的 關係;ForeignKey中 entity 爲要關聯的父實體類;parentColumns 爲關聯父實體類的列名;childColumns此實體類中的列名
@Entity(foreignKeys = [ForeignKey(entity = User::class,
        parentColumns = ["id"],
        childColumns = ["user_id"])])
class Book {
    @PrimaryKey
    var bookId: Int = 0
    var title: String? = null
    @ColumnInfo(name = "user_id")
    var userId: Int = 0
}
  • 嵌套對象@Embedded 
  1. 使用 @Embedded 註釋來表示要分解到表中子字段的對象(此時數據庫的列爲兩個類中所有的字段)
class Address {
    public var street: String? = null
    public var state: String? = null
    public var city: String? = null
    @ColumnInfo(name = "post_code")
    public var postCode = 0
}

// 在User實體中引入Address
@Embedded
public var address: Address? = null

訪問數據庫

  • 使用@DAO註解:包含用於訪問數據庫的方法
@Dao                   
public interface UserDao {
    @Insert      // 添加數據註解
    void insertAll(User... users);

    @Delete    // 刪除數據註解
    void delete(User user);
}

4、實例實戰

  • insert:使用註解@Insert,Room會自動將所有參數在單個事物中插入數據庫
@Insert
public fun inertUser(user: User)   // 單個參數可以返回 long

@Insert
public fun insertUserList(array: Array<User>)  // 參數爲集合可以返回long[]
  1. 數據庫添加User
val user = User()
user.name = "趙雲 編號 = $number"
val address = Address()
address.street = "成都接頭"
address.state = "蜀漢"
address.city = "常山"
address.postCode = 10010
user.address = address
userDao.inertUser(user)   // 添加User
  1. 添加數據結果:

  • upadte:使用 @Update註解
@Update
public fun update(user: User)     // 可以讓此方法返回一個int值,表示數據庫中更新的行數  


val user = User()
user.id = 1
user.name = "張翼德"
address.city = "涿郡"
.....
userDao.update(user)
  1. 點擊 Update 後再查詢結果:此時的趙雲已經改爲張翼徳了

  • delete:使用@Delete註解
@Delete 
public fun delete(user: User)    //可以返回一個int值,表示從數據庫中刪除的行數


val user = User()
user.id = 1      // 要刪除的主鍵 id
userDao.delete(user)
  1. 點擊delete後再次查詢數據:編號爲1的數據已被刪除

  • 查詢信息 :@Query註解對數據庫執行讀/寫操作
@Query("SELECT * FROM user")
public fun selectAll(): Array<User>    // 查詢所有數據

@Query("SELECT * FROM user WHERE name = :name")
public fun selectUser(name:String): Array<User>    // 條件查詢
  • 返回列的子集:創建子類在每個屬性中使用@ColumnInfo(name = "name")標記對應數據庫中的列名
public class UserTuple{                                  // 1、根據要查詢的字段創建POJO對象           
    @ColumnInfo(name = "name")
    public var name: String? = null
    @ColumnInfo(name = "city")
    public var city: String? = null
}

@Query("SELECT name ,city FROM user")  // 2、查詢的結果會映射到創建的對象中
    public List<UserTuple> loadFullName();


val userList = userDao.loadFullName()
for (userTuple in userList) {
    stringBuilder.append(userTuple.name)
            .append("   ")
            .append(userTuple.city)
            .append("\n")
}
  1. 輸出的結果:只有name和city兩列

  • 範圍條件查詢 :查詢城市中所有用戶
@Query("SELECT name ,street FROM user WHERE city IN (:cityArray)")
fun loadUserInCity(cityArray: Array<String>): List<UserTuple>


val userList = userDao.loadUserInCity(arrayOf("常山"))  // 查詢常山,只會出現趙雲不會出現張翼德

  • Observable查詢:使用LiveData作爲查詢方法的返回值,註冊觀察者後,數據表更改時自動更新UI
@Query("SELECT name ,street FROM user WHERE city IN (:cityArray"))
fun loadUserInCityLive(cityArray: Array<String>): LiveData<List<UserTuple>>



private lateinit var liveData: LiveData<Array<UserTuple>>   // 定義一個LiveData
get() {
return userDao.loadUserInCityLive(arrayOf("常山"))
}

val observer = Observer<Array<UserTuple>> {      // 定義一個觀察者
    val stringBuilder = StringBuilder()
    for (index in it!!.indices) {
        val userTuple = it[index]
        stringBuilder.append(userTuple.name)
                .append("   ")
                .append(userTuple.name)
                .append("   \n")
    }
   tv_main_show.text = stringBuilder.toString()
}
liveData.observe(this, observer)     // 註冊觀察者

運行結果:此時當添加數據時,UI會自動更新;

  • RxJava 查詢 :返回Observable實例可以使用RxJava訂閱觀察者
@Query("SELECT * FROM user WHERE id = :id LIMIT 1")
fun loadUserRxJava(id:Int) : Flowable<User>


userDao.loadUserRxJava(4)
        .subscribe(Consumer {
            val stringBuilder = StringBuilder()
            stringBuilder.append(it.id)
                    .append("   ")
                    .append(it.name)
                    .append("   \n")
            tv_main_show.text = stringBuilder.toString()
        })

  •  Cursor查詢:返回Cursor對象
fun loadUserCursor(id:Int) : Cursor
  • 多表查詢:根據表的外鍵多表查詢
@Query("SELECT user.name AS userName, pet.name AS petName "
          + "FROM user, pet "
          + "WHERE user.id = pet.user_id")

5、更新數據庫

  • 編寫 Migration 的實例。每個 Migration 類指定一個startVersion和endVersion
  • Room運行每個 Migration 類的 migrate() 方法,使用正確的順序將數據庫遷移到更高版本
static final Migration MIGRATION_1_2 = new Migration(1, 2) { //由1升級到版本2
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        database.execSQL("CREATE TABLE book (id  INTEGER , name TEXT )")
    }
};

static final Migration MIGRATION_2_3 = new Migration(2, 3) { //由2升級到版本3
    @Override
    public void migrate(SupportSQLiteDatabase database) {
         database.execSQL("ALTER TABLE user ADD COLUMN strength INTEGER NOT NUll DEFAULT 0")  //添加strength列
    }
};


Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name")
        .addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();

升級完數據庫後再次查詢,結果顯示數據庫增加了strength列名:

6、引用複雜數據

Room提供了在原始類型和目標類型之間進行轉換的功能,但不允許實體之間的對象引用,對於其他類型之間的使用需要自定義轉換器

  • 使用類型轉換器

使用TypeConverter,它將自定義類轉換爲Room可以保留的已知類型,如:想保存Date類型,而Room無法持久化實例Date卻可以實例long,因此提供和long的相互轉換

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({Converters.class})
  • 使用 類型轉換器
@Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
List findUsersBornBetweenDates(Date from, Date to);

以上就是數據庫Room的使用簡介了,基本數據庫的增刪改查以及常見的設置都在其中了,下面我們來看看Room是如何實現這些過程的,從源碼角度分析數據庫。

7、源碼分析

數據庫的創建和升級

Room數據庫實例的創建由Room.databaseBuilder(context.applicationContext,RoomTestData::class.java, "Sample.db").build()開始的,從代碼中看出時使用Builder模式創建DataBase,所以我們先看看RoomDatabase.Builde類

  • RoomDatabase.Builder:除了包含Room的實現類、數據庫名稱的常規設置外,也包含了數據庫的升級信息
@NonNull
public Builder<T> addMigrations(@NonNull  Migration... migrations) {  // 添加數據庫版本升級信息
    if (mMigrationStartAndEndVersions == null) {
        mMigrationStartAndEndVersions = new HashSet<>();
    }
    for (Migration migration: migrations) {
        mMigrationStartAndEndVersions.add(migration.startVersion);
        mMigrationStartAndEndVersions.add(migration.endVersion);
    }

    mMigrationContainer.addMigrations(migrations);
    return this;
}
  • build():創建並初始化數據庫
private static final String DB_IMPL_SUFFIX = "_Impl"
。。。。。。
T db = Room.getGeneratedImplementation(mDatabaseClass, DB_IMPL_SUFFIX); // 創建DataBase實現類的實例 
db.init(configuration);  // 初始化數據庫
  • getGeneratedImplementation():反射創建DataBase的實現類
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;  // 拼接類名
    //noinspection TryWithIdenticalCatches
    try {

        @SuppressWarnings("unchecked")
        final Class<T> aClass = (Class<T>) Class.forName(
                fullPackage.isEmpty() ? implName : fullPackage + "." + implName);   // 獲取自動生成的類文件
        return aClass.newInstance();  // 創建並返回實例
    } catch (ClassNotFoundException e) {
        。。。。。。
    } 
}

此處獲取到的是系統根據註解自動創建的是實現類RoomDataBase_Impl,Room採用的是註解自動生成代碼方式,根據@DataBase和@Dao的註解,自動生成這兩個註解標記的實現類,系統創建類如下圖:

  • RoomTestData_Impl:系統自動生成的實現類
public class RoomTestData_Impl extends RoomTestData {
  private volatile UserDao _userDao;
......
  @Override
  public UserDao userDao() {   
    if (_userDao != null) {
      return _userDao;
    } else {
      synchronized(this) {
        if(_userDao == null) {
          _userDao = new UserDao_Impl(this);  // 創建並返回UserDao的實例
        }
        return _userDao;
      }
    }
  }
}

從上面的代碼中看出,系統自動創建了RoomTestData的實現類,並重寫了抽象方法userDao(),在userDao()中使用單例的方式提供UserDao的實現類UserDao_Impl,UserDao_Impl的形成和RoomTestData_Impl的生成一樣,在代碼中從DataBase中調用userDao返回的就是UserDao_Impl的實例;

接着分析數據庫的創建,在上面的代碼中有一句數據庫的初始化代碼db.init(),在db.init()的方法中會調用RoomDataBase中的抽象方法createOpenHelper(),這裏調用的是createOpenHelper()就是RoomTestData_Impl自動實現的方法:

protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration configuration) {

  final SupportSQLiteOpenHelper.Callback _openCallback = new RoomOpenHelper(configuration, new RoomOpenHelper.Delegate(3) {
    @Override
    public void createAllTables(SupportSQLiteDatabase _db) {
      _db.execSQL("CREATE TABLE IF NOT EXISTS `user` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `strength` INTEGER NOT NULL, `name` TEXT, `street` TEXT, `state` TEXT, `city` TEXT, `post_code` INTEGER)");   // 創建數據庫
      _db.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)");
      _db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"8ece9a1581b767a0f460940849e9b463\")");
    }

    @Override
    public void dropAllTables(SupportSQLiteDatabase _db) {
      _db.execSQL("DROP TABLE IF EXISTS `user`");  // 刪除數據庫
    }

   @Override
protected void validateMigration(SupportSQLiteDatabase _db) {  // 處理數據庫的版本升級
 。。。。。。
}
  }, "8ece9a1581b767a0f460940849e9b463", "061261cef54147a569851cbbb906c3be");
}
 。。。。。。
  return _helper;
}

上面的代碼中執行一下操作:

  1. 創建SupportSQLiteOpenHelper.Callback 的實例並重寫方法
  2. 在onCreate()中Sql語句創建user表和room_master_table表
  3. 在dropAllTables()中創建刪除數據庫的SQL語句
  4. 在validateMigration()中完成數據庫的升級

上面SupportSQLiteOpenHelper.Callback 的實現類爲RoomOpenHelper,下面一起看看RoomOpenHelper源碼:

@Override
public void onCreate(SupportSQLiteDatabase db) {
    updateIdentity(db);
    mDelegate.createAllTables(db);  // mDelegate爲上面創建的RoomOpenHelper.Delegate實例
    mDelegate.onCreate(db);
}

@Override
public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
if (mConfiguration != null) {
    List<Migration> migrations = mConfiguration.migrationContainer.findMigrationPath(
            oldVersion, newVersion);
    if (migrations != null) {
        for (Migration migration : migrations) {
            migration.migrate(db);
        }
        mDelegate.validateMigration(db);   // 調用validateMigration方法處理數據庫的更新
        updateIdentity(db);
        migrated = true;
    }
}
}

@Override
public void onDowngrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
    onUpgrade(db, oldVersion, newVersion);
}

從上面代碼中可以看出,在onCreate()方法中調用了mDelegate.createAllTables(db),這裏的mDelegate就是上面創建RoomOpenHelper方法中第二個參數RoomOpenHelper.Delegate,所以這裏就是在onCreate()中創建了數據庫,在onUPgrade()中調用 mDelegate.validateMigration(db)完成數據庫的升級,到這裏數據庫的創建和升級已經介紹完畢了,下面就一起看看Room是如何訪問數據庫的。

數據庫的訪問

  • @Dao數據庫的實現類: UserDao_Impl
private final RoomDatabase __db;   // 傳入的數據庫

private final EntityInsertionAdapter __insertionAdapterOfUser;  // 處理insert方法

private final EntityDeletionOrUpdateAdapter __deletionAdapterOfUser;   // 處理delete方法

private final EntityDeletionOrUpdateAdapter __updateAdapterOfUser;  // 處理update方法

在UserDao_Impl的類中除了數據庫RoomDataBase實例外,還有三個成員變量分別爲:__insertionAdapterOfUser、__deletionAdapterOfUser、__updateAdapterOfUser,從名字上可以看出來他們三個分別對應數據庫增、刪、改的三個操作,我們以insert操作爲例,查看insert方法:

@Override
public void inertUser(User user) {
  __db.beginTransaction();
  try {
    __insertionAdapterOfUser.insert(user);
    __db.setTransactionSuccessful();
  } finally {
    __db.endTransaction();
  }
}

insert()方法的實現是在__insertionAdapterOfUser中執行的,查看__insertionAdapterOfUser的實現

this.__insertionAdapterOfUser = new EntityInsertionAdapter<User>(__db) {
  @Override
  public String createQuery() {  // 創建SupportSQLiteStatement時傳入的Sql語句
    return "INSERT OR ABORT INTO `user`(`id`,`strength`,`name`,`street`,`state`,`city`,`post_code`) VALUES (nullif(?, 0),?,?,?,?,?,?)";
  }

  @Override
  public void bind(SupportSQLiteStatement stmt, User value) {
    stmt.bindLong(1, value.getId());
    stmt.bindLong(2, value.getStrength());
    if (value.getName() == null) {  // 判斷此列是否爲null,部位Null則設置數據
      stmt.bindNull(3);
    } else {
      stmt.bindString(3, value.getName());
    }
    final Address _tmpAddress = value.getAddress();
    if(_tmpAddress != null) {
      if (_tmpAddress.getStreet() == null) {
        stmt.bindNull(4);
      } else {
        stmt.bindString(4, _tmpAddress.getStreet());
      }
      if (_tmpAddress.getState() == null) {
        stmt.bindNull(5);
      } else {
        stmt.bindString(5, _tmpAddress.getState());
      }
      if (_tmpAddress.getCity() == null) {
        stmt.bindNull(6);
      } else {
        stmt.bindString(6, _tmpAddress.getCity());
      }
      stmt.bindLong(7, _tmpAddress.getPostCode());
    } else {
      stmt.bindNull(4);
      stmt.bindNull(5);
      stmt.bindNull(6);
      stmt.bindNull(7);
    }
  }
};

__insertionAdapterOfUser的實例重寫了兩個方法:

  1. createQuery():創建數據庫插入數據的sql語句
  2. bind():綁定數據庫中每個列對應的值
  • __insertionAdapterOfUser.insert()

insert()方法中創建SupportSQLiteStatement的實例,並調用bind()完成數據的綁定,然後執行stmt.executeInsert()插入數據

public final void insert(T entity) {
    final SupportSQLiteStatement stmt = acquire();  // 最終創建的是FrameworkSQLiteStatement的包裝的SQLiteStatement實例
    try {
        bind(stmt, entity);  // 綁定要插入的數據
        stmt.executeInsert();   // 提交保存數據,執行
    } finally {
        release(stmt);
    }
}


@Override
public long executeInsert() {  // 最終執行數據庫的插入操作
    return mDelegate.executeInsert();
}
  • 查尋數據庫

在UserDao_Impl中自動實現了查詢的方法selectUser:

@Override
public User[] selectUser(String name) {
  final String _sql = "SELECT * FROM user WHERE name = ?";
  final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);   //   創建RoomSQLiteQuery 
  int _argIndex = 1;
  if (name == null) {
    _statement.bindNull(_argIndex);
  } else {
    _statement.bindString(_argIndex, name);
  }
  final Cursor _cursor = __db.query(_statement); //   執行查詢反會Cursor
  try {
    final int _cursorIndexOfId = _cursor.getColumnIndexOrThrow("id");
    final int _cursorIndexOfStrength = _cursor.getColumnIndexOrThrow("strength");
    final int _cursorIndexOfName = _cursor.getColumnIndexOrThrow("name");
    final int _cursorIndexOfStreet = _cursor.getColumnIndexOrThrow("street");
    final int _cursorIndexOfState = _cursor.getColumnIndexOrThrow("state");
    final int _cursorIndexOfCity = _cursor.getColumnIndexOrThrow("city");
    final int _cursorIndexOfPostCode = _cursor.getColumnIndexOrThrow("post_code");
    final User[] _result = new User[_cursor.getCount()];
    int _index = 0;
    while(_cursor.moveToNext()) {
      final User _item;
      final Address _tmpAddress;
      if (! (_cursor.isNull(_cursorIndexOfStreet) && _cursor.isNull(_cursorIndexOfState) && _cursor.isNull(_cursorIndexOfCity) && _cursor.isNull(_cursorIndexOfPostCode))) {
        _tmpAddress = new Address();
        final String _tmpStreet;
        _tmpStreet = _cursor.getString(_cursorIndexOfStreet);
        _tmpAddress.setStreet(_tmpStreet);
        final String _tmpState;
        _tmpState = _cursor.getString(_cursorIndexOfState);
        _tmpAddress.setState(_tmpState);
        final String _tmpCity;
        _tmpCity = _cursor.getString(_cursorIndexOfCity);
        _tmpAddress.setCity(_tmpCity);
        final int _tmpPostCode;
        _tmpPostCode = _cursor.getInt(_cursorIndexOfPostCode);
        _tmpAddress.setPostCode(_tmpPostCode);
      }  else  {
        _tmpAddress = null;
      }
      _item = new User();
      final int _tmpId;
      _tmpId = _cursor.getInt(_cursorIndexOfId);
      _item.setId(_tmpId);
      final int _tmpStrength;
      _tmpStrength = _cursor.getInt(_cursorIndexOfStrength);
      _item.setStrength(_tmpStrength);
      final String _tmpName;
      _tmpName = _cursor.getString(_cursorIndexOfName);
      _item.setName(_tmpName);
      _item.setAddress(_tmpAddress);
      _result[_index] = _item;
      _index ++;
    }
    return _result;
  } finally {
    _cursor.close();
    _statement.release();
  }
}

上面執行的也是數據庫的正常操作,先創建了RoomSQLiteQuery的實例,在調用db。query()執行查詢,查詢返回Cursor實例,最終從Cursor中獲取信息轉換爲對象並返回數據。

到此Room的使用和源碼執行流程就到此結束了,本文旨在執行的流程分析,具體的如何使用SQLite數據庫操作的讀者可以自己點擊源碼查看,不過使用的SQLite的查詢和添加方法和平時使用的不同,讀者想分析的話就會找到了,好了,希望本篇文章對想了解和使用Room組件的同學有所幫助!

本文使用Room的Demo,歡迎Star

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章