Room 是 Google 官方对 SqliteDatabase 的封装库,本文列举了 Room 数据库组件的基本操作。
Room 官方文档:https://developer.android.google.cn/training/data-storage/room可与 Sqlite 的操作对比来看:
文章目录
- 一、数据库
- 1.1 定义
- 1.2 获取
- 1.2.1 初始化
- 1.2.2 升降级
- 二、表
- 2.1 定义
- 2.2 操作
- 三、数据
- 3.1 定义
- 3.2 操作
- 四、运行
- 4.1 普通运行
- 4.2 RxJava 运行
- 五、SQLite 替换为 Room
- 六、db 文件覆盖升级
- 七、其他
一、数据库
1.1 定义
定义一个数据库需要继承 RoomDatabase,并在注解中提供 entities 和 version。它相当于定义了一个 SQLiteOpenHelper,并在其 onCreate 回调中执行创表语句。
entities 即数据库中数据的实体类,用来生成表。
version 即数据库的版本号。
@Database(entities = {City.class}, version = 1)
public abstract class ChinaDatabase extends RoomDatabase {
public abstract CityDao cityDao();
}
在编译过后,Room 会自动生成 ChinaDatabase 的实现类 ChinaDatabase_Impl。
1.2 获取
通过 Room.databaseBuilder() 来生成一个 RoomDatabase,同时对它进行一些配置,如初始化处理、升降级处理等。
build 不会阻塞主线程,可以在主线程执行。
public static void accessDb(Context context) {
chinaDb = Room.databaseBuilder(context, ChinaDatabase.class, "china-room")
.build();
}
1.2.1 初始化
在 build 时,可以添加 Callback,相当于 SQLiteOpenHelper 中的 onCreate 回调。可以在这里进行数据库的初始化操作,如添加一些数据等(SQLiteDatabase 中 onCreate 经常用来建表,但在 Room 中,已经在定义 RoomDatabase 时通过注解生成了)。
这里不能操作 chinaDb(还没初始化),但可以操作 SQLiteDatabase 的封装 db,通过 db 来 execSQL()。
public static void accessDb(Context context) {
chinaDb = Room.databaseBuilder(context, ChinaDatabase.class, "china-room")
.addCallback(new RoomDatabase.Callback() {
@Override
public void onCreate(@NonNull SupportSQLiteDatabase db) {
db.execSQL("insert into city (id, name) values (1, '上海')");
}
})
.build();
}
1.2.2 升降级
Room 数据库的升降级通过 Migration 来实现,在 build 时,给 RoomDatabase 加入一个 Migration。
Migration 指明了负责的版本变化路径,即从 startVersion 到 endVersion。当版本号的变化符合该 Migration,那 migrate 就会执行。如版本号 1->2,那 Migration(1, 2) 会执行;版本号 2->1,那 Migration(2, 1) 会执行。
如果当前的版本变化路径没有设置相应的 Migration,那就会抛出一个异常。可以通过 build 时 设置 fallbackToDestructiveMigration 来避免,但这会清除当前数据库的所有数据来重新创建。
如果修改了数据库的模式(修改了 RoomDatabase 的定义),但没有修改版本号,也会抛出一个异常。
public static void accessDb(Context context) {
chinaDb = Room.databaseBuilder(context, ChinaDatabase.class, "china-room.db")
.addMigrations(new Migration(1, 2) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("insert into city (city_id, city_name) values ('4', '杭州')");
}
})
.build();
}
onCreate 回调、migrate 回调在通过 Dao 执行数据操作时才会执行,所以默认必须在子线程执行。
RoomDatabase 是对 SQLiteOpenHelper 的封装。在 build 时,相当于 new 了一个 SQLiteOpenHelper,但没有调 SQLiteOpenHelper 的 getWritableDatabase 方法,即没有生成一个 SQLiteDatabase 对象。所以 SQLiteOpenHelper 的 onCreate、onUpgrade 方法不会执行,RoomDatabase 的 onCreate、migrate 回调也就没有执行。
二、表
2.1 定义
表的定义与数据的定义合到了一起。定义一个数据,就相当于定义了一个表。
@Entity(tableName = "city")
public class City {
@PrimaryKey(autoGenerate = true)
public int id;
@ColumnInfo(name = "city_id")
public String cityId;
@ColumnInfo(name = "city_name")
public String cityName;
}
2.2 操作
表的创建、修改、删除都只能在定义阶段进行,在编码时修改数据实体类、数据库类和它们的注解来实现。
@Database(entities = {City.class}, version = 1)
public abstract class ChinaDatabase extends RoomDatabase {
public abstract CityDao cityDao();
}
也可以通过 usaDatabase.getOpenHelper().getWritableDatabase() 获得 SupportSQLiteDatabase 对象,或在 RoomDatabase build 时通过 onCreate、migrate 回调中的 SupportSQLiteDatabase 对象进行 db.execSQL("")
操作来修改表,但这会打乱所有的东西,产生异常。
三、数据
3.1 定义
如上节所述,与表的定义合在一起。
3.2 操作
Room 对数据的操作通过 Dao (data access objects)来实现。
Dao 可以是接口,也可以是抽象类。
@Dao
public interface CityDao {
@Insert
void insertCity(City city);
@Delete
void deleteCity(City city);
@Delete
void deleteAllCity(City...city);
@Update
void updateCity(City city);
@Query("select * from city")
List<City> getAllCity();
@Query("select * from city where city_id = (:cityId)")
City getCity(String cityId);
}
@Insert、@Delete、@Update 方法都可以选择返回一个数值,用来标识执行成功的条数。
@Query 中的 sql 语句会在编译时检测,如果不正确会编译失败。
在编译过后,Room 会自动生成 CityDao 的实现类 CityDao_Impl。
注意:sql 表名称是纯小写的,如果类是 UsaCity,sql 语句中应该是 usa_city。
四、运行
4.1 普通运行
Room 默认不支持在主线程进行数据访问,因为有可能阻塞线程。通过在 build 时调用 allowMainThreadQueries()
可去除这个限制。
Dao 的操作最终都是通过 getWritableDatabase.xxx()
来实现的。
//DbActivity.java
public void onCreate(Bundle savedInstanceState) {
...
new Thread(new Runnable() {
@Override
public void run() {
SqliteBehavior.behave(DbActivity.this);
}
}).start();
}
//SqliteBehavior.java
public static void behave(Context context) {
accessDb(context);
CityDao cityDao = chinaDb.cityDao();
cityDao.deleteAllCity(cityDao.getAllCity().toArray(new City[0]));
Log.d(TAG, "after deleteAll: " + cityDao.getAllCity());
cityDao.insertCity(new City("1", "北京"));
cityDao.insertCity(new City("2", "上海"));
cityDao.insertCity(new City("3", "广州"));
Log.d(TAG, "after insert: " + cityDao.getAllCity());
City city = cityDao.getCity("3");
city.setCityName("深圳");
cityDao.updateCity(city);
Log.d(TAG, "after update: " + cityDao.getAllCity());
cityDao.deleteCity(city);
Log.d(TAG, "after delete: " + cityDao.getAllCity());
}
输出
D/database-room: after deleteAll: []
D/database-room: after insert: [City{id=4, cityId='1', cityName='北京'}, City{id=5, cityId='2', cityName='上海'}, City{id=6, cityId='3', cityName='广州'}]
D/database-room: after update: [City{id=4, cityId='1', cityName='北京'}, City{id=5, cityId='2', cityName='上海'}, City{id=6, cityId='3', cityName='深圳'}]
D/database-room: after delete: [City{id=4, cityId='1', cityName='北京'}, City{id=5, cityId='2', cityName='上海'}]
4.2 RxJava 运行
todo
五、SQLite 替换为 Room
将原有的 SQLite 替换为 Room,只需创建 Room 所需类: RoomDatabase、CityDao、City,替换即可。
注意点:
- 给 RoomDatabase 的版本号 +1
否则会报错:
java.lang.IllegalStateException: Room cannot verify the data integrity.
Looks like you've changed schema but forgot to update the version number.
You can simply fix this by increasing the version number.
- 添加 Migration(old, old + 1)
否则会报错,缺少 Migration。 - 建表语句要完全一致,即字段的名称、约束都要完全一致
Room 中数据的实体类中,int 这样的基本类型,在创表时,会添加 not null 约束。
而如果之前的 SQLite 数据库的建表语句中没有 not null 约束,那迁移时就会报错:
java.lang.IllegalStateException: Migration didn't properly handle city(com.gdeer.gdtesthub.db.room.City).
Expected:
TableInfo{name='city', columns={city_name=Column{name='city_name', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0}, id=Column{name='id', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=1}, city_id=Column{name='city_id', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0}}, foreignKeys=[], indices=[]}
Found:
TableInfo{name='city', columns={city_name=Column{name='city_name', type='text', affinity='2', notNull=false, primaryKeyPosition=0}, id=Column{name='id', type='integer', affinity='3', notNull=false, primaryKeyPosition=1}, city_id=Column{name='city_id', type='text', affinity='2', notNull=false, primaryKeyPosition=0}}, foreignKeys=[], indices=[]}
可以看到 Expected 的 id 的 notNull 是 ture,Found 的 id 的 notNull 是 false。
要解决这个问题,将 Room 的 City 实体类的 id 改为 Integer 类型,这样建表时就不会添加 not null 约束了。
如果有的列需要 not null 约束,那给那个字段加上 @NonNULL 的注解就好。
六、db 文件覆盖升级
通过 db 文件覆盖升级时,不用修改 RoomDatabase 的版本号,在 build 之前替换掉文件即可。
修改版本号,添加 Migration,在 migrate 中覆盖反而有问题,会抛异常 SQLiteException: attempt to write a readonly database
。
七、其他
- setJournalMode()
RoomDatabase 在 build 时可以通过 setJournalMode() 来设置读写模式。读写模式分为 TRUNCATE
、WRITE_AHEAD_LOGGING
、AUTOMATIC
。默认是 AUTOMATIC,它会判断手机版本号,16及以上会使用 WRITE_AHEAD_LOGGING,以下会使用 TRUNCATE。
当使用 TRUNCATE,数据库读写都不能并发。数据会实时写入 db。
当使用 WRITE_AHEAD_LOGGING,数据库读可并发,写不能并发。数据不会实时写入 db(可能在 wal 文件内)。