Room数据库是Google官方Jetpack框架中推荐的数据库组件,虽然其速度和不使用Sqlite的Realm数据库比还是有一些差距,但是其可以直接返回LiveData对象或者RxJava的Observable对象,使用起来十分方便。
之前我写过两篇有关于Room的博客
LiveData结合Room数据库使用以及线程问题:https://blog.csdn.net/weixin_44666188/article/details/105500779
Android使用AIDL共享Room数据库:
https://blog.csdn.net/weixin_44666188/article/details/105582000
当时演示都是使用最基本的Room数据类型,今天记录一下复杂类型的使用方法吧。
Room如果碰到自定义类型嵌套自定义类型的情况下怎么办?
比如一个User类型中包含有一个Family类型:
data class User(
var id:Long = 0,
var name:String?=null,
var age:Int = 0,
var sex:String?=null
var family:Family ?=null
)
data class Family(
var id:Long = 0,
var father:String?=null,
var mother:String?=null
)
如果在使用Room的时候Entiy中直接使用地定义类型编译时就会直接报错,比如这么写
@Entity(tableName = "user")
data class User(
@PrimaryKey
@ColumnInfo(id= "id")
var id:Long = 0,
@ColumnInfo(name= "name")
var name:String?=null,
@ColumnInfo(age= "age")
var age:Int = 0,
@ColumnInfo(sex= "sex")
var sex:String?=null
@ColumnInfo(family= "family")
var family:Family ?=null
)
报错
cannot figure out how to save this field into database. You can consider adding a type converter for it.
编译器提示我们应该使用一个转换器去转换这个非基本类型。网上搜到的大部分解决方案也是使用Type Converter 来转换类型。这个方法我先不介绍,其实有更加简单的方式来实现。
Room提供了一个 @Embedded 注解来帮助我们解决这个问题。
贴一下官方注释:
/**
* Can be used as an annotation on a field of an {@link Entity} or {@code Pojo} to signal that
* nested fields (i.e. fields of the annotated field's class) can be referenced directly in the SQL
* queries.
*
* If the container is an {@link Entity}, these sub fields will be columns in the {@link Entity}'s
* database table.
*
* For example, if you have 2 classes:
*
* public class Coordinates {
* double latitude;
* double longitude;
* }
* public class Address {
* String street;
* {@literal @}Embedded
* Coordinates coordinates;
* }
*
* Room will consider {@code latitude} and {@code longitude} as if they are fields of the
* {@code Address} class when mapping an SQLite row to {@code Address}.
*
* So if you have a query that returns {@code street, latitude, longitude}, Room will properly
* construct an {@code Address} class.
*
* If the {@code Address} class is annotated with {@link Entity}, its database table will have 3
* columns: {@code street, latitude, longitude}
*
* If there is a name conflict with the fields of the sub object and the owner object, you can
* specify a {@link #prefix()} for the items of the sub object. Note that prefix is always applied
* to sub fields even if they have a {@link ColumnInfo} with a specific {@code name}.
*
* If sub fields of an embedded field has {@link PrimaryKey} annotation, they will not be
* considered as primary keys in the owner {@link Entity}.
*
* When an embedded field is read, if all fields of the embedded field (and its sub fields) are
* {@code null} in the {@link android.database.Cursor Cursor}, it is set to {@code null}. Otherwise,
* it is constructed.
*
* Note that even if you have {@link TypeConverter}s that convert a {@code null} column into a
* {@code non-null} value, if all columns of the embedded field in the
* {@link android.database.Cursor Cursor} are null, the {@link TypeConverter} will never be called
* and the embedded field will not be constructed.
*
* You can override this behavior by annotating the embedded field with
* {@link androidx.annotation.NonNull}.
*/
简单来说就是使用 @Embedded 标识的嵌套字段可以直接在SQL中引用。
那么之前那个例子可以这么写:
@Entity(tableName = "user")
data class User(
@PrimaryKey
@ColumnInfo(id= "id")
var id:Long = 0,
@ColumnInfo(name= "name")
var name:String?=null,
@ColumnInfo(age= "age")
var age:Int = 0,
@ColumnInfo(sex= "sex")
var sex:String?=null
@Embedded
var family:Family ?=null
)
@Entity(tableName = "family")
data class Family(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "faimly_id")
var id:Long = 0,
@ColumnInfo(name = "faimly_father")
var father:String?=null,
@ColumnInfo(name = "faimly_mother")
var mother:String?=null
)
这样写就没有问题了。
这里我们给Family也定义了一张表,使用 @Embedded后User中嵌套Family类,这其实是把两张表嵌套成了一张,这会带来一些问题,比如字段重名。
User类中有id字段,Family中也有id字段,这里我把Family中的id字段重命名为faimly_id了,如果也写为id和User类中的一样,那么就会报错:
Multiple fields have the same columnName: id. Field names: id, user > id.
除了这种重名情况还有一种坑也会报重名异常
一个类型被多次嵌套的情况:
比如换一种复杂点的情况,有一个User基本类型,还有一个Employee类型与Manager类型
@Entity(tableName = "user")
data class User(
@PrimaryKey
@ColumnInfo(id= "id")
var id:Long = 0,
@ColumnInfo(name= "name")
var name:String?=null,
@ColumnInfo(age= "age")
var age:Int = 0,
@ColumnInfo(sex= "sex")
var sex:String?=null
)
employee中包含manager对象和user对象
@Entity(tableName = "employee")
data class Employee(
@PrimaryKey
@ColumnInfo(id= "employee_id")
var id:Long = 0,
@Embedded
var user:User?=null
@Embedded
var manager:Manager?=null
)
manager中也包含user对象
@Entity(tableName = "manager")
data class Manager(
@PrimaryKey
@ColumnInfo(id= "manager_id")
var id:Long = 0,
@Embedded
var user:User?=null
)
此时在为employee建表的时候,employee包含user类型,为user建立了一张表,employee中还有manager类型,manager中也包含user类型,他又会给user建立一张表。被建立的两张user表是会被嵌套在一起的,这个时候自动生成的代码user中的字段全部都是重名的,编译无法通过。
这种情况下的解决方案有两种,一是为Manager中的User类型重新定义一个Entity类,其中的字段名与Employee中嵌套的字段名不能重名,比如
@Entity(tableName = "manager_user")
data class ManagerUser(
@PrimaryKey
@ColumnInfo(id= "manager_id")
var id:Long = 0,
@ColumnInfo(name= "manager_name")
var name:String?=null,
@ColumnInfo(age= "manager_age")
var age:Int = 0,
@ColumnInfo(sex= "manager_sex")
var sex:String?=null
)
然后再Manager中引用ManagerUser类型而不是User类型。但是这种方法增加了很多重复代码。
第二种方案就是前面说过的Type Converter方式了。
Room中的Type Converter其实就是让使用者自定义的类型转换工具。
向上面这种情况可以把User数据转化为Json数据存储在数据库中。
首先定义Converters类:
class UserConverters {
@TypeConverter
fun stringToUser(value: String):User{
val type = object :TypeToken<User>(){
}.type
return Gson().fromJson(value,type)
}
@TypeConverter
fun userToString(user:User): String {
val gson = Gson()
return gson.toJson(user)
}
}
在其他Entity嵌套User类型的时候就不用使用 @Embedded 注解了,而是在Entity头部使用 @TypeConverters(UserConverters::class) 注解
@Entity(tableName = "employee")
@TypeConverters(UserConverters::class)
data class Employee(
@PrimaryKey
@ColumnInfo(id= "employee_id")
var id:Long = 0,
@ColumnInfo(name = "employee_user")
var user:User?=null
@Embedded
var manager:Manager?=null
)
@Entity(tableName = "manager")
@TypeConverters(UserConverters::class)
data class Manager(
@PrimaryKey
@ColumnInfo(id= "manager_id")
var id:Long = 0,
@ColumnInfo(name = "manager_user")
var user:User?=null
)
这样就OK了,不需要再写一个User类型。
除了使用Json进行转换外任何转换方式都可以自定义,比如将Long类型的时间转化为Date类型
class DateConverter {
@TypeConverter
fun revertDate(value:Long):Date {
return Date(value);
}
@TypeConverter
fun converterDate(value:Date):long {
return value.getTime();
}
}
容器类型的数据存储也是同理,将List数据转化为Json存储
data class PicUrls(
var thumbnail_pic: String? = null
)
class PicUrlsConverters {
@TypeConverter
fun stringToObject(value: String): List<PicUrls> {
val listType = object : TypeToken<List<PicUrls>>() {
}.type
return Gson().fromJson(value, listType)
}
@TypeConverter
fun objectToString(list: List<Any>): String {
val gson = Gson()
return gson.toJson(list)
}
}
顺便提一下
aused by: java.lang.RuntimeException: cannot find implementation for com.xxx.xxxx.data.database.xxxDataBase.xxxDataBase_Impl does not exist
at androidx.room.Room.getGeneratedImplementation(Room.java:94)
at androidx.room.RoomDatabase$Builder.build(RoomDatabase.java:667)
这个异常
这种情况通常都是使用kotlin编写项目的时候引入Room依赖时使用的是java的关键字而不是kotlin的关键字导致的。
如果是Java项目,在app 的build.gradle中需要使用
annotationProcessor 'android.arch.persistence.room:compiler:1.1.1'
如果你使用的kotlin编写而用了annotationProcessor引入room compiler就会出现上述异常
kotlin项目应该在build.gradle头部先
apply plugin: 'kotlin-kapt'
然后使用kapt引入依赖
kapt 'android.arch.persistence.room:compiler:1.1.1'
这个问题大部分都是没使用kapt导致的,当然也有小部分比如我是因为自己犯二在写
abstract class XXXDataBase : RoomDatabase() {
的时候忘了使用@Database注解~
@Database(entities = [Status::class,User::class,RetweetedStatusBean::class,Visible::class],version = 1,exportSchema = false)
abstract class XXXDataBase : RoomDatabase() {
Over!