Room是Google推出的Android架構組件庫中的數據持久化組件庫, 也能夠說是在SQLite上實現的一套ORM解決方案。Room主要包含三個部分:html
其關係以下圖所示:java
Room Architecture Diagramandroid
一個簡單Entity定義以下:git
@Entity(tableName = "user" indices = {@Index(value = {"first_name", "last_name"})}) public class User { @PrimaryKey private int uid; @ColumnInfo(name = "first_name") private String firstName; @ColumnInfo(name = "last_name") private String lastName; @Ignore public User(String firstName, String lastName) { this.uid = UUID.randomUUID().toString(); this.firstName = firstName; this. lastName = lastName; } public User(String id, String firstName, String lastName) { this.uid = id; this.firstName = userName; this. lastName = userName; } // Getters and setters }
@Entity(tableName = "table_name**")
註解POJO類,定義數據表名稱;@PrimaryKey
定義主鍵,若是一個Entity使用的是複合主鍵,能夠經過@Entity
註解的primaryKeys
屬性定義複合主鍵:@Entity(primaryKeys = {"firstName", "lastName"})
@ColumnInfo(name = 「column_name」)
定義數據表中的字段名@Ignore
用於告訴Room須要忽略的字段或方法@Entity
註解的indices
屬性中添加索引字段。例如:indices = {@Index(value = {"first_name", "last_name"}, unique = true), ...}
, unique = true
能夠確保表中不會出現{"first_name", "last_name"}
相同的數據。不一樣於目前存在的大多數ORM庫,Room不支持Entitiy對象間的直接引用。(具體緣由能夠參考: Understand why Room doesn't allow object references)
但Room容許經過外鍵(Foreign Key)來表示Entity之間的關係。github
@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; }
如上面代碼所示,Book對象與User對象是屬於的關係。Book中的user_id,對應User中的id。 那麼當一個User對象被刪除時, 對應的Book會發生什麼呢?sql
@ForeignKey
註解中有兩個屬性onDelete
和onUpdate
, 這兩個屬性對應ForeignKey
中的onDelete()
和onUpdate()
, 經過這兩個屬性的值來設置當User對象被刪除/更新時,Book對象做出的響應。這兩個屬性的可選值以下:數據庫
CASCADE
:User刪除時對應Book一同刪除; 更新時,關聯的字段一同更新NO_ACTION
:User刪除時不作任何響應RESTRICT
:禁止User的刪除/更新。當User刪除或更新時,Sqlite會立馬報錯。SET_NULL
:當User刪除時, Book中的userId會設爲NULLSET_DEFAULT
:與SET_NULL
相似,當User刪除時,Book中的userId會設爲默認值在某些狀況下, 對於一張表中的數據咱們會用多個POJO類來表示,在這種狀況下能夠用@Embedded
註解嵌套的對象,好比:數組
class Address { public String street; public String state; public String city; @ColumnInfo(name = "post_code") public int postCode; } @Entity class User { @PrimaryKey public int id; public String firstName; @Embedded public Address address; }
以上代碼所產生的User表中,Column 爲id, firstName, street, state, city, post_code
安全
@Dao public interface UserDao { @Query("SELECT * FROM user") List<User> getAll(); @Query("SELECT * FROM user WHERE uid IN (:userIds)") List<User> loadAllByIds(int[] userIds); @Query("SELECT * FROM user WHERE first_name LIKE :first AND " + "last_name LIKE :last LIMIT 1") User findByName(String first, String last); @Insert void insertAll(List<User> users); @Insert(onConflict = OnConflictStrategy.REPLACE) public void insertUsers(User... users); @Delete void delete(User user); @Update public void updateUsers(List<User> users); }
DAO 能夠是一個接口,也能夠是一個抽象類, Room會在編譯時建立DAO的實現。架構
Tips:
@Insert
方法也能夠定義返回值, 當傳入參數僅有一個時返回long
, 傳入多個時返回long[]
或List<Long>
, Room在實現insert方法的實現時會在一個事務進行全部參數的插入。@Insert
的參數存在衝突時, 能夠設置onConflict
屬性的值來定義衝突的解決策略, 好比代碼中定義的是@Insert(onConflict = OnConflictStrategy.REPLACE)
, 即發生衝突時替換原有數據@Update
和@Delete
能夠定義int
類型返回值,指更新/刪除的函數DAO中的增刪改方法的定義都比較簡單,這裏不展開討論,下面更多的聊一下查詢方法。
Talk is cheap, 直接show code:
@Query("SELECT * FROM user") List<User> getAll();
Room會在編譯時校驗sql語句,若是
@Query()
中的sql語句存在語法錯誤,或者查詢的表不存在,Room會在編譯時報錯。
@Query("SELECT * FROM user WHERE uid IN (:userIds)") List<User> loadAllByIds(int[] userIds); @Query("SELECT * FROM user WHERE first_name LIKE :first AND " + "last_name LIKE :last LIMIT 1") User findByName(String first, String last);
看代碼應該比較好理解, 方法中傳遞參數arg
, 在sql語句中用:arg
便可。編譯時Room會匹配對應的參數。
若是在傳參中沒有匹配到
:arg
對應的參數, Room會在編譯時報錯。
在實際某個業務場景中, 咱們可能僅關心一個表部分字段的值,這時我僅須要查詢關心的列便可。
定義子集的POJO類:
public class NameTuple { @ColumnInfo(name="first_name") public String firstName; @ColumnInfo(name="last_name") public String lastName; }
在DAO中添加查詢方法:
@Query("SELECT first_name, last_name FROM user") public List<NameTuple> loadFullName();
這裏定義的POJO也支持使用
@Embedded
Room中查詢操做除了返回POJO對象及其List之外, 還支持:
LiveData<T>
:@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)") public LiveData<List<User>> loadUsersFromRegionsSync(List<String> regions);
Flowablbe<T>
Maybe<T>
Single<T>
:Flowablbe
, Maybe
和Single
對象,對於使用RxJava的項目能夠很好的銜接, 但須要在gradle添加該依賴:android.arch.persistence.room:rxjava2
。@Query("SELECT * from user where id = :id LIMIT 1") public Flowable<User> loadUserById(int id);
Cursor
:Caution: It's highly discouraged to work with the Cursor API because it doesn't guarantee whether the rows exist or what values the rows contain. Use this functionality only if you already have code that expects a cursor and that you can't refactor easily.
Room支持聯表查詢,接口定義上與其餘查詢差異不大, 主要仍是sql語句的差異。
@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); }
Room中DataBase相似SQLite API中SQLiteOpenHelper,是提供DB操做的切入點,可是除了持有DB外, 它還負責持有相關數據表(Entity)的數據訪問對象(DAO), 因此Room中定義Database須要知足三個條件:
@Database(entities = {User.class}, version = 1) public abstract class AppDatabase extends RoomDatabase { public abstract UserDao userDao(); }
建立好以上Room的三大組件後, 在代碼中就能夠經過如下代碼建立Database實例。
AppDatabase db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "database-name").build();
在傳統的SQLite API中,咱們若是要升級數據庫, 一般在SQLiteOpenHelper.onUpgrade
方法執行數據庫升級的sql語句,這些sql語句的一般根據數據庫版本以文件的方式或者用數組來管理。有人說這種方式升級數據庫就像在拆炸彈,相比之下在Room中升級數據庫簡單的就像是按一個開關而已。
Room提供了Migration類來實現數據庫的升級:
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"); } };
在建立Migration類時須要指定startVersion
和endVersion
, 代碼中MIGRATION_1_2
和MIGRATION_2_3
的startVersion和endVersion是遞增的, Migration實際上是支持從版本1直接升到版本3,只要其migrate()
方法裏執行的語句正常便可。那麼Room是怎麼實現數據庫升級的呢?其實本質上仍是調用SQLiteOpenHelper.onUpgrade
,Room中本身實現了一個SQLiteOpenHelper
, 在onUpgrade()
方法被調用時觸發Migration
,當第一次訪問數據庫時,Room作了如下幾件事:
SQLiteOpenHelper.onUpgrade
被調用,而且觸發Migration
這樣一看, Room中處理數據庫升級確實很像是加一個開關。
由於Room使用的也是SQLite, 因此能夠很好的支持原有Sqlite數據庫遷移到Room。
假設原有一個版本號爲1的數據庫有一張表User, 如今要遷移到Room, 咱們須要定義好Entity, DAO, Database, 而後建立Database時添加一個空實現的Migraton便可。須要注意的是,即便對數據庫沒有任何升級操做,也須要升級版本, 不然會拋異常IllegalStateException
.
@Database(entities = {User.class}, version = 2) public abstract class UsersDatabase extends RoomDatabase { … static final Migration MIGRATION_1_2 = new Migration(1, 2) { @Override public void migrate(SupportSQLiteDatabase database) { // Since we didn't alter the table, there's nothing else to do here. } }; … database = Room.databaseBuilder(context.getApplicationContext(), UsersDatabase.class, "Sample.db") .addMigrations(MIGRATION_1_2) .build();
在某些場景下咱們的應用可能須要存儲複雜的數據類型,好比Date
,可是Room的Entity僅支持基本數據類型和其裝箱類之間的轉換,不支持其它的對象引用。因此Room提供了TypeConverter
給使用者本身實現對應的轉換。
一個Date
類型的轉換以下:
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(); } }
定義好轉換方法後,指定到對應的Database上便可, 這樣就能夠在對應的POJO(User)中使用Date
類了。
@Database(entities = {User.class}, version = 1) @TypeConverters({Converters.class}) public abstract class AppDatabase extends RoomDatabase { public abstract UserDao userDao(); }
@Entity public class User { ... private Date birthday; }
在SQLite API方式實現數據持久化的項目中,相信都有一個任務繁重的SQLiteOpenHelper
實現, 一堆維護表的字段的Constant
類, 一堆代碼相似的數據庫訪問類(DAO),訪問數據庫時須要作Cursor的遍歷,構建並返回對應的POJO類...相比之下,Room做爲在SQLite之上封裝的ORM庫確實有諸多優點,比較直觀的體驗是:
想要了解更多Room相關內容能夠戳下面的連接: