Android Architecture Components 系列(五)Room

Room定義
    Room是一個持久化工具,和ORMLite greenDao相似。在開發中利用Room來操做SQLite數據庫,在SQLite上提供了一個方便訪問的抽象層。
 
傳統SQLite的缺陷:
  •     沒有編譯時SQL語句的檢查。當數據庫發生變化時,須要手動的更新相關代碼,會花費至關多的時間而且容易出錯。
  •     編寫大量SQL語句和Java對象之間相互轉換的代碼。
 
針對以上的缺陷 Room的組成由:
    
  •     Database  建立數據庫
        使用註解申明一個類 ,註解中包含若干個Entity類,這個Database類主要負責建立數據庫以及獲取數據對象
 
  •     Entitles  數據庫表中對應的Java對象
        表示每一個數據庫的總的一個表結構,一樣也是使用註解表示,類中的每一個字斷都對應表中的一列
 
  •     DAO 訪問數據庫
        Data Access Object的縮寫 ,表示從代碼中直接訪問數據庫,屏蔽掉Sql語句
 
官方 Room結構圖:
 
下面以存儲User信息未爲實例 :
 
User.java 實體domain類
@Entity
publicclass User { 
    @PrimaryKey 
    privateintuid; 
 
    @ColumnInfo(name ="first_name」) 
    privateString firstName; 
    
    @ColumnInfo(name ="last_name」) 
    privateString lastName; 
    // Getters and setters are ignored for brevity,    
    // but they're required for Room to work.}
    Getters和setters爲了簡單起見就省略了,可是對Room來講是必須的 }
 
UserDao數據庫類
@Dao
publicinterface 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 
    voidinsertAll(User... users); 
 
    @Delete
    voiddelete(User user); }
 
// AppDatabase.java
@Database(entities = {User.class}, version =1) 
    publicabstractclass AppDatabase extends RoomDatabase { 
        publicabstractUserDaouserDao(); 
}
 
代碼中的建立數據庫:
AppDatabase db = Room.databaseBuilder(getApplicationContext(),
                         AppDatabase.class,"database-name").build();
ps:注意: Database最好設計成單利模式,不然對象太多會有性能的影響。
 
Entities
    用來註解一個實體類,對應數據庫一張表。Room爲實體中定義的每一個成員變量在數據庫中建立對應的字斷,若是不想保存某個字段到數據庫表中,可使用 @Ignore 註解該字斷 好比:
@Entity
class User { 
    @PrimaryKey 
    publicintid; 
 
    @ColumnInfo(name ="first_name」) 
    publicString firstName; 
 
    @ColumnInfo(name ="last_name」)
    publicString lastName; 
    
    @Ignore 
    Bitmap picture; 
}
ps:爲了保存每個字段,這個字段須要有能夠訪問的getter/setter方法或是public 屬性 
 

Primary Key 主鍵

一個Entity必須定義一個field爲主鍵 ,即便該表只有一個成員變量或是說字段。
 
自動生成primary key :
    @primaryKey 的autoGenerate屬性
 
多個Field 複合Key:
@Entity(primaryKeys = {"firstName","lastName」})
class User { 
    publicString firstName;
    publicString lastName; 
    @IgnoreBitmap picture; 
}
 
Entity的參數 指定表名:
在默認狀況下 Room使用類名做爲數據庫的表名。若是想要自定義表名,在@Entity後使用tableName參數來指定表名
@Entity( tableName ="users」)
class User { }
 
Entity的參數 指定表的列名:
@ColumnInfo註解是改變成員變量對應的數據庫的字段名稱。
經過@ColumnInfo(name = "first_name")設置
Entity的參數 indices
indices的參數值是@Index 的數組,根據訪問數據庫的方式,對特定的fiedl創建索引來在某些狀況下加快 查詢速度 ,能夠須要加入索引 
@Entity( indices = { @Index("name"),@Index("last_name","address" )}
    class User { 
    @PrimaryKey 
    publicintid; 
    
    publicString firstName; 
    publicString address; 
    
    @ColumnInfo(name ="last_name」)
    publicString lastName; 
    
    @IgnoreBitmap picture; 
}
Entity的參數 unique
    有時候某些字段或字段組必須是惟一的,經過將@Index的unique 設置爲true ,能夠強制執行此惟一性屬性
    這個表中的firstName 和 LastName 不能同時相同
@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; 
}
Entity的參數 foreignKeys(外鍵)
由於SQLite是一種關係型數據庫,能夠指定對象之間的關係。儘管大多數ORM庫容許實體對象的相互引用, 可是Room明確禁止!實體之間沒有對象引用!詳細的緣由,能夠參考這裏。
因爲不能使用直接關係,因此就要用到foreignKeys(外鍵)
例如: 一個Pet類 須要和User類 創建關係
@Entity(foreignKeys = @ForeignKey(entity = User.class, 
                                                         parentColumns = "id」, 
                                                         childColumns = "user_id」) )
    class Pet { 
        @PrimaryKey 
        public int petId; 
        public String name; 
        @ColumnInfo(name = "user_id」) 
        public int userId; 
    }
     外鍵容許指定運用實體更新時發生的操做,好比你能夠定義當刪除User時對應的Pet類也被刪除,也能夠在@ForeignKey 中 添加 onDelete = CASCADE 實現 
同理的添加操做:
    @Insert(OnConflict = REPLACE)
定義裏REMOVE 和REPLACE 而不是簡單的UPDATE操做。這樣產生的後果會影響外鍵定義的約束行爲,詳細的信息能夠參考 SQLite documentation。
 
Entity的參數 關聯Entity
Entity之間可能也有一對多之間的關係,好比一個User有多個Pet,經過一次查詢獲取多個關聯的Pet。
public class UserAndAllPets {
    @Embedded
    public User user;//父類Entity
    @Relation (parentColumn = 「id」 , entityColumn = 「user_id")
    public List<Pet>pets ;//空list
}
    @Dao
 public interface UserPetDao {
    @Query (「SELECT * from User")
    public List<UserAndAllPets> loadUserAndPets();
}
ps:注意
    使用@Relation 註解的field 必須是一個List 或是 一個Set 。Entity的類型是從返回類型中推斷出來的,能夠經過定義entity()來定義特定的返回類型。
    用@Relation 註解的field 必須是public 或者有public的setter。這是由於加載數據是分爲兩個步驟的:
        a、父Entity被查詢
        b、觸發用@Relation註解的entity的查詢
    因此在上面的UserAndAllPets類中,首先User所在的數據庫被查詢,而後才觸發查詢Pets的查詢。
便是Room首先建立一個空的對象,而後設置父Entity 和一個 空的list ,在第二次查詢的時候Room將會填充這個list。
 
Entity的參數 對象嵌套對象
    有時候須要在類裏面把另外一個類做爲field ,這時候就須要使用@Embedded,這樣就能夠像查詢其餘列同樣查詢這個field類了。
    好比user類包含了一個field Address類,表明user的地址包括所在的街道,城市,州和郵編 。
 
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;
}
 
 
Data Access Objects DAO類:
數據庫訪問的抽象層
        Dao 能夠是一個接口 也能夠是一個抽象類 。若是是一個抽象類,那麼它能夠
接受一個RoomDatabase 做爲構造器的惟一參數。
 
        Room不容許再煮現吃中訪問數據庫,除非在builder裏面調用 allowMainThreadQueries().
由於訪問數據庫是耗時操做,在主線程中進行操做可能會阻塞線程,引發UI卡頓或是ANR。
 
Dao @Insert 
    @Insert 註解的方法,Room將會生成插入的代碼
 
@Dao
publicinterface MyDao { 
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    publicvoidinsertUsers(User... users);
    
    @InsertpublicvoidinsertBothUsers(User user1, User user2); 
    @InsertpublicvoidinsertUsersAndFriends(User user, List<User> friends); 
}
ps:若是@Insert 只接受一個參數,那麼將返回一個long,對應着插入的rowId;
        若是接受多個參數,或是數組、集合,那麼就會返回一個long的數組或是list
 
Dao @Update、@Delete操做
 
@Dao
publicinterface MyDao { 
    @Update 
    publicvoidupdateUsers(User... users); 
    @Delete
    publicvoiddeleteUsers(User... users); 
}
可讓update、delete方法返回一個int類型的整數,表明被update、delete的行號
 
Dao @Query操做的方法
    @Query註解的方法在編譯時候就會被檢查到,若是有任何查詢的問題,都會拋出編譯異常,而不是等到運行後才觸發異常。
    Room也會檢查查詢返回值的類型,若是返回類型的字段和數據路列表名存在不一致,會收到警告,若是二者徹底不一致會報錯。
 
  • 一個簡單查詢示例
@Dao
public interface MyDao {
    @Query("SELECT * FROM user")
    public User[] loadAllUsers();
}
 
  • 帶參數的查詢操做
@Dao
publicinterface MyDao { 
    @Query("SELECT * FROM userWHERE age > :minAge」)
    publicUser[]loadAllUsersOlderThan(intminAge);
}
ps:在編譯時作類型檢查,若是表中沒有age這個列或是說字段,那麼就會拋出錯誤。
  • 帶多個參數
@Dao
    publicinterface MyDao { 
    
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge」) 
    publicUser[]loadAllUsersBetweenAges(intminAge,intmaxAge); 
   
 @Query("SELECT * FROM user WHERE first_name LIKE :search OR last_name LIKE :search」)
    publicList<User>findUserWithName(String search); 
}
 
支持返回列的子集
    有時候只須要Entity的幾個field,例如只須要獲取User的姓名就好了。那麼經過只獲取這兩個列的數據不只可以節省寶貴的資源,還能加快查詢速度!
    好比上面的User,我只須要firstName和lastName,首先定義一個子集,而後結果改爲對應子集便可
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(); //定義一個子集
}
 
支持返回集合的子集
    查詢兩個地區的全部用戶,直接用sql中的in便可,但若是這個地區是程序指定的,個數不肯定呢?
@Dao
publicinterface MyDao { 
    
    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)」)
    publicList<NameTuple>loadUsersFromRegions(List<String> regions);
}
 
支持Observable 可被觀察的查詢
    經過LiveData的配合使用,就能夠實現當數據庫內容發生變化時自動收到變化後的數據的功能;能夠異步的獲取數據,那麼咱們的Room也是支持異步查詢的。
@Dao
publicinterface MyDao { 
  @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)」) 
  publicLiveData<List<User>> 
    loadUsersFromRegionsSync(List<String> regions); 
}
 
支持RxJava
Room也能夠返回RxJava2中的Publisher 和 Flowable 格式的數據。RxJava是另一個異步操做庫,一樣也是支持的。
ps:在Gradle中添加android.arch.persistence.room:rxjava2
詳細的信息能夠參考 Room and RxJava這篇文章。
@Dao
publicinterface MyDao { 
    @Query("SELECT * from user where id = :id LIMIT 1」) 
    publicFlowable<User>loadUserById(intid);
}
 
支持直接獲取cursor
原始的Android系統查詢結果是經過Cursor來獲取的,一樣也支持。
@Dao
publicinterface MyDao {
    @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5」)
    publicCursorloadRawUsersOlderThan(intminAge);
}
 
支持多表查詢
    有時候數據庫存在範式相關,數據拆到了多個表中,那麼就須要關聯多個表進行查詢,若是結果只是一個表的數據,那麼很簡單,直接用Entity定義的類型便可。
 
下面這段代碼演示瞭如何從一個包含借閱用戶信息的表和一個包含已經被借閱的書的表中獲取信息:
@Dao
publicinterface 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」) 
publicList<Book>findBooksBorrowedByNameSync(String userName);
}
 
    固然也能夠從查詢中返回的POJO(Domain實體)類。可是須要單獨定義一個POJO類,來接受數據。
// You can also define this class in a separate file, as long as you add the
   // "public" access modifier.
publicclass UserPet { 
    publicString userName; 
    publicString petName;
@Dao
publicinterface MyDao {
    @Query("SELECT user.name AS userName, pet.name AS petName 「 
        +"FROM user, pet 「 
        +"WHERE user.id = pet.user_id」) 
    publicLiveData<List<UserPet>>loadUserAndPetNames(); 
}
 
支持類型轉換器
      有時候Java定義的數據類型和數據庫中存儲的數據類型是不一致的,Room提供類型轉換,這樣在操做數據庫的時候能夠自動轉換類型。
    好比在Java中,時間用Date表示,可是在數據庫中類型倒是long,這樣有利於存儲
publicclass Converters { 
    @TypeConverter 
    publicstaticDatefromTimestamp(Long value) { 
        returnvalue ==null?null:newDate(value); 
    }     
    @TypeConverter 
    publicstaticLongdateToTimestamp(Date date) { 
        returndate ==null?null: date.getTime(); 
    } 
}
ps:也能夠存儲等價的Unix時間戳。經過  TypeConverter 能夠很方便的作到這一點
 
定義數據庫時候須要指定類型轉換,同時定義號Entity和Dao類
將@TypeConverters添加到AppDatabase中,這樣Room就能自動識別這種轉換:
@Database(entities = {User.java}, version =1) 
@TypeConverters({Converter.class}) 
publicabstractclass AppDatabase extends RoomDatabase { 
        publicabstractUserDaouserDao(); 
}
@Entity
publicclass User { 
            … 
    privateDate birthday; 
@Dao
publicinterface UserDao {
        ... 
    @Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
     List<User> findUsersBornBetweenDates(Date from, Date to);
}
關於更多 @TypeConverters的用法,能夠參考這裏
最後來講一下數據庫升級,或是叫數據庫遷移及輸出模式
    隨着版本迭代,不可避免的會遇到數據庫升級的問題,Room也爲咱們提供了數據庫升級的處理接口。
     Room使用 Migration 來實現數據庫的遷移。每一個 Migration 都指定了startVersion 和 endVersion 。在運行的時候Room運行每一個  Migration  的migrate()方法 ,按正確的順序來遷移數據庫到下一個版本。若是沒有提供足夠的遷移信息,Room會從新建立數據庫。
ps:這意味着失去原來保存的信息!!!
Room.databaseBuilder(getApplicationContext(),
             MyDb.class,"database-name").addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();
    staticfinalMigration MIGRATION_1_2 =newMigration(1,2) { 
        @Override 
        publicvoidmigrate(SupportSQLiteDatabase database) { 
         database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, `name` TEXT, PRIMARY KEY(`id`))」);
        } 
    }; 
   staticfinalMigration MIGRATION_2_3 =newMigration(2,3) { 
        @Override
        publicvoidmigrate(SupportSQLiteDatabase database) { 
         database.execSQL("ALTER TABLE Book ADD COLUMN pub_year INTEGER"); 
        } 
    };
    遷移過程結束後,Room將驗證架構以確保遷移正確發生。若是Room發現問題,則拋出包含不匹配信息的異常。
再次警告: 若是不提供必要的遷移,Room會從新構建數據庫,這意味着將丟失數據庫中的全部數據。
輸出模式
能夠在gradle中設置開啓輸出模式,便於咱們調試,查看數據庫表狀況,以及作數據庫遷移。
android {
    ...
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = ["room.schemaLocation":
                             "$projectDir/schemas".toString()]
            }
        }
    }
}
 
相關文章
相關標籤/搜索