Android Jetpack组件 —— Room使用详解及常用数据库对比
一、 Room介绍
- Room是Jetpack组件中一个对象关系映射(ORM)库。可以很容易将 SQLite 表数据转换为 Java 对象。
- Room 在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库。
- 支持与LiveData、RxJava、Kotlin协成组合使用。
- Google 官方强烈推荐使用Room。
二、 Room接入以及基础使用
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使用
- 在使用数据的时候,需要主要涉及到Room三个部分:
- Entity: 数据库中表对应的实体
- Dao: 操作数据库的方法
- DataBase: 创建数据库实例
第一步 创建实体类
@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 表示字段,方法,参数返回值不能为空
- @ColumnInfo(name = “faceId”) 如果希望表中字段名跟类中的成员变量名不同,添加此字段指明
第二步 创建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是数据访问对象,指定SQL查询,并让他与方法调用相关联。
- DAO必须是一个接口或者抽象类。
- 默认情况下,所有的查询都必须在单独的线程中执行
第三步 创建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();
}
}
创建一个抽象类继承自RoomDatabase
给他添加一个注解@Database表名它是一个数据库,注解有两个参数第一个是数据库的实体,它是一个数组,可以传多个,当数据库创建的时候,会默认给创建好对应的表,第二个参数是数据库的版本号
定义跟数据库一起使用的相关的DAO类
创建一个RoomDemoDatabase的单例,防止同时打开多个数据库的实例
使用Room提供的数据库构建器来创建该实例,第一个参数application,第二个参数当前数据库的实体类,第三个参数数据库的名字
-
exportSchema = true 支持导出Room生成的配置文件
javaCompileOptions { annotationProcessorOptions { arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] } }
三、 Room 数据库迁移
添加新的实体类
@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 升级语句,可以根据Room导出的json文件获取
多版本升级
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_1_4)
四、 Room 关联表
关联表配置
- foreignKeys 配置外键
- parentColumns:父表外键
- childColumns:子表外键
- NO_ACTION: parent表中某行被删掉(更新)后。child表中与parent这一行发生映射的行不发生任何改变
- RESTRICT: parent表中想要删除(更新)某行。如果child表中有与这一行发生映射的行。那么改操作拒绝。
- SET_NULL/SET_DEFAULT:parent表中某行被删掉(更新)后。child表中与parent这一行发生映射的行设置为NULL(DEFAULT)值。
- CASCADE:parent表中某行被删掉(更新)后。child表中与parent这一行发生映射的行被删掉(其属性更新到对应设置)
@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 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.
创建关联Dao
@Dao
public interface UserAndFaceDao {
@Transaction // 保障事务
@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();
}
关联表数据插入注意
- 保障事务
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);
}
});
五、 数据库数据加密
5.1 文件加密
SQLCipher
SQLCipher是一个在SQLite基础之上进行扩展的开源数据库,它主要是在SQLite的基础之上增加了数据加密功能。
- 加密性能高、开销小,只要5-15%的开销用于加密
- 完全做到数据库100%加密
- 采用良好的加密方式(CBC加密模式——密文分组链接模式)
- 使用方便,做到应用级别加密
- 采用OpenSSL加密库提供的算法
- 开源,支持多平台
- SQLCipjer加密原理介绍
Realm
Realm 介绍:
- Realm 是一个 MVCC (多版本并发控制)数据库,
- 由Y Combinator公司在2014年7月发布一款支持运行在手机、
- 平板和可穿戴设备上的嵌入式数据库,目标是取代SQLite。
- Realm 本质上是一个嵌入式数据库,他并不是基于SQLite所构建的。
- 它拥有自己的数据库存储引擎,可以高效且快速地完成数据库的构建操作。
- 和SQLite不同,它允许你在持久层直接和数据对象工作。
- 在它之上是一个函数式风格的查询api,众多的努力让它比传统的SQLite 操作更快 。
Realm 加密:
- 借助 Realm,我们可以轻松地进行加密,
- 因为我们可以轻松地决定数据库内核所应该做的事情。
- 内部加密和通常在 Linux 当中做的加密哪样很类似。
- 因为我们对整个文件建立了内存映射,
- 因此我们可以对这部分内存进行保护。
- 如果任何人打算读取这个加密的模块,
- 我们就会抛出一个文件系统警告“有人正视图访问加密数据。
- 只有解密此模块才能够让用户读取。”通过非常安全的技术我们有一个很高效的方式来实现加密。
- 加密并不是在产品表面进行的一层封装,而是在内部就构建好的一项功能。
5.2 内容加密
- 在存储数据时加密内容,在查询时进行解密。但是这种方式不能彻底加密,数据库的表结构等信息还是能被查看到,另外检索数据也是一个问题。
加密算法 | 描述 | 优点 | 缺点 |
---|---|---|---|
DES,3DES | 对称加密算法 | 算法公开、计算量小、加密速度快、加密效率高 | 双方都使用同样密钥,安全性得不到保证 |
AES | 对称加密算法 | 算法公开、计算量小、加密速度快、加密效率高 | 双方都使用同样密钥,安全性得不到保证 |
XOR | 异或加密 | 两个变量的互换(不借助第三个变量),简单的数据加密 | 加密方式简单 |
Base64 | 算不上什么加密算法,只是对数据进行编码传输 | ||
SHA | 非对称加密算法。安全散列算法,数字签名工具。著名的图片加载框架Glide在缓存key时就采用的此加密 | 破解难度高,不可逆 | 可以通过穷举法进行破解 |
RSA | 非对称加密算法,最流行的公钥密码算法,使用长度可变的秘钥 | 不可逆,既能用于数据加密,也可以应用于数字签名 | RSA非对称加密内容长度有限制,1024位key的最多只能加密127位数据 |
MD5 | 非对称加密算法。全程:Message-Digest Algorithm,翻译为消息摘要算法 | 不可逆,压缩性,不容易修改,容易计算 | 穷举法可以破解 |
5.3 Room数据库数据库加密
- SQLCipher并不直接支持Room的数据库进行加密,所以没法直接实现。
- 可以通过开源库(swac-saferoom)进行数据加密(底层也是通过SQLCipher对数据库文件加密)
集成 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与其他数据库对比
参数 | 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 |
七、 数据库调试工具分享
debug调试
使用debug-db 可以在浏览器查看表结构及数据
普通数据库
- implementation 'com.amitshekhar.android:debug-db:1.0.6'
加密数据库
debug {
resValue("string", "PORT_NUMBER", "8081")
resValue("string", "DB_PASSWORD_PERSON", "123456")
}
implementation 'com.amitshekhar.android:debug-db-encrypt:1.0.6'