Room 是 Google 官方对 SqliteDatabase 的封装库,本文列举了 Room 数据库组件的基本操作。
Room 官方文档:https://developer.android.google.cn/training/data-storage/room可与 Sqlite 的操作对比来看:https://blog.csdn.net/Gdeer/article/details/88869051
定义一个数据库需要继承 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。
通过 Room.databaseBuilder() 来生成一个 RoomDatabase,同时对它进行一些配置,如初始化处理、升降级处理等。
build 不会阻塞主线程,可以在主线程执行。
public static void accessDb(Context context) {
chinaDb = Room.databaseBuilder(context, ChinaDatabase.class, "china-room")
.build();
}
在 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();
}
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 回调也就没有执行。
表的定义与数据的定义合到了一起。定义一个数据,就相当于定义了一个表。
@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;
}
表的创建、修改、删除都只能在定义阶段进行,在编码时修改数据实体类、数据库类和它们的注解来实现。
@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("")
操作来修改表,但这会打乱所有的东西,产生异常。
如上节所述,与表的定义合在一起。
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。
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='上海'}]
todo
将原有的 SQLite 替换为 Room,只需创建 Room 所需类: RoomDatabase、CityDao、City,替换即可。
注意点:
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 文件覆盖升级时,不用修改 RoomDatabase 的版本号,在 build 之前替换掉文件即可。
修改版本号,添加 Migration,在 migrate 中覆盖反而有问题,会抛异常 SQLiteException: attempt to write a readonly database
。
RoomDatabase 在 build 时可以通过 setJournalMode() 来设置读写模式。读写模式分为 TRUNCATE
、WRITE_AHEAD_LOGGING
、AUTOMATIC
。默认是 AUTOMATIC,它会判断手机版本号,16及以上会使用 WRITE_AHEAD_LOGGING,以下会使用 TRUNCATE。
当使用 TRUNCATE,数据库读写都不能并发。数据会实时写入 db。
当使用 WRITE_AHEAD_LOGGING,数据库读可并发,写不能并发。数据不会实时写入 db(可能在 wal 文件内)。