Room使用详解及常用数据库对比

Android Jetpack组件 —— Room使用详解及常用数据库对比

一、 Room介绍

  • Room是Jetpack组件中一个对象关系映射(ORM)库。可以很容易将 SQLite 表数据转换为 Java 对象。
  • Room 在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库。
  • 支持与LiveData、RxJava、Kotlin协成组合使用。
  • Google 官方强烈推荐使用Room。
image.png

二、 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)
image

四、 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
image

七、 数据库调试工具分享

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'
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容