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'
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容