Room數據庫,用過你才知道好


Room是一個數據持久化庫,它是 Architecture Component的一部分。它讓SQLiteDatabase的使用變得簡單,大大減少了重複的代碼,並且把SQL查詢的檢查放在了編譯時。

Room官方文檔介紹:https://developer.android.google.cn/training/data-storage/room?

一. 初識Room

Room主要由3個重要的組件組成:DataBase、Entity、Dao。三者的關係如下:
在這裏插入圖片描述

1. DataBase

數據庫持有者,並作爲與應用持久關聯數據的底層連接的主要訪問點。在運行時,通過Room.databaseBuilder() 或者 Room.inMemoryDatabaseBuilder()獲取Database實例。

DataBase需要滿足下面幾個條件:

  1. 必須是abstract類而且的extends RoomDatabase。
  2. 必須在類頭的註釋中包含與數據庫關聯的實體列表(Entity對應的類)。
  3. 包含一個具有0個參數的抽象方法,並返回用@Dao註解的類。
    在這裏插入圖片描述
/**
 * 使用中 annotationProcessor 'androidx.room:room-compiler:2.2.2'
 * 改爲kapt 'androidx.room:room-compiler:2.2.2' ,如果項目中使用了kotlin
 *
 * @author 羅發新
 * TypeConverters({Converters.class}) TODO 類型轉化 有待研究其作用
 */
@Database(entities = {
        UpImage.class
//        , Book.class
//        , Loan.class
}, version = 3, exportSchema = false)
public abstract class AbstractAppDataBase extends RoomDatabase {

    public abstract UpImageDao upImageDao();

//  public abstract BookDao bookDao();

    private static volatile AbstractAppDataBase INSTANCE;

    /**
     * 關於AppDataBase 的使用:
     * 1)如果database的版本號不變。app操作數據庫表的時候會直接crash掉。(錯誤的做法)
     * 2)如果增加database的版本號。但是不提供Migration。app操作數據庫表的時候會直接crash掉。(錯誤的做法)
     * 3)如果增加database的版本號。同時啓用fallbackToDestructiveMigration。這個時候之前的數據會被清空掉。
     * 如下fallbackToDestructiveMigration()設置。(不推薦的做法)
     */
    public static AbstractAppDataBase getDatabase(Context context) {
        if (INSTANCE == null) {
            synchronized (AbstractAppDataBase.class) {
                if (INSTANCE == null) {
                    INSTANCE = Room.databaseBuilder(context.getApplicationContext(), AbstractAppDataBase.class, "android_room_dev.db")
                            // 設置是否允許在主線程做查詢操作
                            .allowMainThreadQueries()
                            // 設置數據庫升級(遷移)的邏輯
                            .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
                            // setJournalMode(@NonNull JournalMode journalMode)  設置數據庫的日誌模式
                            // 設置遷移數據庫如果發生錯誤,將會重新創建數據庫,而不是發生崩潰
                            // .fallbackToDestructiveMigration() 會清理表中的數據 ,不建議這樣做
                            //設置從某個版本開始遷移數據庫如果發生錯誤,將會重新創建數據庫,而不是發生崩潰
                            //.fallbackToDestructiveMigrationFrom(int... startVersions);
                            .addCallback(new RoomDatabase.Callback() {
                                // 進行數據庫的打開和創建監聽
                                @Override
                                public void onCreate(@NonNull SupportSQLiteDatabase db) {
                                    super.onCreate(db);
                                }

                                @Override
                                public void onOpen(@NonNull SupportSQLiteDatabase db) {
                                    super.onOpen(db);
                                }
                            })

                            //默認值是FrameworkSQLiteOpenHelperFactory,設置數據庫的factory。
                            // 比如我們想改變數據庫的存儲路徑可以通過這個函數來實現
                            // .openHelperFactory(SupportSQLiteOpenHelper.Factory factory);
                            .build();
                }
            }
        }
        return INSTANCE;
    }

    /**
     * 數據庫版本 1->2 user表格新增了age列
     */
    private final static Migration MIGRATION_1_2 = new Migration(1, 2) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {
            database.execSQL("ALTER TABLE User ADD COLUMN age integer");
        }
    };

    /**
     * 數據庫版本 2->3 新增book表格
     */
    private final static Migration MIGRATION_2_3 = new Migration(2, 3) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {
            database.execSQL("CREATE TABLE IF NOT EXISTS `book` (`uid` INTEGER PRIMARY KEY autoincrement, `name` TEXT , `userId` INTEGER, 'time' INTEGER)");
        }
    };
}

事實上Database有兩種生成方式:

  1. Room.databaseBuilder():生成Database對象,並且創建一個存在文件系統中的數據庫。
  2. Room.inMemoryDatabaseBuilder():生成Database對象並且創建一個存在內存中的數據庫。當應用退出的時候(應用進程關閉)數據庫也消失。

2. Entity(實體)

代表數據庫中某個表的實體類。默認情況下Room會把Entity裏面所有的字段對應到表上的每一列。

Entity類中需要映射到表中的字段需要保證外部能訪問到,所以每個字段要麼Public,要麼實現getter和setter方法。

/**
 * 作者:羅發新
 * 時間:2019/12/2 0002    星期一
 * 郵件:[email protected]
 * 說明:Room 測試類
 * Entity註解包含的屬性有:
 * tableName:設置表名字。默認是類的名字,不區分大小寫。
 * indices:設置索引。
 * inheritSuperIndices:父類的索引是否會自動被當前類繼承。
 * primaryKeys:設置主鍵。
 * foreignKeys:設置外鍵。
 */
//@Entity(primaryKeys = {"firstName", "lastName"}) 也可以這樣設置多個主鍵
//@Entity(indices = {@Index("firstName"), @Index(value = {"last_name", "address"},unique = true)}) unique 設置是否唯一索引
@Entity(tableName = "UserBean")
public class User {
    @Ignore
    public User(String userName) {
//        id = UUID.randomUUID().toString();
        mUserName = userName;
    }

    public User( ) {

    }

    @Ignore
    public User(int id, String userName) {
        this.id = id;
        this.mUserName = userName;
    }
    
    //設置主鍵自增, 每個類需要一個主鍵
    @PrimaryKey(autoGenerate = true)
    public int id;

    @ColumnInfo(name = "username")
    public String mUserName;

    //自定義設置列名
    @ColumnInfo(name = "first_name")
    public String firstName;

    @ColumnInfo(name = "last_name")
    public String lastName;

    public int age;

    /**
     * 如果有多個相同類型的嵌入字段,則可以設置前綴屬性來保持每個列的唯一性。
     * 然後Room會將提供的值添加到嵌入對象中每個列名的開頭。
     */
    @Embedded(prefix = "first")
    public Address address;

    @Embedded(prefix = "second")
    public Address address2;
    
    public String school;

    /**
     * @Ignore 默認每個字段隊列數據庫中的一列,除非用 @Ignore註解不添加進入數據表中
     * 
     */
    @Ignore
    public Bitmap picture;
}

/**
 * onDelete即當parent中有刪除操作時,onUpdate即當parent中有更新操作時,
 * <p>
 * child對應響應的動作有四種:
 * 1. NO_ACTION:當parent中的key有變化的時候child不做任何動作,默認動作
 * 2. RESTRICT:當parent中的key有依賴的時候禁止對parent做動作,做動作就會報錯。
 * 3. SET_NULL:當paren中的key有變化的時候child中依賴的key會設置爲NULL。
 * 4. SET_DEFAULT:當parent中的key有變化的時候child中依賴的key會設置爲默認值。
 * 5. CASCADE:當parent中的key有變化的時候child中依賴的key會跟着變化
 *
 * deferred:默認值false,在事務完成之前,是否應該推遲外鍵約束。
 * 比如當我們啓動一個事務插入很多數據的時候,事務還沒完成之前,parent引起key變化的時候。
 * 可以設置deferred爲ture,讓key立即改變。爲false時,事務完成後child的key纔會統一進行相應處理
 *
 */
@Entity(foreignKeys = @ForeignKey(entity = User.class,
        parentColumns = "id",
        childColumns = "user_id",
        onDelete = CASCADE,
        onUpdate = NO_ACTION,
        deferred = false
))
class Book {
    @PrimaryKey
    public int bookId;

    public String title;

    @ColumnInfo(name = "user_id")
    public int userId;
}

public class Address {
    public String street;
    public String state;
    public String city;

    @ColumnInfo(name = "post_code")
    public int postCode;
}

3. Dao(數據訪問對象)

Data Access Objects 是Room的主要組件,負責定義訪問數據庫的方法,Room在編譯時創建每個DAO實例。

DAO抽象地以一種乾淨的方式去訪問數據庫,它可以是一個接口也可以是一個抽象類。如果它是一個抽象類,它可以有一個構造函數,它將RoomDatabase作爲其唯一參數。

/**
 * dao  在編譯期就會自動報錯,強大的一匹
 * Data Access Object for the users table.
 *
 * @author 羅發新
 */
@Dao
public interface UserDao {

    /**
     * 當DAO裏面的某個方法添加了@Insert註解。Room會生成一個實現,將所有參數插入到數據庫中的一個單個事務。
     * <p>
     * onConflict:表示當插入有衝突的時候的處理策略。OnConflictStrategy封裝了Room解決衝突的相關策略:
     * OnConflictStrategy.REPLACE:衝突策略是取代舊數據同時繼續事務。
     * OnConflictStrategy.ROLLBACK:衝突策略是回滾事務。
     * OnConflictStrategy.ABORT:衝突策略是終止事務。默認策略
     * OnConflictStrategy.FAIL:衝突策略是事務失敗。
     * OnConflictStrategy.IGNORE:衝突策略是忽略衝突。
     */
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    long[] insertUsers(User... users);

    /**
     * 當參數只有一個時,返回值只可以是long
     *
     * @param user 參數
     * @return 表示插入的rowId
     */
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    long insertUser(User user);

    /**
     * 有多個參數時,返回值可以是long[]或者List<long>,
     *
     * @param users 參數
     * @return long表示插入的rowId
     */
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    List<Long> insertUser(User... users);

    @Update(onConflict = OnConflictStrategy.REPLACE)
    int updateUsers(User... users);

    /**
     * Room會把對應的參數信息更新到數據庫裏面
     * @param users  操作參數
     * @return    表示更新成功了多少行
     */
    @Update()
    int updateAll(User... users);

    @Update
    int updateAll(List<User> user);

    /**
     * Room會把對應的參數信息指定的行刪除掉
     * @param users  操作參數
     * @return   表示刪除了多少行
     */
    @Delete
    int deleteUsers(User... users);

    /**
     * Delete all users.
     */
    @Query("DELETE FROM " + TABLE_NAME)
    void deleteAll();

    //所有的CURD根據primary key進行匹配
    String TABLE_NAME = "UserBean";

    //------------------------query------------------------
    // 簡單sql語句,查詢user表所有的column
    @Query("SELECT * FROM " + TABLE_NAME)
    List<User> loadAllUsers();

    /**
     * 它允許您對數據庫執行讀/寫操作。@Query在編譯的時候會驗證準確性,
     * 所以如果查詢出現問題在編譯的時候就會報錯。比如字段名稱不匹配,沒有該字段
     * @param firstName  條件插敘
     * @return  查詢返回的列表
     */
    @Query("SELECT * FROM UserBean WHERE first_name == :firstName")
    User[] loadAllUsersByFirstName(String firstName);

    @Query("SELECT * FROM UserBean WHERE age BETWEEN :minAge AND :maxAge")
    List<User> loadAllUsersBetweenAges(int minAge, int maxAge);

    @Query("SELECT * FROM UserBean WHERE first_name LIKE :search " + "OR last_name LIKE :search")
    List<User> findUserWithName(String search);

//    /**
//     * 只查詢特定列信息
//     */
//    @Query("SELECT first_name, last_name FROM UserBean")
//    List<NameTuple> loadFullName();


    /**
     * 傳遞一組的參數,返回的對象可以用單獨的對象接收
     */
    @Query("SELECT first_name, last_name FROM UserBean WHERE school IN (:regions)")
    List<NameTuple> loadUsersFromRegions(List<String> regions);

    /**
     * LiveData
     */
    @Query("SELECT first_name, last_name FROM UserBean WHERE school IN (:regions)")
    LiveData<List<NameTuple>> loadUsersFromRegionsSync(List<String> regions);

    /**
     * Rxjava2 中的 對象
     */
    @Query("SELECT * from UserBean")
    Flowable<List<User>> loadUser();

    /**
     * 直接返回cursor
     */
    @Query("SELECT * FROM userbean WHERE age > :minAge LIMIT 5")
    Cursor loadRawUsersOlderThan(int minAge);

    /*
     * 多表聯查
     */
//    @Query("SELECT UserBean.* FROM book "
//            + "INNER JOIN loan ON loan.bookId = book.bookId "
//            + "INNER JOIN UserBean ON userbean.id = loan.userId "
//            + "WHERE userbean.last_name LIKE :lastName")
//    List<Book> findBooksBorrowedByNameSync(String lastName);


//    @Query("SELECT userbean.last_name AS userName, book.name AS bookName "
//            + "FROM userbean, book "
//            + "WHERE :userId = book.user_id")
//    LiveData<List<LendingBook>> loadUserAndPetNames(int userId);

    /**
     * 返回第一個用戶
     *
     * @return the user from the table
     */
    @Query("SELECT * FROM " + TABLE_NAME + " LIMIT 1")
    Flowable<User> getUser();

    /**
     * Insert a user in the database. If the user already exists, replace it.
     *
     * @param user the user to be inserted.
     */
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    Completable insertUserSingle(User user);

    /**
     * 多表聯查
     */
    @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);
    }

    /**
     * 查詢指定列的 多表聯查
     * @return
     */
    @Query("SELECT user.name AS userName, pet.name AS petName "
            + "FROM user, pet "
            + "WHERE user.id = pet.user_id")
    public LiveData<List<UserPet>> loadUserAndPetNames();

}

二. 如何使用Room

1.依賴引入

在app.module中配置依賴。
在這裏插入圖片描述

    implementation 'androidx.room:room-runtime:2.2.5'
    // room 配合 RxJava
    implementation 'androidx.room:room-rxjava2:2.2.5'
    kapt 'androidx.room:room-compiler:2.2.5'

2. 方法調用

AbstractAppDataBase.getDatabase(sysApplication).upImageDao().insert(upImage);

AbstractAppDataBase.getDatabase(MyBaseApplication.getInstance()).upImageDao().queryAll();

AbstractAppDataBase.getDatabase(MyBaseApplication.getInstance()).upImageDao();

三、使用注意事項

1. 數據庫關閉異常

E/SQLiteLog: (283) recovered 9 frames from WAL file /data/data/com.*/databases/android_room_dev.db-wal

出現如上情況,應該在程序關閉時,關閉Room數據庫。

 if (AbstractAppDataBase.getDatabase(MyBaseApplication.getInstance()).isOpen()) {
            AbstractAppDataBase.getDatabase(MyBaseApplication.getInstance()).close();
        }

2. SQL用Integer

在進行SQL 拼寫是要用Integer,不要用int ,特別是Migrating中,否則會報出現異常。

3. schemas的導出

如果在AppDataBase中配置了Schema = true才能導出schemas文件,當然Schema 默認就是true。

需要在build.gradle中配置:

android {
    ...
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = ["room.schemaLocation":
                             "$projectDir/schemas".toString()]
            }
        }
    }
}

總結

之所以推薦你用Room ,和其他sqlite相比它的優點有很多

  1. 使用原生sql來表達對數據庫的操作,會在編譯時會驗證SQL的正誤,可擴展性非常強大
  2. 分層清晰,上手簡單,代碼相比於第三方也更加可靠
  3. 存儲對象裏嵌套對象時,可使用@Embedded註解進行自動拆分存儲。
  4. 通過註解生成代碼,減少了代碼量

一般Google推出的技術絕大多數還是挺好用的,我已經棄用了ormLite,根據本人的使用體驗,確實用起來會流暢的多,且使用簡單,它是Google Jatpack的組成之一。

博客書寫不易,如覺得文章還行,請您點個贊 ^ _ ^ !

推薦鏈接:

  1. 熱更新你都知道哪些?
  2. ART與Dalvik、JVM之間的關係你懂了嗎?
  3. 多項目Project共享同一個Library
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章