Room持久性库在SQLite上提供了一个抽象层,帮助开发者更友好、流畅的访问SQLite数据库。
以下是Room的初步使用经验,可供基础功能的使用。完整的文档请参考https://developer.android.com/training/data-storage/room
一、添加依赖
// room
implementation "androidx.room:room-runtime:2.2.6"
annotationProcessor "androidx.room:room-compiler:2.2.6"
// optional - RxJava support for Room
implementation "androidx.room:room-rxjava2:2.2.6"
//rxAndroid
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
二、创建Entity
- 一个Entity类对应一个表结构。
- 数据模型中的每个字段都会映射为数据库表中的一列。
- 必须为类添加@Entity注解,为主键添加@PrimaryKey注解。
- @Entity - 类注解
tableName
:用于指定表名,未指定时使用类名作为表名。 - @PrimaryKey - 字段注解:表明该字段是主键,一个类中一定要有一个主键。
autoGenerate
:设置主键自增,自增主键必须为int型。 - @ColumnInfo - 字段注解
name
:设置字段在表中的列名,未指定时使用字段名称。 - @Ignore - 字段注解:不需要存储在数据库中的字段可以使用该注解。
- 数据模型需要添加set、get方法供room使用。
三、创建Dao
- Dao定义为一个接口。
- 必须为类添加@Dao注解,在每个需要访问数据库中的方法上添加@Insert、@Delete、@Update、@Query注解。
- 一般情况下,对于同一个表的数据库操作会放到一个Dao类中。
- @Dao - 类注解
- @Insert - 方法注解
将传入的数据插入到数据库表中。插入数据的id会放在列表中返回。@Insert Single
- > insert(Product... products);
- @Delete - 方法注解
删除数据库表中的数据,会按照主键查找删除,会返回成功删除的数目。@Delete Single
delete(Product... products); - @Update - 方法注解
根据主键更新表中数据,会返回成功更新的数目。@Update Single
update(Product... products); - @Query - 方法注解
查询数据库的注解,需要自己书写sql语句。
此处推荐使用Flowable,在数据更新时会自动触发查询,但要在页面关闭的时候及时注销,Flowable默认在子线程查询,不需要手动切换线程。
需要传入参数进行查询的时候可以使用“:id”占位符。@Query("select * from product") Flowable
- > getAll();
@Query("select * from product where id=:id") Flowable
findById(int id); @Query("select * from product where name like :name") Flowable - > findByName(String name);
四、创建DataBase
- 此类为抽象类,需要继承自androidx.room.RoomDatabase
- 必须为类添加@Database注解。
- 对于需要暴露的Dao,请添加获取对应Dao的抽象方法。
public abstract ProductDao productDao();
- 建议使用工具类或在本类实现单例模式供整个App使用,大量创建DataBase会消耗一定的资源。
- @Database - 类注解
version
:int型的版本号,用于标识数据库的版本。在数据库升级的时候需要增加version的数字。
entities
:该字段为Class>[]类型,需要将数据库中所包含表的Entity类添加到注解中。entities的格式为“entities = {Product.class}”。
exportSchema
:控制编译器是否输出数据库结构文件,默认为true,需要配置room.schemaLocation
字段才能看到输出结果。
配置方法:
在App级别的build.gradle
文件中添加数据库结构导出配置,添加配置信息后重新编译工程,工程中的database目录下会生成当前版本数据库的表结构。(文件名为1.json
)。android { // ... defaultConfig { // ... javaCompileOptions { annotationProcessorOptions { arguments += ["room.schemaLocation": "$projectDir/database".toString()] } } } }
五、 使用
- 构建Database单例
private static final String DB_NAME = "localData.db"; private static volatile AppDatabase INSTANCE; public static AppDatabase getInstance(Context context) { if (INSTANCE == null) { synchronized (AppDatabase.class) { if (INSTANCE == null) { INSTANCE = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DB_NAME).build(); } } } return INSTANCE; }
- 进行数据插入
由于Entity中的主键设置了自增,所以在插入数据的时候不需要设置id。Product product1 = new Product(); product1.setJdID("jdId_1 by insert"); product1.setName("name_1 by insert"); Product product2 = new Product(); product2.setJdID("jdId_2 by insert"); product2.setName("name_2 by insert"); Disposable disposable = AppDatabase.getInstance(this).productDao().insert(product1, product2) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer
- >() {
@Override
public void accept(List
ids) throws Exception { System.out.println("insert number = " + ids.size()); for (long id : ids) { System.out.println("insert id = " + id); } } }, new Consumer () { @Override public void accept(Throwable throwable) throws Exception { System.out.println("insert error = " + throwable.getMessage()); throwable.printStackTrace(); } }); - 删除数据
删除数据的接口使用的是@Delete注解,自动生成的方法仅支持根据传入对象的主键进行删除,如果需要使用其他删除条件,建议使用@Query注解并手动编写sql语句。Product product = new Product(); product.setId(6); Disposable disposable = AppDatabase.getInstance(this).productDao().delete(product) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer
() { @Override public void accept(Integer integer) throws Exception { System.out.println("delete number = " + integer); } }, new Consumer () { @Override public void accept(Throwable throwable) throws Exception { System.out.println("delete error = " + throwable.getMessage()); throwable.printStackTrace(); } }); - 修改数据
修改数据的接口使用的是@Update注解,自动生成的方法仅支持根据传入对象的主键进行更新,如果需要使用其他更新条件,建议使用@Query注解并手动编写sql语句。Product product = new Product(); product.setId(2); product.setName("name by update"); Disposable disposable = AppDatabase.getInstance(this).productDao().update(product) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer
() { @Override public void accept(Integer integer) throws Exception { System.out.println("update number = " + integer); } }, new Consumer () { @Override public void accept(Throwable throwable) throws Exception { System.out.println("update error = " + throwable.getMessage()); throwable.printStackTrace(); } }); - 查询数据
查询数据方法返回的是一个Flowable,在对应表更新数据之后会重新进行查询并返回结果。
由于数据更新的单位是整张表,更新的数据不在查询范围内时依旧会重新查询并返回结果,可以使用distinctUntilChanged操作符将连续重复的数据过滤掉。Disposable disposable = AppDatabase.getInstance(this).productDao().getAll() .distinctUntilChanged() .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer
- >() {
@Override
public void accept(List
products) throws Exception { System.out.println("query number = " + products.size()); for (Product product : products) { System.out.println("query = " + product); } } }, new Consumer () { @Override public void accept(Throwable throwable) throws Exception { System.out.println("query error = " + throwable.getMessage()); throwable.printStackTrace(); } }); - 检查
以上方法返回的Disposable都未进行处理,数据库操作是在异步线程执行的,为防止回调时出现错误,请将返回的disposable与页面生命周期进行绑定,及时终止操作。
六、数据库升级
数据库的表结构很难一次性定义完整,有时会根据业务的变化而修改数据库的表结构,修改表结构就要对数据库进行升级,升级的流程如下:
- 创建或修改Entity
现在就可以根据业务需要调整数据库中的表结构了,由于表结构是映射为Entity类,所以按照需求调整Entity类就好了。 - 修改
@Database
注解中的参数- 升级版本号
- 更新
entities
中的参数,使其与新版本的表结构一致。
- 创建Migration类
- 创建一个继承自
Migration
的类或直接实现一个Migration
匿名类。 - 在构造函数中指定数据库升级时旧的版本号和新的版本号。
- 实现
migrate
方法,migrate
方法中会附带一个database
参数,直接通过database.execSQL("")
方法执行数据库表的更新语句。以下列举几个常见的数据库更新Migration
。
添加新表
添加字段private static class Migration1_2 extends Migration { public Migration1_2() { super(1, 2); } @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("CREATE TABLE IF NOT EXISTS 'log' ('id' INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 'time' INTEGER NOT NULL, 'code' INTEGER NOT NULL, 'message' TEXT)"); } }
修改表名:我之前创建的表名是Product,希望修改成product。private static class Migration2_3 extends Migration { public Migration2_3() { super(2, 3); } @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("alter table 'product' add 'enable' integer not null default '1'"); } }
private static class Migration3_4 extends Migration { public Migration3_4() { super(3, 4); } @Override public void migrate(@NonNull SupportSQLiteDatabase database) { // 数据库中的表名在使用时不区分大小写,可在展示的时候是有大写字母的,所以在修改表名的时候先转换成一个临时的表名。 database.execSQL("alter table 'Product' rename to 'product_tmp'"); database.execSQL("alter table 'product_tmp' rename to 'product'"); } }
- 创建一个继承自
- 将自定义的
Migration
对象通过addMigrations
方法添加到数据库初始化方法中,此方法可以添加不定数量的Migration
并根据版本号依次执行。Room.databaseBuilder(application.getApplicationContext(), AppDatabase.class, DB_NAME) .addMigrations( new Migration1_2(), new Migration2_3(), new Migration3_4()) .build();
- 重新编译安装app,在使用数据库的时候会自动根据上面的配置信息进行数据库升级,由于数据库升级会消耗一定的时间,第一次调用数据库的方法时间会稍长一点。
七、进阶功能
对于小型的app,基本的数据库功能就足以满足业务需求,下面这些进阶的功能暂未尝试。
- @ForeignKey注解
数据库中的外键,用于确定表与表的关系。 - @TypeConverters注解
数据库的类型转换器,复杂的数据结构可以通过类型转换器转换成简单的数据再存放到数据库中。 - @Index注解
数据库的索引,常用于大型的数据库中,可以减少超大数据量的查询时间。