Android Jetpack組件 —— Room使用詳解及常用數(shù)據(jù)庫對比
一、 Room介紹
- Room是Jetpack組件中一個對象關(guān)系映射(ORM)庫??梢院苋菀讓?SQLite 表數(shù)據(jù)轉(zhuǎn)換為 Java 對象。
- Room 在 SQLite 上提供了一個抽象層,以便在充分利用 SQLite 的強大功能的同時,能夠流暢地訪問數(shù)據(jù)庫。
- 支持與LiveData、RxJava、Kotlin協(xié)成組合使用。
- Google 官方強烈推薦使用Room。

image.png
二、 Room接入以及基礎(chǔ)使用
Room引用配置
dependencies {
def room_version = "2.2.5"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
// optional - RxJava support for Room
implementation "androidx.room:room-rxjava2:$room_version"
// optional - Guava support for Room, including Optional and ListenableFuture
implementation "androidx.room:room-guava:$room_version"
// optional - Test helpers
testImplementation "androidx.room:room-testing:$room_version"
}
Room使用
- 在使用數(shù)據(jù)的時候,需要主要涉及到Room三個部分:
- Entity: 數(shù)據(jù)庫中表對應(yīng)的實體
- Dao: 操作數(shù)據(jù)庫的方法
- DataBase: 創(chuàng)建數(shù)據(jù)庫實例
第一步 創(chuàng)建實體類
@Entity(tableName = UserModel.USER_TABLE_NAME,
indices = {@Index(value = {UserModel.FACE_ID}, unique = true),
@Index(value = {UserModel.NAME}, unique = true)})
public class UserModel implements Parcelable {
public static final String USER_TABLE_NAME = "user" ;
public static final String NAME = "name";
public static final String FACE_ID = "faceId";
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = BaseColumns._ID)
public long id;
@NonNull
@ColumnInfo(name = FACE_ID)
public String faceId;
@NonNull
@ColumnInfo(name = NAME)
public String name;
public UserModel(@NonNull String faceId, @NonNull String name) {
this.faceId = faceId;
this.name = name;
}
}
- @Entity: 代表一個表中的實體,默認類名就是表名,如果不想使用類名作為表名,可以給注解添加表名字段@Entity(tableName = "user")
- @PrimaryKey: 每個實體都需要自己的主鍵
- @NonNull 表示字段,方法,參數(shù)返回值不能為空
- @ColumnInfo(name = “faceId”) 如果希望表中字段名跟類中的成員變量名不同,添加此字段指明
第二步 創(chuàng)建Dao
@Dao
public interface UserDao {
@Query("SELECT * FROM "+ UserModel.USER_TABLE_NAME + " WHERE "+
UserModel.NAME + " = :name")
LiveData<UserModel> queryByName2Lv(String name);
@Query("SELECT * FROM "+ UserModel.USER_TABLE_NAME + " WHERE "+
UserModel.NAME + " = :name")
UserModel queryByName2Model(String name);
@Query("SELECT * FROM "+ UserModel.USER_TABLE_NAME + " WHERE "+
UserModel.FACE_ID + " = :faceId")
LiveData<UserModel> queryByFaceId2Lv(String faceId);
@Query("SELECT * FROM "+ UserModel.USER_TABLE_NAME + " WHERE "+
UserModel.FACE_ID + " = :faceId")
UserModel queryByFaceId2Model(String faceId);
@Query("SELECT COUNT(*) FROM " + UserModel.USER_TABLE_NAME)
int count();
@Query("SELECT * FROM " + UserModel.USER_TABLE_NAME)
LiveData<List<UserModel>> queryAllByLv();
@Query("SELECT * FROM " + UserModel.USER_TABLE_NAME)
List<UserModel> queryAll();
@Update
public int updateUsers(List<UserModel> userModels);
@Insert(onConflict = OnConflictStrategy.REPLACE)
long insertUser(UserModel userModel);
@Insert(onConflict = OnConflictStrategy.REPLACE)
long[] insertAllUser(List<UserModel> userModels);
@Delete
void delete(UserModel... userModels);
@Delete
void deleteAll(List<UserModel> userModels);
@Query("DELETE FROM " + UserModel.USER_TABLE_NAME + " WHERE " +
UserModel.FACE_ID + " = :faceId")
int deleteByFaceId(String faceId);
}
- DAO是數(shù)據(jù)訪問對象,指定SQL查詢,并讓他與方法調(diào)用相關(guān)聯(lián)。
- DAO必須是一個接口或者抽象類。
- 默認情況下,所有的查詢都必須在單獨的線程中執(zhí)行
第三步 創(chuàng)建Database
@Database(entities = {
UserModel.class
},
version = 1, exportSchema = true)
public abstract class RoomDemoDatabase extends RoomDatabase {
public abstract UserDao userDao();
public static final String DATABASE_NAME = "room_demo";
private static RoomDemoDatabase sInstance;
public static RoomDemoDatabase getInstance(Context context) {
if (sInstance == null) {
synchronized (RoomDemoDatabase.class) {
if (sInstance == null) {
sInstance = buildDatabase(context);
}
}
}
return sInstance;
}
private static RoomDemoDatabase buildDatabase(final Context appContext) {
return Room.databaseBuilder(appContext, RoomDemoDatabase.class, DATABASE_NAME)
.allowMainThreadQueries()
// .openHelperFactory(new SafeHelperFactory("123456".toCharArray()))
.addCallback(new Callback() {
@Override
public void onCreate(@NonNull SupportSQLiteDatabase db) {
super.onCreate(db);
}
@Override
public void onOpen(@NonNull SupportSQLiteDatabase db) {
super.onOpen(db);
}
})
.build();
}
}
創(chuàng)建一個抽象類繼承自RoomDatabase
給他添加一個注解@Database表名它是一個數(shù)據(jù)庫,注解有兩個參數(shù)第一個是數(shù)據(jù)庫的實體,它是一個數(shù)組,可以傳多個,當數(shù)據(jù)庫創(chuàng)建的時候,會默認給創(chuàng)建好對應(yīng)的表,第二個參數(shù)是數(shù)據(jù)庫的版本號
定義跟數(shù)據(jù)庫一起使用的相關(guān)的DAO類
創(chuàng)建一個RoomDemoDatabase的單例,防止同時打開多個數(shù)據(jù)庫的實例
使用Room提供的數(shù)據(jù)庫構(gòu)建器來創(chuàng)建該實例,第一個參數(shù)application,第二個參數(shù)當前數(shù)據(jù)庫的實體類,第三個參數(shù)數(shù)據(jù)庫的名字
-
exportSchema = true 支持導(dǎo)出Room生成的配置文件
javaCompileOptions { annotationProcessorOptions { arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] } }
三、 Room 數(shù)據(jù)庫遷移
添加新的實體類
@Entity(tableName = FaceModel.FACE_TABLE_NAME,
foreignKeys = {
@ForeignKey(entity = UserModel.class,
parentColumns = "faceId",
childColumns = "faceId",
onUpdate = ForeignKey.CASCADE,
onDelete = ForeignKey.CASCADE
)
},
indices = {@Index(value = {"faceId"})}
)
public class FaceModel {
public static final String FACE_TABLE_NAME = "face";
public static final String TYPE = "type";
public static final String FACE_ID = "faceId";
public static final String PATH = "path";
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = BaseColumns._ID)
public long id;
@NonNull
@ColumnInfo(name = PATH)
public String path;
@ColumnInfo(name = TYPE)
public int type;
@NonNull
@ColumnInfo(name = FACE_ID)
public String faceId;
public FaceModel(@NonNull String path, int type, @NonNull String faceId) {
this.path = path;
this.type = type;
this.faceId = faceId;
}
}
配置實體類
@Database(entities = {
UserModel.class, FaceModel.class
},
version = 2, exportSchema = true)
添加Migration
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
Log.i(TAG, "migrate: ");
// Create the new table
String sql = "CREATE TABLE IF NOT EXISTS face (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `path` TEXT NOT NULL, `type` INTEGER NOT NULL, `faceId` TEXT NOT NULL, FOREIGN KEY(`faceId`) REFERENCES `user`(`faceId`) ON UPDATE CASCADE ON DELETE CASCADE )";
database.execSQL(
sql);
String sql2 = "CREATE INDEX IF NOT EXISTS `index_face_faceId` ON face (`faceId`)";
database.execSQL(
sql2);
}
};
private static RoomDemoDatabase buildDatabase(final Context appContext) {
return Room.databaseBuilder(appContext, RoomDemoDatabase.class, DATABASE_NAME)
.allowMainThreadQueries()
.addMigrations(MIGRATION_1_2)
//.openHelperFactory(new SafeHelperFactory("123456".toCharArray()))
.addCallback(new Callback() {
@Override
public void onCreate(@NonNull SupportSQLiteDatabase db) {
super.onCreate(db);
}
@Override
public void onOpen(@NonNull SupportSQLiteDatabase db) {
super.onOpen(db);
}
})
.build();
}
- Sql 升級語句,可以根據(jù)Room導(dǎo)出的json文件獲取
多版本升級
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_1_4)
image
四、 Room 關(guān)聯(lián)表
關(guān)聯(lián)表配置
- foreignKeys 配置外鍵
- parentColumns:父表外鍵
- childColumns:子表外鍵
- NO_ACTION: parent表中某行被刪掉(更新)后。child表中與parent這一行發(fā)生映射的行不發(fā)生任何改變
- RESTRICT: parent表中想要刪除(更新)某行。如果child表中有與這一行發(fā)生映射的行。那么改操作拒絕。
- SET_NULL/SET_DEFAULT:parent表中某行被刪掉(更新)后。child表中與parent這一行發(fā)生映射的行設(shè)置為NULL(DEFAULT)值。
- CASCADE:parent表中某行被刪掉(更新)后。child表中與parent這一行發(fā)生映射的行被刪掉(其屬性更新到對應(yīng)設(shè)置)
@Entity(tableName = FaceModel.FACE_TABLE_NAME,
foreignKeys = {
@ForeignKey(entity = UserModel.class,
parentColumns = "faceId",
childColumns = "faceId",
onUpdate = ForeignKey.CASCADE,
onDelete = ForeignKey.CASCADE
)
},
indices = {@Index(value = {"faceId"})}
)
創(chuàng)建嵌套對象
public class UserAndFaceModel {
@Relation(parentColumn = "faceId", entityColumn = "faceId", entity = FaceModel.class)
public List<FaceModel> faceModels;
@Embedded
public UserModel userModel;
}
- Relation: A convenience annotation which can be used in a POJO to automatically fetch relation entities.
When the POJO is returned from a query, all of its relations are also fetched by Room. - Embedded: Marks a field of an Entity or POJO to allow nested fields (fields of the annotated
field's class) to be referenced directly in the SQL queries.
創(chuàng)建關(guān)聯(lián)Dao
@Dao
public interface UserAndFaceDao {
@Transaction // 保障事務(wù)
@Query("SELECT * FROM "+ UserModel.USER_TABLE_NAME + " WHERE "+
UserModel.NAME + " = :name")
LiveData<UserAndFaceModel> queryByName2Lv(String name);
@Transaction
@Query("SELECT * FROM "+ UserModel.USER_TABLE_NAME + " WHERE "+
UserModel.NAME + " = :name")
UserAndFaceModel queryByName2Model(String name);
@Transaction
@Query("SELECT * FROM "+ UserModel.USER_TABLE_NAME + " WHERE "+
UserModel.FACE_ID + " = :faceId")
LiveData<UserAndFaceModel> queryByFaceId2Lv(String faceId);
@Transaction
@Query("SELECT * FROM "+ UserModel.USER_TABLE_NAME + " WHERE "+
UserModel.FACE_ID + " = :faceId")
UserAndFaceModel queryByFaceId2Model(String faceId);
@Transaction
@Query("SELECT * FROM "+ UserModel.USER_TABLE_NAME )
List<UserAndFaceModel> queryAll();
}
關(guān)聯(lián)表數(shù)據(jù)插入注意
- 保障事務(wù)
RoomDemoDatabase.getInstance(MainActivity.this.getApplicationContext()).runInTransaction(new Runnable() {
@Override
public void run() {
UserModel userModel = new UserModel(1, "1", "2");
RoomDemoDatabase.getInstance(MainActivity.this.getApplicationContext()).userDao().insertUser(userModel);
FaceModel faceModel = new FaceModel("fa", 1, "fa");
RoomDemoDatabase.getInstance(MainActivity.this.getApplicationContext()).faceDao().insertFace(faceModel);
}
});
五、 數(shù)據(jù)庫數(shù)據(jù)加密
5.1 文件加密
SQLCipher
SQLCipher是一個在SQLite基礎(chǔ)之上進行擴展的開源數(shù)據(jù)庫,它主要是在SQLite的基礎(chǔ)之上增加了數(shù)據(jù)加密功能。
- 加密性能高、開銷小,只要5-15%的開銷用于加密
- 完全做到數(shù)據(jù)庫100%加密
- 采用良好的加密方式(CBC加密模式——密文分組鏈接模式)
- 使用方便,做到應(yīng)用級別加密
- 采用OpenSSL加密庫提供的算法
- 開源,支持多平臺
- SQLCipjer加密原理介紹
Realm
Realm 介紹:
- Realm 是一個 MVCC (多版本并發(fā)控制)數(shù)據(jù)庫,
- 由Y Combinator公司在2014年7月發(fā)布一款支持運行在手機、
- 平板和可穿戴設(shè)備上的嵌入式數(shù)據(jù)庫,目標是取代SQLite。
- Realm 本質(zhì)上是一個嵌入式數(shù)據(jù)庫,他并不是基于SQLite所構(gòu)建的。
- 它擁有自己的數(shù)據(jù)庫存儲引擎,可以高效且快速地完成數(shù)據(jù)庫的構(gòu)建操作。
- 和SQLite不同,它允許你在持久層直接和數(shù)據(jù)對象工作。
- 在它之上是一個函數(shù)式風(fēng)格的查詢api,眾多的努力讓它比傳統(tǒng)的SQLite 操作更快 。
Realm 加密:
- 借助 Realm,我們可以輕松地進行加密,
- 因為我們可以輕松地決定數(shù)據(jù)庫內(nèi)核所應(yīng)該做的事情。
- 內(nèi)部加密和通常在 Linux 當中做的加密哪樣很類似。
- 因為我們對整個文件建立了內(nèi)存映射,
- 因此我們可以對這部分內(nèi)存進行保護。
- 如果任何人打算讀取這個加密的模塊,
- 我們就會拋出一個文件系統(tǒng)警告“有人正視圖訪問加密數(shù)據(jù)。
- 只有解密此模塊才能夠讓用戶讀取?!蓖ㄟ^非常安全的技術(shù)我們有一個很高效的方式來實現(xiàn)加密。
- 加密并不是在產(chǎn)品表面進行的一層封裝,而是在內(nèi)部就構(gòu)建好的一項功能。
5.2 內(nèi)容加密
- 在存儲數(shù)據(jù)時加密內(nèi)容,在查詢時進行解密。但是這種方式不能徹底加密,數(shù)據(jù)庫的表結(jié)構(gòu)等信息還是能被查看到,另外檢索數(shù)據(jù)也是一個問題。
| 加密算法 | 描述 | 優(yōu)點 | 缺點 |
|---|---|---|---|
| DES,3DES | 對稱加密算法 | 算法公開、計算量小、加密速度快、加密效率高 | 雙方都使用同樣密鑰,安全性得不到保證 |
| AES | 對稱加密算法 | 算法公開、計算量小、加密速度快、加密效率高 | 雙方都使用同樣密鑰,安全性得不到保證 |
| XOR | 異或加密 | 兩個變量的互換(不借助第三個變量),簡單的數(shù)據(jù)加密 | 加密方式簡單 |
| Base64 | 算不上什么加密算法,只是對數(shù)據(jù)進行編碼傳輸 | ||
| SHA | 非對稱加密算法。安全散列算法,數(shù)字簽名工具。著名的圖片加載框架Glide在緩存key時就采用的此加密 | 破解難度高,不可逆 | 可以通過窮舉法進行破解 |
| RSA | 非對稱加密算法,最流行的公鑰密碼算法,使用長度可變的秘鑰 | 不可逆,既能用于數(shù)據(jù)加密,也可以應(yīng)用于數(shù)字簽名 | RSA非對稱加密內(nèi)容長度有限制,1024位key的最多只能加密127位數(shù)據(jù) |
| MD5 | 非對稱加密算法。全程:Message-Digest Algorithm,翻譯為消息摘要算法 | 不可逆,壓縮性,不容易修改,容易計算 | 窮舉法可以破解 |
5.3 Room數(shù)據(jù)庫數(shù)據(jù)庫加密
- SQLCipher并不直接支持Room的數(shù)據(jù)庫進行加密,所以沒法直接實現(xiàn)。
- 可以通過開源庫(swac-saferoom)進行數(shù)據(jù)加密(底層也是通過SQLCipher對數(shù)據(jù)庫文件加密)
集成 swac-saferoom
添加 maven { url "https://s3.amazonaws.com/repo.commonsware.com" }
dependencies {
implementation 'com.commonsware.cwac:saferoom:1.1.3'
}
添加openHelperFactory
private static AppDatabase buildDatabase(final Context appContext) {
return Room.databaseBuilder(appContext, AppDatabase.class, DATABASE_NAME)
.allowMainThreadQueries()
.openHelperFactory(new SafeHelperFactory("123456".toCharArray()))
.addCallback(new Callback() {
@Override
public void onCreate(@NonNull SupportSQLiteDatabase db) {
super.onCreate(db);
}
@Override
public void onOpen(@NonNull SupportSQLiteDatabase db) {
super.onOpen(db);
}
})
.build();
}
六、 Room與其他數(shù)據(jù)庫對比
| 參數(shù) | Room | GreenDao | Realm |
|---|---|---|---|
| 集成包大小 | 0.05M | 0.05M | 9.06M |
| 插入10000條速度 | 551ms | 806ms | 195ms |
| 查詢10000條速度 | 126ms | 71ms | 4ms |
| 刪除10000條速度 | 3ms | 6ms | 5ms |
| 更新10000條速度 | 622ms | 838ms | 242ms |
image
七、 數(shù)據(jù)庫調(diào)試工具分享
debug調(diào)試
使用debug-db 可以在瀏覽器查看表結(jié)構(gòu)及數(shù)據(jù)
普通數(shù)據(jù)庫
- implementation 'com.amitshekhar.android:debug-db:1.0.6'
加密數(shù)據(jù)庫
debug {
resValue("string", "PORT_NUMBER", "8081")
resValue("string", "DB_PASSWORD_PERSON", "123456")
}
implementation 'com.amitshekhar.android:debug-db-encrypt:1.0.6'