Room数据库存储自定义数据类和List类型以及碰到的一些坑

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数据库存储自定义数据类和List类型以及碰到的一些坑_第1张图片

1.使用自定义类型

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 来转换类型。这个方法我先不介绍,其实有更加简单的方式来实现。

1.1 @Embedded注解

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方式了。

1.2 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!

你可能感兴趣的:(Android)