Home > Software design >  Handle the json to Room Database Entity
Handle the json to Room Database Entity

Time:11-05

[
  {
    "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"
      }
    ]
  }
]

I'm going to read json with the following components and put it in the Room Database. So I tried to create and insert MenuEntity and SubMenu,

MenuEntity

@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>,

)

SubMenu

data class Submenu(

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

Menu

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

but the following error occurred.

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

I want Read the json file -> Insert into database -> Read from database

I'd like to ask you how to make the entity.

CodePudding user response:

The simplest way to avoid this error is to add default values to your entities properties.

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

CodePudding user response:

Your issue is that you cannot have List/Array types as columns. Columns have to be of a limited set of types String, Integer, Long, Double, Float, ByteArray are the most common acceptable types.

So

@Embedded
val submenus: List<Submenu>

is not going to work and I believe is the reason why you are getting the error (Room doesn't how to handle the List).

There are two routes that you could take:-

  1. utilise a relationship and thus a second table (Entity) for the list of submenus.
  2. create and store a representation of the submenus, probably as a String (json). In which case you would extract the json for the submenus and store that.

here's a working example of 1

An option that appears to be closest is to utilise two tables with a one-many relationship. That is a Menu can have 0-n submenus. To make the relationship you have an additional column in the Submenu that uniquely identifies the Menu to which a Submenu belongs.

The Menu and Submenu classes are used unchanged.

MenuEntity is :-

@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
    )
  • the Submenu list has been removed
  • an additional (but not really) column has been added
    • as the column definition will be menu_id INTEGER PRIMARY KEY then it is an alias of the normally hidden rowid column, which exists in all tables (except WITHOUT ROWID tables which Room doesn't currently allow).
    • so there is no overhead to adding this column. However, the rowid is the processed more efficiently, up to twice as fast, so there will likely be an increased efficiency.
  • the extra column simplifies the relationship between the Menu and Submenu a simple key is used rather than a composite key.

A new class for the Submenu table 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)
        }
}
  • Note that the Submenu class has been embedded

  • This is basically a Submenu with 2 additional columns

    • the submenu_id column which is an alias of the rowid as previously explained, and
    • a column to store the menu_id of the parent Menu i.e. the relationship
  • For completeness a Foreign Key constraint has been added. This reduces the chance of a Submenu not having a parent and thus being an orphan. As per the comments the constraint is not required.

To all retrieval of Menus with the SubMenus via the realtionship then you have a POJO that embeds the parent (menu) with the @Embedded annotation and has a list of the children annotated with @Relation e.g. :-

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

You then want the means of accessing the database, these are Dao's and you have either an interface or an abstract class annotated with @Dao such as

@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>
}

It is all put together via an abstract class annotated with @Database where the entities are defined and an abstract function is defined to get the class(es) annotated with @Dao. Often a method will be included that caters for getting a single instance of the Database class. So you could have :-

@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
        }
    }
}
  • Note for brevity and convenience .allowMainThreadQueries has been used to allow the demo to run on the main thread and thus not have all the extra code to handle running off the main thread. The latter being recommended for Apps that are to be distributed.

Finally an Activity MainActivity to demonstrate the above.

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)
    }
}

The above is designed to only run once. When run then the following is output to the log:-

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

The menus table, as displayed via App Inspection aka Database Inspector :-

enter image description here

and the submenus table :-

enter image description here

  • Related