处理 json 到房间数据库实体

Posted

技术标签:

【中文标题】处理 json 到房间数据库实体【英文标题】:Handle the json to Room Database Entity 【发布时间】:2021-12-18 09:05:38 【问题描述】:
[
  
    "name" :  "phase1",
    "type" : "purchase",
    "submenu": [
      
        "name": "insert",
        "route": "insert"
      ,
      
        "name": "send",
        "route": "send"
      ,
      
        "name": "delete",
        "route": "delete"
      
    ]
  ,
  
    "name" :  "phase2",
    "type" : "refund",
    "submenu": [
      
        "name": "insert",
        "route": "insert"
      ,
      
        "name": "send",
        "route": "send"
      ,
      
        "name": "delete",
        "route": "delete"
      
    ]
  
]

我将读取带有以下组件的 json 并将其放入 Room Database。所以我尝试创建并插入 MenuEntity 和 SubMenu,

菜单实体

@Entity(tableName = "menus", primaryKeys = ["name", "type"])
data class MenuEntity(

    @ColumnInfo(name = "name")
    val name: String,

    @ColumnInfo(name = "type")
    val type: String,

    @Embedded
    val submenus: List<Submenu>,

)

子菜单

data class Submenu(

    @ColumnInfo(name = "submenu_name")
    val name: String,
    @ColumnInfo(name = "route")
    val route: String
)

菜单

data class Menu(
    val name: String,
    val type: String,
    val submenus: List<Submenu>,
)

但出现以下错误。

Entities and POJOs must have a usable public constructor. You can have an empty constructor or a constructor whose parameters match the fields (by name and type). - java.util.List

我想要 读取json文件->插入数据库->从数据库中读取

想请教一下实体的制作方法。

【问题讨论】:

【参考方案1】:

您的问题是您不能将列表/数组类型作为列。列必须是一组有限的类型 String、Integer、Long、Double、Float、ByteArray 是最常见的可接受类型。

所以

@Embedded
val submenus: List<Submenu>

不起作用,我相信这是您收到错误的原因(Room 没有如何处理列表)。

你可以走两条路:-

    利用关系,从而为子菜单列表使用第二个表(实体)。 创建和存储子菜单的表示形式,可能是字符串 (json)。在这种情况下,您将提取子菜单的 json 并将其存储。

这是 1 的一个工作示例

似乎最接近的一个选项是利用具有一对多关系的两个表。也就是说,一个 Menu 可以有 0-n 个子菜单。为了建立这种关系,您在子菜单中有一个额外的列,用于唯一标识子菜单所属的菜单。

Menu 和 Submenu 类保持不变。

MenuEntity 是:-

@Entity(tableName = "menus", indices = [Index("name", "type", unique = true)])
data class MenuEntity(
    @PrimaryKey
    @ColumnInfo(name = "menu_id")
    val menuId: Long? = null,
    @ColumnInfo(name = "name")
    val name: String,
    @ColumnInfo(name = "type")
    val type: String
    )
子菜单列表已被删除 添加了一个额外的(但不是真的)列 因为列定义将是menu_id INTEGER PRIMARY KEY,所以它是通常隐藏的rowid 列的别名,它存在于所有表中(Room 当前不允许的 WITHOUT ROWID 表除外)。 因此添加此列没有开销。但是,rowid 的处理效率更高,最高可达两倍,因此可能会提高效率。 额外的列简化了 Menu 和 Submenu 之间的关系,使用的是简单键而不是复合键。

子菜单表的新类SubmenuEntity

@Entity(tableName = "submenus",
        /* Optional but useful as referential integrity is enforced */
        foreignKeys = [
                ForeignKey(
                        entity = MenuEntity::class,
                        parentColumns = ["menu_id"],
                        childColumns = ["menu_id_map"],
                        /* Optional within A Foreign Key */
                        onDelete = CASCADE,
                        onUpdate = CASCADE
                )
        ]
)
data class SubmenuEntity(
        @PrimaryKey
        @ColumnInfo(name = "submenu_id")
        val id: Long? = null,
        @Embedded
        val submenu: Submenu,
        @ColumnInfo(name = "menu_id_map")
        val menuMapId: Long
) 
        // Bonus function that will get a Submenu from a SubmenuEntity
        fun getSubmenuFromSubmenuEntity(submenuEntity: SubmenuEntity): Submenu 
                return Submenu(name = submenuEntity.submenu.name, route = submenuEntity.submenu.route)
        

请注意,Submenu 类已嵌入

这基本上是一个带有 2 个附加列的子菜单

submenu_id 列,如前所述,它是 rowid 的别名,并且 用于存储父菜单的 menu_id 的列,即关系

为了完整起见,添加了外键约束。这减少了子菜单没有父项并因此成为孤儿的机会。根据 cmets,不需要约束。

要通过关系使用 SubMenus 检索所有菜单,那么您有一个 POJO,它嵌入了带有 @Embedded 注释的父级(菜单),并有一个带有 @Relation 注释的子级列表,例如:-

data class MenuWithSubmenus (
    @Embedded
    val menuEntity: MenuEntity,
    @Relation(
        entity = SubmenuEntity::class,
        parentColumn = "menu_id",
        entityColumn = "menu_id_map")
    val submenuList: List<SubmenuEntity>
)

然后你想要访问数据库的方法,这些是 Dao 的,你有一个接口或一个带有 @Dao 注释的抽象类,例如

@Dao
abstract class AllDao 
    @Insert
    abstract fun insert(menuEntity: MenuEntity): Long
    @Insert
    abstract fun insert(submenu: SubmenuEntity): Long
    @Transaction
    @Query("SELECT * FROM menus")
    abstract fun getALlMenusWithSubmenus(): List<MenuWithSubmenus>

所有这些都通过一个带有@Database 注释的抽象类组合在一起,其中定义了实体并定义了一个抽象函数来获取带有@Dao 注释的类。通常会包含一个方法来满足获取 Database 类的单个实例。所以你可以:-

@Database(entities = [MenuEntity::class, SubmenuEntity::class],version = 1)
abstract class TheDatabase: RoomDatabase() 
    abstract fun getAllDao(): AllDao

    companion object 
        private var instance: TheDatabase? = null
        fun getInstance(context: Context) : TheDatabase 
            if (instance == null) 
                instance = Room.databaseBuilder(
                    context,
                    TheDatabase::class.java,
                    "menu_database.db"
                )
                    .allowMainThreadQueries() // Used for convenience/brevity
                    .build()
            
            return instance  as TheDatabase
        
    

注意 为简洁和方便起见,.allowMainThreadQueries 已用于允许演示在主线程上运行,因此没有所有额外的代码来处理从主线程运行的所有额外代码。后者推荐用于要分发的应用程序。

最后一个 Activity MainActivity 来演示上述内容。

class MainActivity : AppCompatActivity() 

    lateinit var db: TheDatabase
    lateinit var dao: AllDao
    override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        db = TheDatabase.getInstance(this)
        dao = db.getAllDao();

        // Get the JSON and add the Menu's and Submenu's to the database.
        Log.d("DBINFO",getTheJsonInputString()) // Write the JSON to the log
        val menuList = Gson().fromJson(getTheJsonInputString(), Array<Menu>::class.java)
        for (m: Menu in menuList) 
            var menuId = dao.insert(MenuEntity(name = m.name, type = m.type))
            for (sm: Submenu in m.submenus) 
                dao.insert(SubmenuEntity(submenu = Submenu(sm.name,sm.route),menuMapId = menuId))
            
        

        // extract the data from the database as MenuWithSubmenus objects
        for (mws: MenuWithSubmenus in dao.getALlMenusWithSubmenus()) 
            Log.d("DBINFO"," Menu is $mws.menuEntity.name Type is $mws.menuEntity.type ID is $mws.menuEntity.menuId")
            for(sm: SubmenuEntity in mws.submenuList) 
                var currentSubmenu = sm.getSubmenuFromSubmenuEntity(sm) // Check the function works
                Log.d("DBINFO","\tSubmenu is $sm.submenu.name Route is $sm.submenu.route ID is $sm.id Parent Menu is $sm.menuMapId")
                Log.d("DBINFO", "\tSubmenu extract from SubmenuEntity is $currentSubmenu.name Route is $currentSubmenu.route")
            
        
    

    /**
     *  generate some test data
     */
    private fun getTheJsonInputString(): String 

        val menulist: List<Menu> = listOf(
            Menu("menu1","type1",
                listOf(
                    Submenu(name = "SM1", route = "route1"),
                    Submenu(name = "SM2", route = "route2"),
                    Submenu(name = "SM3", route = "route3")
                )
            ),
            Menu( name = "menu2", type = "type2",
                listOf(
                    Submenu(name = "SM4", route = "route4"),
                    Submenu(name = "SM5", route = "route5")
                )
            )
        )
        return Gson().toJson(menulist)
    

上面的设计只运行一次。运行时将以下内容输出到日志:-

D/DBINFO: ["name":"menu1","submenus":["name":"SM1","route":"route1","name":"SM2","route":"route2","name":"SM3","route":"route3"],"type":"type1","name":"menu2","submenus":["name":"SM4","route":"route4","name":"SM5","route":"route5"],"type":"type2"]
D/DBINFO:  Menu is menu1 Type is type1 ID is 1
D/DBINFO:   Submenu is SM1 Route is route1 ID is 1 Parent Menu is 1
D/DBINFO:   Submenu extract from SubmenuEntity is SM1 Route is route1
D/DBINFO:   Submenu is SM2 Route is route2 ID is 2 Parent Menu is 1
D/DBINFO:   Submenu extract from SubmenuEntity is SM2 Route is route2
D/DBINFO:   Submenu is SM3 Route is route3 ID is 3 Parent Menu is 1
D/DBINFO:   Submenu extract from SubmenuEntity is SM3 Route is route3
D/DBINFO:  Menu is menu2 Type is type2 ID is 2
D/DBINFO:   Submenu is SM4 Route is route4 ID is 4 Parent Menu is 2
D/DBINFO:   Submenu extract from SubmenuEntity is SM4 Route is route4
D/DBINFO:   Submenu is SM5 Route is route5 ID is 5 Parent Menu is 2
D/DBINFO:   Submenu extract from SubmenuEntity is SM5 Route is route5

菜单表,通过 App Inspection aka Database Inspector 显示:-

和子菜单表:-

【讨论】:

感谢您的友好评论和示例。多亏了这一点,我对问题以及如何进行有了清晰的认识。再次感谢您。 我能再问你一个问题吗?如何将 MenuWithSubmenus 更改为 List?如果你不介意,我希望你能帮助我。 @PolarisNation 从房间的角度来看,你不能直接。请注意,当您检索 List 时,您基本上拥有它,所以有一个 List。但是,如果 Menu 有一个父级,例如 Window,那么您可以拥有 WindowWithMenuWithSubmenus,其中 Window 是 @Embedded 并且 List@Relation,然后通过层次结构获取 @Relation 子菜单。说不确定您要达到的目标。也许再问一个问题。 想想你说的话,你是对的。我误解了。谢谢你的回答。【参考方案2】:

避免此错误的最简单方法是为您的实体属性添加默认值。

data class Menu(
    val name: String = "",
    val type: String = "",
    val submenus: List<Submenu> = listOf<Submenu>()
)

【讨论】:

以上是关于处理 json 到房间数据库实体的主要内容,如果未能解决你的问题,请参考以下文章

有两张桌子的房间实体?

将 Kotlin 内联类作为实体字段的房间数据库

将 PostgreSQL JSON 列映射到 Hibernate 实体属性

将房间数据库中的唯一约束添加到多列

为在 Sqlite 中具有 LONG 数据类型的字段的表创建房间实体

使用房间关系在房间数据库中三重加入