Android Room数据库学习笔记
谷歌推的,维护、稳定性各方面都有保障一点。新项目还是值得使用的,不过封装得再好要上手还是挺麻烦的,尤其需求比较奇葩的时候,得花时间学习下。
Room 包含 3 个主要组件:数据库、Entity和DAO
官方文档
导入:
dependencies {
def room_version = "2.2.5"
// 这两个是必要库
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
// RxJava支持库
implementation "androidx.room:room-rxjava2:$room_version"
// 我也不知道是啥
implementation "androidx.room:room-guava:$room_version"
// 测试支持
testImplementation "androidx.room:room-testing:$room_version"
}
一、Database
@Database(
version = 2,
entities = {
Device.class,
DeviceCollectionInfo.class,
Manufacturer.class,
User.class,
UserRelateDevice.class
}
)
@TypeConverters(value = {BaseCollectionData.class})
public abstract class LzDatabase extends RoomDatabase {
static LzDatabase getDatabase(Context context) {
if (context == null) {
throw new NullPointerException("context is null");
}
RoomDatabase.Builder<LzDatabase> builder = Room.databaseBuilder(context.getApplicationContext(),
LzDatabase.class, "lz_db.db");
// 内存中创建数据库,不用了就会被回收
// builder = Room.inMemoryDatabaseBuilder(context.getApplicationContext(), BookDB.class);
// 添加构建SupportSQLiteOpenHelper的工厂,SQLiteOpenHelper就是在里面实现的,默认是FrameworkSQLiteOpenHelperFactory
// builder.openHelperFactory(null);
// 数据库迁移操作添加,相当于SQLiteOpenHelper.onUpgrade的一些操作
builder.addMigrations(MIGRATION_1_2);
builder.allowMainThreadQueries(); // 允许主线程执行查询操作
// 有些APP的数据库需要预置数据,这两个就是导入预置数据库的
// builder.createFromAsset();
// builder.createFromFile()
// 多进程需要调用,这个在inMemoryDatabaseBuilder没用
// builder.enableMultiInstanceInvalidation();
// 允许破坏性迁移,数据会被丢弃
// builder.fallbackToDestructiveMigration();
// 允许特定版本迁移到当前版本进行破坏性迁移,数据会被丢弃
// builder.fallbackToDestructiveMigrationFrom(2, 3);
// 允许降级破坏性迁移,数据会被丢弃
// builder.fallbackToDestructiveMigrationOnDowngrade();
// 这个默认就好,默认是WRITE_AHEAD_LOGGING
// builder.setJournalMode(JournalMode.WRITE_AHEAD_LOGGING);
// 看名称就知道是啥,这两个都是ArchTaskExecutor,是fixed线程池,核心线程4个,一般来说是够用的
// builder.setQueryExecutor();
// builder.setTransactionExecutor();
// builder.addCallback(new Callback() {
// @Override
// public void onCreate(@NonNull SupportSQLiteDatabase db) {
// // 第一次创建数据库调用
// super.onCreate(db);
// }
//
// @Override
// public void onOpen(@NonNull SupportSQLiteDatabase db) {
// // 数据库打开调用
// super.onOpen(db);
// }
//
// @Override
// public void onDestructiveMigration(@NonNull SupportSQLiteDatabase db) {
// // 破坏性迁移
// super.onDestructiveMigration(db);
// }
// });
return builder.build();
}
private static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE user ADD COLUMN age interger NOT NULL default 10");
}
};
public abstract DeviceCollectionInfoDao getCollectionInfoDao();
public abstract DeviceDao getDeviceDao();
public abstract ManufacturerDao getManufacturerDao();
public abstract UserDao getUserDao();
public abstract UserRelateDeviceDao getUserRelateDeviceDao();
public static class LzDatabaseFactory {
private static Context context;
private static volatile LzDatabase database;
public static void initContext(Context context) {
LzDatabaseFactory.context = context.getApplicationContext();
}
public static LzDatabase build() {
if (context == null) {
throw new NullPointerException("LzDatabaseFactory.initContext has't called");
}
if (database == null) {
synchronized (LzDatabaseFactory.class) {
if (database == null) {
database = LzDatabase.getDatabase(context);
}
}
}
return database;
}
}
}
以上就会构建一个数据库,最好用单例模式,getDatabase可以写在任何地方,当然从封装得角度来说,写这里最好。RoomDatabase有三个抽象方法,都由apt生成不用管。
- @Database
- version是数据库版本
- entities表实体,会根据这些实体来创建数据库表
- views视图,会根据这些视图定义类来构建视图
- exportSchema是否允许导出架构,默认true
getOpenHelper可以获取读/写数据库(实现在FrameworkSQLiteDatabase),实际上就是在调用SQLiteDatabase。
这里需要特别注意的是allowMainThreadQueries,建议调用,dao生成的代码都是在调用方的线程中执行的,如果在主线程中调用,而你没有设置这个,就会被断言报错。
这两个线程池在源码中仅在livedata的体系中有调用。
- 数据迁移
在Migration中执行一些操作,构建数据库时调用addMigrations进行添加,针对数据库版本变动执行,两个参数都是数据库版本。理论上在这里面干啥都行,但是最好只做版本间的必要变动(别浪,这玩意乱来可能让你的数据库回到解放前),其实就是onUpgrade里面的一些操作,写过数据库升级的很容易理解,这玩意比那更直观。这个可以多个组成一个路径,即1-> 2 + 2-> 3 = 1-> 3。
fallbackToDestructiveMigration、fallbackToDestructiveMigrationFrom、fallbackToDestructiveMigrationOnDowngrade都是处理破坏性迁移的,但是尽量不要走这种方式,通过设计来避免这种问题是最好的方式
-
@DatabaseView
- value:查询语句
- viewName:视图名
-
@TypeConverter和@TypeConverters
public class BaseCollectionData {
public static final int THERMAL_MONITOR = 1;
public static final int WATCH = 2;
@SerializedName("clzName")
private String clzName;
{
clzName = this.getClass().getName();
}
public String getClzName() {
return clzName;
}
@TypeConverter
public static BaseCollectionData fromJson(String json) {
JsonElement element = JsonParser.parseString(json);
if (element == null || !element.isJsonObject()) {
return null;
}
JsonObject obj = element.getAsJsonObject();
if (!obj.has("clzName")) {
return null;
}
try {
String clzName = obj.get("clzName").getAsString();
if (TextUtils.isEmpty(clzName)) {
return null;
}
return (BaseCollectionData) new Gson().fromJson(json, Class.forName(clzName));
} catch (Exception ignore) {
}
return null;
}
@TypeConverter
public static String toJson(BaseCollectionData person) {
return new Gson().toJson(person);
}
}
@Entity(
tableName = "collection_info",
foreignKeys = @ForeignKey(entity = Device.class, parentColumns = "id", childColumns = "device_id", onDelete = ForeignKey.CASCADE)
)
public class DeviceCollectionInfo {
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
public long id;
@ColumnInfo(name = "device_id", index = true)
public long deviceId;
@ColumnInfo(name = "data")
public BaseCollectionData data;
}
这个是用来自定义数据类型的,这个设计可能非常有用。
不是所有的数据都去直接建表的,一些拓展性比较强的数据建表会造成表的频繁变动,很可能会使用json来统合数据,这个时候可以让相应的继承上面的的类来避免自己一个个的去写解析。上面都是写的静态方法,可以写普通方法,加好@TypeConverter就行了。TypeConverter需要通过@TypeConverters注册到数据库类上
二、Entity
这一块有两种:建表实体和关系实体。
1. 建表实体
@Entity(
tableName = "device",
foreignKeys = @ForeignKey(entity = Manufacturer.class, parentColumns = "id", childColumns = "manufacturer_id", onDelete = ForeignKey.CASCADE)
)
public class Device {
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
public long id;
@ColumnInfo(name = "name")
public String name;
@ColumnInfo(name = "manufacturer_id", index = true)
public long manufacturerId;
@ColumnInfo(name = "device_type")
public int deviceType;
}
@Entity(tableName = "user")
public class User {
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
public long id;
@ColumnInfo(name = "name", collate = UNICODE)
public String name;
@ColumnInfo(name = "age", defaultValue = "10")
public int age;
@Embedded(prefix = "user_")
public BodyInfo bodyInfo;
@Ignore
public String test;
}
public class BodyInfo {
@ColumnInfo(name = "height")
public int height;
@ColumnInfo(name = "weight")
public float weight;
}
上面就是一个简单合格的建表实体了,必须实现get和set方法或者把属性写成public,不然报错。
-
@Entitiy:实体
- tableName:表名,最好是写下,不写就会直接使用类名(注意混淆),不区分大小写
- indices:索引,索引里面有unique标识,需要unique可以在这里配置
- inheritSuperIndices:继承父类索引
- primaryKeys:主键,单个主键用@PrimaryKey比较好(直观),如果是交叉表可以使用这个来配置组合外键
- foreignKeys:外键
- ignoredColumns:忽略属性,用@Ignore就好了,比较直观
-
@PrimaryKey:主键
- autoGenerate:主键是否自增,默认是false(感觉用自增的情况多一点,为啥默认false)
-
@ColumnInfo:列
- name:列名,最好写下,不写就会直接使用属性名(注意混淆),不区分大小写
- typeAffinity:列数据类型,没必要写,room会自己推断
- index:true:构建列索引
- collate:BINARY:大小写敏感;NOCASE:大小写不敏感;RTRIM:首尾空格忽略;LOCALIZED:系统编码应用;UNICODE:unicode编码应用(感觉就这个可能有用,类似颜文字的编码写入数据库会报错,但是能不使用就不要使用)
- defaultValue:列默认值
-
@Ignore:忽略,能忽略方法、构造器和属性,因为room会根据entity进行推断,随便写个属性也会被识别为列,当entity存在不希望建表的列时,需要进行忽略
-
@ForeignKey:外键,这个没有配置@Target,直接放在Entitiy里面啊,别像我写在属性上,这是无效的
- entity:父表的实体
- parentColumns:父表的对应列
- childColumns:子表列名
- onDelete:父表元素删除时,关联的子表元素的关联操作。默认NO_ACTION;CASCADE:同步删除或更新;RESTRICT:存在子表记录时,父表记录不允许删除或更新,受deferred影响;SET_NULL:赋空;SET_DEFAULT:赋列默认值;
- onUpdate:和onDelete一样,约束更新操作;
- deferred:延迟外键约束,只在提交的时候进行约束检查。外键约束
-
@Fts3/@Fts4:虚表,模糊查询对比普通表会有极大的性能提高(还有没有啥特性我也不知道,毕竟没用过),官方文档
-
@Embedded:镶嵌实体,把本该在一个实体中的部分属性分离到另一个实体中(强迫症晚期必备),和写在一个里面没啥区别,建表时,字段在同一个表中。
- prefix:防字段冲突的,加个前缀,会和ColumnInfo的name拼接,当然没写ColumnInfo就会和推断名拼接
2. 关系实体
public class UserAndDevice {
@Embedded
public UserRelateDevice userRelateDevice;
@Relation(
parentColumn = "user_id",
entityColumn = "id",
entity = User.class
)
public User user;
@Relation(
parentColumn = "device_id",
entityColumn = "id",
entity = Device.class
)
public DeviceAndManufacturer device;
}
public class DeviceAndManufacturer {
@Embedded
public Device device;
@Relation(
parentColumn = "manufacturer_id",
entityColumn = "id"
)
public Manufacturer manufacturer;
}
public class DevicesBelongToUser {
@Embedded
public User user;
@Relation(
parentColumn = "id",
entityColumn = "id",
associateBy = @Junction(value = UserRelateDevice.class, parentColumn = "user_id", entityColumn = "device_id"),
entity = Device.class
)
public List<DeviceAndManufacturer> devices;
}
-
@Relation
- entity:关联实体:必须是建表实体,和entityColumn对应
- parentColumn:父实体关联字段,就是@Embedded注释实体里面的字段
- entityColumn:关联实体中对应的字段,如果@Relation注释的字段本身就是一个关系实体,那就对应其@Embedded注释的实体
- associateBy:父实体和关联实体本身没有关联关系,需要其它实体来提供关联关系,如果DevicesBelongToUser
- projection:可以通过这个字段设置查询输出。比如@Relation注释的是新建的类,其仅包含entity的部分字段,通过这个字段就可以对应设置。
-
@Junction
- value:提供关联关系的关系实体
- parentColumn:value对应实体里面的字段,和Relation.parentColumn对应
- entityColumn:value对应实体里面的字段,和Relation.entityColumn对应
这一部分并不难,奈何官方文档的举例太简单,测试了好久才搞明白这一块咋设计的,查询挺好用。
关系实体首先最重要的是@Embedded,这个是查询语句查询的实体,每个关系实体都必须有一个@Embedded修饰的实体,利用这个实体去关联其它实体。@Relation注释的可以是建表实体,也可以是关系实体,关系实体里面又可以关联其它实体(禁止套娃),怎么关联在上面已经解释清楚了,不详说。
三、DAO
@Dao
public interface DeviceDao {
@Insert
void insert(Device device);
@Insert
@Transaction
void bulkInsert(List<Device> list);
@Delete
void delete(Device device);
@Update
void update(Device device);
@Query("SELECT * FROM device WHERE id = :id")
Device queryById(long id);
@Query("SELECT * FROM device WHERE name = :name")
Device queryByName(String name);
@Query("SELECT * FROM device")
List<Device> queryAll();
@Query("SELECT * FROM device")
@Transaction
List<DeviceAndManufacturer> queryTakeManufacturer();
@Query("DELETE FROM device")
void clearTable();
@Query("update SQLITE_SEQUENCE set seq = 0 where name='device'")
void resetSeq();
@Transaction
@RawQuery(observedEntities = {UserRelateDevice.class})
LiveData<List<DeviceAndManufacturer>> rawQueryDevice(SupportSQLiteQuery query);
@Transaction
@Query("select * from device where id in (SELECT device_id from (select device_id, COUNT(*) as c from user_relate_device GROUP BY device_id) where c > 1)")
LiveData<List<DeviceAndManufacturer>> querySharedDevice();
}
- dao里面的注解都特别简单,不细讲。增删改的注释根据参数可以是单个处理也可以是批量处理,自己选择就好,批量处理最好加上事务@Transaction。
- 关系实体查询是需要@Transaction的,一般都会涉及多个表的查询。
- 增改的onConflict:ABORT:发生冲突时,事务回滚;REPLACE:发生冲突时,替换已存在记录;IGNORE:忽略冲突保留已存在的记录(增有这些正常,改是啥情况,修改被拒绝?搞不明白)
- @RawQuery和@Query,这两个有些区别:
- RawQuery必须有返回值,Query更随意一点;
- 在查询监控(LiveData)上,Query不需要配置监听对象,RawQuery需要。比如上面的querySharedDevice和rawQueryDevice用同一查询时,如果不写observedEntities = {UserRelateDevice.class},那UserRelateDevice的变化不会引起rawQueryDevice查询的监听回调;
- 在查询监控(LiveData)上,它们都会根据返回值推断要监控的变化。
- @SkipQueryVerification:跳过sql语句校验,上面这个dao除了rawQueryDevice都可以配置,已经是定型的语句,有没有问题早就验过了,确实没必要检查,但是我懒得写,也感觉没啥必要去纠结这个。