Android 具有复杂对象的工作室类型转换器

Android Studio Type Converters with complex objects

在尝试使用 Room 向我的 AndroidStudio 应用添加持久性时,我遇到了这个问题:

error: Cannot figure out how to save this field into database. You can consider adding a type converter for it. private User creator;

我已经尝试过所说的类型转换器,看起来就是这样

class Converters {
    @TypeConverter
    fun usuarioToString(user: User): String {
        return user.getID().toString()
    }

    @TypeConverter
    fun stringToUser(value: String?): UUID {
        return UUID.fromString(value)
    }
}
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract val appDAO: AppDAO
    ...
}
@Entity(tableName = "element_table")
abstract class Element (
    private var IDClass: Int,
    @TypeConverters(Converters::class)
    private var creator: User
    )

我想在 类 中维护复杂的对象,但我不明白为什么类型转换器不能正常工作。

PD:我已经制作了用户和元素 类

您有很多问题。

第 1 期 - private var ....var ....

通常,在数据 Class 中只使用 var ....。但是,如果创建 private 那么对于 room 你需要有 getter 和 setter 以便能够访问成员变量。

所以你可以使用 :-

@Entity(tableName = "element_table")
abstract class Element (
    @PrimaryKey
    private var IDClass: Int,
    //@TypeConverters(Converters::class) // not needed here as defined at the @Database level
    private var creator: User
) {
    fun getIDClass(): Int {
        return IDClass
    }

    fun setIDClass(id: Int) {
        this.IDClass = id
    }

    fun getCreator(): User {
        return creator
    }

    fun setCreator(user: User) {
        this.creator = user
    }
}

或者(在这种情况下不需要 getter 和 setter,因为值可以直接访问):-

@Entity(tableName = "element_table")
abstract class Element (
    @PrimaryKey
    var IDClass: Int,
    //@TypeConverters(Converters::class) // not needed here as defined at the @Database level
    var creator: User
)

TypeConverter 的第二个问题是您需要匹配对。也就是一个从Object转换,另一个转换成Object。

前者,fromObject必须获取Object并将其转换为room可以操作的类型:-

  • 字符串,
  • 整数类型,例如 Long 或 Int,
  • 十进制类型(SQLite 术语中的 REAL),例如 Double、Float,
  • 字节数组(QLite 术语中的 BLOB)例如 ByteArray

后者,toObject,必须从数据库中获取值(因此是 fromObject 类型转换器的结果类型)并将其转换为 Type。

所以你有:-

@TypeConverter
fun usuarioToString(user: User): String {
    return user.getID().toString()
}

但是!!您没有匹配的转换器。必须向匹配的转换器传递一个字符串(fromObject 转换器 usuarioToString 的结果)和 return 一个用户。

因此而不是(returns 一个 UUID 对象):-

@TypeConverter
fun stringToUser(value: String?): UUID {
    return UUID.fromString(value)
}

你可以:-

@TypeConverter
fun stringToUser(value: String?): User {
    
    return User(????) /* construct the User based upon the String as an ID */
}

function/converter :-

@TypeConverter
fun stringToUser(value: String?): UUID {
    return UUID.fromString(value)
}

将从字符串转换为 UUID 而不是用户,因此您提供的内容没有用。

但是,不需要将 Int 转换为 String 即可存储 Int。 但是 仅仅存储 id 就足够了,如果 User 有名字和/或其他信息,id 从 Int 转换为 String 就足以构建一个实际的 User(因此 User(????)).

例如,用户 class 是:-

data class User(
    val id: Int,
    val userName: String

) {

    fun getID(): Int {
        return id
    }
}

那么仅根据 id 生成用户名将很困难,但并非不可能。有两个基本选项可以存储 ID 和用户名(以及其他详细信息)。

您可以使用更复杂的 TypeConverter 将所有成员变量(id 和 userName)转换为字符串,或者您可以为 User 使用单独的 table 并在元素和用户。

在关系数据库术语中(SQLite,因此 Room 是一个关系数据库)后者被认为是更可取的。然而,通常情况是放弃数据库的关系方面并将关系数据存储在单个列中,并且通常 JSON 字符串用于存储此类数据。

将用户存储为 JSON 字符串的示例

首先,您需要为 GSON 添加依赖项,例如:-

implementation 'com.google.code.gson:gson:2.9.0'

那么您的 TypeConverter 可能是:-

class Converters {
    @TypeConverter
    fun usuarioToString(user: User): String {

        return Gson().toJson(user)
        //return user.getID().toString()
    }

    @TypeConverter
    fun stringToUsuario(value: String): User {
        return Gson().fromJson(value,User::class.java)
        //return User(value.toInt(),"blah")
    }

}

这将保存所有用户信息,而不仅仅是 ID,例如像 :-

  • 可以看出,同一用户已被存储两次,这与规范化的不重复数据方面相矛盾。然而,它很方便(至少一开始是这样),但可能会显得笨拙,尤其是在需要根据用户信息搜索数据的情况下。

示例将用户存储在 table 中并利用元素和用户之间的关系

元素将存储相应用户的 ID,而不是使用字符串表示 JSON,因此元素 class 可以是:-

@Entity(
    tableName = "element_table",
    )
data class Element (
    @PrimaryKey
    val IDClass: Int,
    @ColumnInfo(index = true) /* optional but negates warning */
    val creator: Int
)

由于用户将是 table,用户 class 可能是:-

@Entity(tableName = "user_table")
data class User(
    @PrimaryKey
    val id: Int,
    val userName: String
)

因为你想用用户检索一个元素,所以使用了一个额外的 POJO(即不是带注释的@Entity class),例如:-

data class ElementWithUser (

    @Embedded
    var element: Element,
    @Relation(
        entity = User::class,
        parentColumn = "creator",
        entityColumn = "id"
    )
    var user: User
)
  • 这介绍了@Embedded 和@Relation 注释
    • @Embedded 基本上复制了嵌入的class

    • @Relation 用于告诉room如何获取相关item

      • 实体是从中获取相关数据的class (table)
      • parentColumn 是父 class (@Embedded) 中的列,maps/references 子 (ren) 在另一个 table
      • childColumn 是另一个 table 中被引用的列(最常见的是主键)

要使用以上内容,您需要能够:-

  • 将用户插入 user_table
  • 将元素插入 element_table
  • 有提取数据的方法

将其组合成一个带@Dao 注释的接口(或抽象 class):-

@Dao
interface ElementDao {
    @Insert
    fun insert(element: Element): Long
    @Query("SELECT * FROM element_table")
    fun getAllElements(): List<Element>
    @Insert
    fun insert(user: User): Long
    @Query("SELECT * FROM user_table")
    fun getAllUsers(): List<User>
    @Transaction
    @Query("SELECT * FROM element_table")
    fun getAllElementsWithUser(): List<ElementWithUser>
}

对于 Room,您需要一个带注释的 @Database class 以及实际构建数据库的方法。在这个例子中:-

@Database(entities = [Element::class,User::class], version = 1, exportSchema = false)
//@TypeConverters( value = [DateTypeConverter::class, Converters::class]) //<<<<< not needed
abstract class TheDatabase: RoomDatabase() {
    abstract fun getElementDao(): ElementDao

    companion object {
        private var instance: TheDatabase? = null
        fun getInstance(context: Context): TheDatabase {
            if (instance == null) {
                instance = Room.databaseBuilder(context,TheDatabase::class.java,"the_database.db")
                    .allowMainThreadQueries()
                    .build()
            }
            return instance as TheDatabase
        }
    }
}
  • 注意 .alowMainThreadQueries 用于简洁和方便nce

最后显然你需要代码来做任何事情,例如:-

class MainActivity : AppCompatActivity() {
    lateinit var db: TheDatabase
    lateinit var dao: ElementDao
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        db = TheDatabase.getInstance(this)    
        dao = db.getElementDao()
        dao.insert(User(1,"Fred Bloggs"))
        dao.insert(User(2,"Jane Doe"))
        dao.insert(User(3,"Mary Smith"))

        dao.insert(Element(1000,2 /* Jane Doe */))
        dao.insert(Element(2000,1 /* Fred Bloggs */))
        dao.insert(Element(3000,2 /* Jane Doe also in this Element */))

        for (ewu: ElementWithUser in dao.getAllElementsWithUser()) {
            Log.d("DBINFO","Element is ${ewu.element.IDClass} User is ${ewu.user.userName}")
        }
    }
}

结果

日志包括:-

D/DBINFO: Element is 1000 User is Jane Doe
D/DBINFO: Element is 2000 User is Fred Bloggs
D/DBINFO: Element is 3000 User is Jane Doe

数据库,通过App Inspection可以看出是:-

和 user_table :-

与将用户存储为 JSON 的版本相比,您可以看到用户的数据仅存储一次(即使 Jane Doe 被引用了两次)。