androidsqliteandroid-roomentitytypeconverter

How to create Entity and data classes by ROOM in android


How to create Entity and data classes by ROOM in android?

I have JSON structure:

enter image description here

data class ListResponse(val item: ListItem)

data class ListItem(
    @SerializedName("id")
    val id: List<CheckUnCheckItem>
)

data class CheckUnCheckItem(
    @SerializedName("check")
    val check: CheckItem,

    @SerializedName("unCheck")
    val UnCheck: UnCheckItem
)

data class CheckItem(
    @SerializedName("url")
    val url: String,

    @SerializedName("text")
    val text: String,

    @SerializedName("color")
    val color: String
)

data class UnCheckItem(
    @SerializedName("url")
    val urlUnCheck: String,

    @SerializedName("text")
    val textUnCheck: String,

    @SerializedName("color")
    val colorUnCheck: String
)

But How can I create such ROOM Entity?

Do I need to use @TypeConverter?

@Entity(tableName = TABLE_NAME)
data class ListEntity(
    @PrimaryKey @SerializedName("id")
    val id: CheckUnCheckItem,

    @SerializedName("check")
    val check: CheckItem,

    @SerializedName("unCheck")
    val unCheck: UnCheckItem,

    @SerializedName("url")
    val url: String,

    @SerializedName("text")
    val text: String,

    @SerializedName("size")
    val size: String
){
    companion object{
        const val TABLE_NAME = "db_table"
    }

    class RoomTypeConverters{
        @TypeConverter
        fun convertCheckItemListToJSONString(checkList: CheckItem): String = Gson().toJson(checkList)
        @TypeConverter
        fun convertJSONStringToCheckItemList(jsonString: String): CheckItem = Gson().fromJson(jsonString,CheckItem::class.java)

    }
}

is my data and entity classes are correct? Do I need class witch extends RoomDatabase? Or better I need to separate db and create for check and uncheck another db?


Solution

  • Or better I need to separate db and create for check and uncheck another db?

    As database implies it is able to store data not just one but many. As such a single database is all that would be required. SQLite is a relational database and is designed to store related data. Related data is typically stored in multiple tables. So again a single database will very likely be sufficient.

    Do I need to use @TypeConverter? You never actually need Type Converters. However, for any Object, other than those directly handled (e.g. String, Int, Long, Double, Float, ByteArray) then you either need to break these down into such handled objects or have a types converter that will convert the object to and from such an object.

    For example, based upon your @Entity annotated ListEntity class then:-

    Room has to know about the Type Converters. So you need an @TypeConverters annotation. It's placement defines the scope. Using the annotation to preced the @Database annotation has the most far reaching scope.

    Do I need class witch extends RoomDatabase? Yes. However it has to be an abstract class and should have an abstract function to retrieve an instance of each @Dao annotated interface (or abstract class, in which case the functions have to be abstract, there is no need for an abstract class with Kotlin as functions in an interface can have bodies)).

    This class should be annotated with the @Database annotation, the entities parameter of the annotation should include the list of classes for each each table (@Entity annotated class). e.g.

    @TypeConverters(value = [ListEntity.RoomTypeConverters::class])
    @Database(entities = [ListEntity::class], exportSchema = false, version = 1)
    abstract class TheDatabase: RoomDatabase(){
    
    }
    

    Amending the RoomTypeConverters class to be:-

    class RoomTypeConverters{
        @TypeConverter
        fun convertItemListToJSONString(invoiceList: Item): String = Gson().toJson(invoiceList)
        @TypeConverter
        fun convertJSONStringToItemList(jsonString: String): Item = Gson().fromJson(jsonString,Item::class.java)
        @TypeConverter
        fun convertCheckUnCheckItemToJSONString(cuc: CheckUnCheckItem): String = Gson().toJson(cuc)
        @TypeConverter
        fun convertJSONStringToCheckUnCheckItem(jsonString: String): CheckUnCheckItem = Gson().fromJson(jsonString,CheckUnCheckItem::class.java)
    
    }
    

    Resolves the build issue and in theory you have a potentially usable database.

    However, you obviously need code to access the database. As such you would very likely want to have. as previously mentioned, an @Dao annotated interface e.g

    @Dao
    interface TheDAOs {
        @Insert
        fun insert(listEntity: ListEntity): Long
        @Query("SELECT * FROM ${TABLE_NAME}")
        fun getAll(): List<ListEntity>
    }
    

    :-

    @TypeConverters(value = [ListEntity.RoomTypeConverters::class])
    @Database(entities = [ListEntity::class], exportSchema = false, version = 1)
    abstract class TheDatabase: RoomDatabase(){
        abstract fun getTheDAOsInstance(): TheDAOs
    
    }
    

    To demonstrate actual use of the above then consider the following code in an activity:-

    class MainActivity : AppCompatActivity() {
    
        lateinit var roomDBInstance: TheDatabase
        lateinit var theDAOs: TheDAOs
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            roomDBInstance = Room.databaseBuilder(this,TheDatabase::class.java,"The_database_name.db")
                .allowMainThreadQueries() /* NOTE ADDED FOR CONVENIENCE AND BREVITY */
                .build()
            /* Note the database itself does not yet exist, it's creation is delayed until an attempt is made to access it. So:- */
            theDAOs = roomDBInstance.getTheDAOsInstance() /* Still the database is not created/accessed */
            showData(theDAOs.getAll()) /* No data has been added BUT the database will now exist */
    
            theDAOs.insert(
                ListEntity(
                    id =  CheckUnCheckItem(
                        check = Item (
                            url ="URL001",
                            text = "TEXT001",
                            color = "RED"
                        ),
                        unCheck =  Item(
                            url ="URL002",
                            text = "TEXT002",
                            color = "BLUE"
                        )
                    ),
                    check =  Item(url = "URL003", text ="TEXT003", color ="WHITE"),
                    unCheck =  Item(url = "URL004", text = "TEXT004", color = "BLACK"),
                    url = "URL005", text = "TEXT005", size = "BIG"
                )
            )
            showData(theDAOs.getAll())
        }
    
        fun showData(listEntities: List<ListEntity>) {
            for (li in listEntities) {
                Log.d(
                    "DBINFO",
                    "id is $li.id.check.url${li.id.check.text}.... " +
                            "\n\tcheck is ${li.check.url} .... " +
                            "\n\tuncheck is ${li.unCheck.url} ...." +
                            "\n\turl is ${li.url} text is ${li.text} size is ${li.size}"
                )
            }
        }
    }
    

    The output to the log being:-

    D/DBINFO: id is ListEntity(id=CheckUnCheckItem(check=Item(url=URL001, text=TEXT001, color=RED), unCheck=Item(url=URL002, text=TEXT002, color=BLUE)), check=Item(url=URL003, text=TEXT003, color=WHITE), unCheck=Item(url=URL004, text=TEXT004, color=BLACK), url=URL005, text=TEXT005, size=BIG).id.check.urlTEXT001.... 
            check is URL003 .... 
            uncheck is URL004 ....
            url is URL005 text is TEXT005 size is BIG
    

    The Database via App Inspection being"-

    enter image description here

    So finally

    is my data and entity classes are correct?

    From a database aspect yes, they work after a few amendments. However, I suspect that your classes are probably not what you intended.


    An Alternative Approach

    If this were to be approached from a database perspective and normalised and without bloat and without the need for type converters then consider the following:-

    The embedded Item's (uncheck and check) are basically repetition, so could probably be a table (related to the db_table). Hence 2 tables. One for the ListEntity (Alternative) and another for the Items (AlternativeItem) so the 2 @Entity annotated classes could be:-

    /* Alternative Approach */
    @Entity(
        /* Foreign Keys NOT REQUIRED, they enforce Referential Integrity */
        foreignKeys = [
            ForeignKey(
                entity = AlternativeItem::class,
                parentColumns = ["alternativeItemId"],
                childColumns = ["unCheckIdMap"]
                /* OPTIONAL within a Foreign Key, they help automatically maintain Referential Integrity*/,
                onDelete = ForeignKey.CASCADE,
                onUpdate = ForeignKey.CASCADE
            ),
            ForeignKey(
                entity = AlternativeItem::class,
                parentColumns = ["alternativeItemId"],
                childColumns = ["checkIdMap"],
                onDelete = ForeignKey.CASCADE,
                onUpdate = ForeignKey.CASCADE
            )
        ]
    )
    data class Alternative(
        @PrimaryKey
        val id: Long?=null,
        @ColumnInfo(index = true)
        val unCheckIdMap: Long, /* map to the id of the related Item (AlternativeItem) for the uncheck */
        @ColumnInfo(index = true)
        val checkIdMap: Long, /* map to the id of the related Item (AlternativeItem) for the uncheck */
        val url: String,
        val text: String,
        val size: String
    )
    @Entity
    data class AlternativeItem(
        @PrimaryKey
        val alternativeItemId: Long?=null,
        val alternativeItemUrl: String,
        val alternativeItemText: String,
        val alternativeItemColor: String
    )
    

    As you would typically want the Alternative along with it's related AlternativeItems then a POJO that caters for the togetherness :-

    data class AlternativeWithUncheckAndCheck(
        @Embedded
        val alternative: Alternative,
        @Relation(entity = AlternativeItem::class, parentColumn = "unCheckIdMap", entityColumn = "alternativeItemId")
        val unCheck: AlternativeItem,
        @Relation(entity = AlternativeItem::class, parentColumn = "checkIdMap", entityColumn = "alternativeItemId")
        val check: AlternativeItem
    )
    

    There would be a need for some extra functions in the @Dao annotated interface, so :-

    @Insert
    fun insert(alternative: Alternative): Long
    @Insert
    fun insert(alternativeItem: AlternativeItem): Long
    
    @Transaction
    @Query("")
    fun insertAlternativeAndUncheckAndCheck(alternative: Alternative, uncheck: AlternativeItem, check: AlternativeItem): Long {
        var uncheckId = insert(uncheck)
        var checkId = insert(check)
        return insert(Alternative(null,url = alternative.url, text = alternative.text, size = alternative.size, unCheckIdMap = uncheckId, checkIdMap = checkId ))
    }
    
    @Transaction
    @Query("SELECT * FROM alternative")
    fun getAllAlternativesWithRelatedUnCheckAndCheck(): List<AlternativeWithUncheckAndCheck>
    

    To demonstrate this, all that is then required is to add the new entities to the entities parameter and to then add some code to the activity.

    The amended @Database annotation:-

    @Database(entities = [ListEntity::class, /* for the alternative approach */ Alternative::class, AlternativeItem::class], exportSchema = false, version = 1)
    

    The activity code (that caters for both approaches in a similar/equivalanet way of storing and retrieving the data) :-

    class MainActivity : AppCompatActivity() {
    
        lateinit var roomDBInstance: TheDatabase
        lateinit var theDAOs: TheDAOs
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            roomDBInstance = Room.databaseBuilder(this,TheDatabase::class.java,"The_database_name.db")
                .allowMainThreadQueries() /* NOTE ADDED FOR CONVENIENCE AND BREVITY */
                .build()
            /* Note the database itself does not yet exist, it's creation is delayed until an attempt is made to access it. So:- */
            theDAOs = roomDBInstance.getTheDAOsInstance() /* Still the database is not created/accessed */
            showData(theDAOs.getAll()) /* No data has been added BUT the database will now exist */
    
            theDAOs.insert(
                ListEntity(
                    id =  CheckUnCheckItem(
                        check = Item (
                            url ="URL001",
                            text = "TEXT001",
                            color = "RED"
                        ),
                        unCheck =  Item(
                            url ="URL002",
                            text = "TEXT002",
                            color = "BLUE"
                        )
                    ),
                    check =  Item(url = "URL003", text ="TEXT003", color ="WHITE"),
                    unCheck =  Item(url = "URL004", text = "TEXT004", color = "BLACK"),
                    url = "URL005", text = "TEXT005", size = "BIG"
                )
            )
            showData(theDAOs.getAll())
    
    
            /* Alternative equivalent */
    
            theDAOs.insertAlternativeAndUncheckAndCheck(
                Alternative(url = "URL005", size = "BIG", text = "TEXT005", checkIdMap = -1, unCheckIdMap = -1),
                check = AlternativeItem(alternativeItemUrl = "URL001", alternativeItemText = "TEXT001", alternativeItemColor = "RED"),
                uncheck = AlternativeItem(alternativeItemUrl = "URL002", alternativeItemText = "TEXT002", alternativeItemColor = "BLUE" )
            )
            showAlternativeData(theDAOs.getAllAlternativesWithRelatedUnCheckAndCheck())
        }
    
        fun showData(listEntities: List<ListEntity>) {
            for (li in listEntities) {
                Log.d(
                    "DBINFO",
                    "id is $li.id.check.url${li.id.check.text}.... " +
                            "\n\tcheck is ${li.check.url} .... " +
                            "\n\tuncheck is ${li.unCheck.url} ...." +
                            "\n\turl is ${li.url} text is ${li.text} size is ${li.size}"
                )
            }
        }
    
        fun showAlternativeData(listAlternatives: List<AlternativeWithUncheckAndCheck>) {
            for (la in listAlternatives) {
                Log.d("DBALTINFO",
                "id is ${la.alternative.id} URL is ${la.alternative.url} TEXT is ${la.alternative.text} SIZE is ${la.alternative.size} " +
                        "\n\t UNCHECK id is ${la.unCheck.alternativeItemId} url is ${la.unCheck.alternativeItemUrl} text is ${la.unCheck.alternativeItemText} color is ${la.unCheck.alternativeItemColor}" +
                        "\n\t CHECK id is ${la.check.alternativeItemId} url is ${la.check.alternativeItemUrl} text is ${la.check.alternativeItemText} color is ${la.check.alternativeItemColor}")
            }
        }
    }
    

    When run then the result is now:-

    D/DBINFO: id is ListEntity(id=CheckUnCheckItem(check=Item(url=URL001, text=TEXT001, color=RED), unCheck=Item(url=URL002, text=TEXT002, color=BLUE)), check=Item(url=URL003, text=TEXT003, color=WHITE), unCheck=Item(url=URL004, text=TEXT004, color=BLACK), url=URL005, text=TEXT005, size=BIG).id.check.urlTEXT001.... 
            check is URL003 .... 
            uncheck is URL004 ....
            url is URL005 text is TEXT005 size is BIG
            
            
    D/DBALTINFO: id is 1 URL is URL005 TEXT is TEXT005 SIZE is BIG 
             UNCHECK id is 1 url is URL002 text is TEXT002 color is BLUE
             CHECK id is 2 url is URL001 text is TEXT001 color is RED
    

    The database, via App Inspection (in regards to the alternative approach) is:-

    enter image description here

    and :-

    enter image description here

    i.e. only the actual data is store the BLOAT (field/type descriptions, separators, enclosing data) is not stored thus

    However, the negative, is that more code and thought is required.

    Note this answer is intended to deal with the basic principles and is most certainly not fully comprehensive.