Home > other >  how to save List into room database without typeConverters in android-kotlin?
how to save List into room database without typeConverters in android-kotlin?

Time:11-23

im getting response as List from api(using retrofit)

data class User( val name :String, val hobbies:List<String> )

how do i save this into database without using TypeConverter for the "hobbies" field ?

is there any ways ?

CodePudding user response:

There are two potential solutions to the issue.

The first is to convert the data yourself from the List to a single value that represents the List e.g. a String with the values embedded within the String, probably with a separator that can be used to reliably split the resultant String.

The second would be to consider the database as what it is, that is a relational database, where you have a table for the related items. That is a table for the Users and a table for the Hobbies.

It is likely that a User may have a hobby that another user has, as such you probably want a many-many relationship (to full/properly normalise the data).


As an example for the first perhaps consider the following:-


const val SEPARATOR = "~~" /* can be placed wherever and can be a suitable value that will not be within a hobby (often a comma for CSV)*/

A modified User class (i.e. function convertHobbiesToString added) :-

data class User(val name: String, val hobbies: List<String>) {
    fun convertHobbiesToString(): String {
        val sb: StringBuilder = java.lang.StringBuilder()
        var firstInList = true
        for (s in hobbies) {
            if (!firstInList) sb.append(SEPARATOR)
            sb.append(s)
            firstInList = false
        }
        return sb.toString()
    }
}

And the @Entity annotated class that does not have the List but instead has a constructor to be able to construct it from a User using the convertHobbiesToString() function :-

@Entity
data class UserTable(
    @PrimaryKey
    val name: String,
    val hobbyList: String
    ) {
    constructor(baseUser: User) : this(
        name = baseUser.name,
        hobbyList = baseUser.convertHobbiesToString()
    )
}
  • Used in the Working Example below

An example of the Second i.e. using the relationship capabilities of SQLite then consider the following:-


The User class can be as per your question the convert function is not required.

The 2 @Entity annotated classes for the Users and the Hobbies:-

(V2 used as both will be demonstrated in the working example):-

@Entity
data class UserTableV2(
    @PrimaryKey
    val name: String
)
@Entity
data class Hobby(
    @PrimaryKey
    val hobby: String
)

As mentioned a many-many relationship, so a third table that maps users to hobbies:-

@Entity(
    primaryKeys = ["usermap","hobbymap"], /* required */
    /* Foreign Keys are optional */
    /* They enforce referential integrity - so suggested*/
    foreignKeys = [
        ForeignKey(
            entity = UserTableV2::class,
            parentColumns = ["name"],
            childColumns = ["usermap"],
            /* ondelete and onupdate are optional*/
            /* using CASCADE does some automatic maintenance of Referential Integrity */
            onDelete = ForeignKey.CASCADE,
            onUpdate = ForeignKey.CASCADE
        ),
    ForeignKey(
        entity = Hobby::class,
        parentColumns = ["hobby"],
        childColumns = ["hobbymap"],
        onDelete = ForeignKey.CASCADE,
        onUpdate = ForeignKey.CASCADE
    )
    ]

)
data class UserTableV2HobbyMap(
    val usermap: String,
    @ColumnInfo(index = true)
    val hobbymap: String
)

It is very likely that you would need to retrieve a User with the related hobbies, so an additional data class that caters for this (utilising the mapping table):-

data class UserTableV2WithHobbies(
    @Embedded
    val userTableV2: UserTableV2,
    @Relation(
        entity = Hobby::class,
        parentColumn = "name",
        entityColumn = "hobby",
        associateBy = Junction(
            value = UserTableV2HobbyMap::class,
            parentColumn = "usermap",
            entityColumn = "hobbymap"
        )
    )
    val hobby: List<Hobby>
)

Working Example

using the above (the example uses both methods in the same database i.e. 4 tables in all). In addition to the @Entity annotated classes a @Dao annotated class (or classes):-

@Dao
interface AllDao {
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(userTable: UserTable): Long
    @Query("SELECT * FROM usertable")
    fun getAllUserTableRows(): List<UserTable>

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(userTableV2: UserTableV2): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(hobby: Hobby): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(userTableV2HobbyMap: UserTableV2HobbyMap)

    @Transaction
    @Query("")
    fun insertUserWithHobbiesFromUser(user: User) {
        insert(UserTableV2(user.name))
        for (h in user.hobbies) {
            insert(Hobby(h))
            insert(UserTableV2HobbyMap( usermap = user.name, hobbymap = h))
        }
    }

    @Query("SELECT * FROM usertablev2")
    fun getAllUserTableV2RowsWithTheHobbies(): List<UserTableV2WithHobbies>
}
  • Note the first two functions are for the UserTable (first solution)
  • The subsequent functions are for the UserTableV2 (second solution)
  • The insertuserWithHobbiesFromUser is very much as it says.

Additionally an @Database annotated class is required:-

@Database(entities = [UserTable::class,UserTableV2::class,Hobby::class,UserTableV2HobbyMap::class], exportSchema = false, 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,"the_database.db")
                    .allowMainThreadQueries()
                    .build()
            }
            return instance as TheDatabase
        }
    }
}
  • as can be seen all 4 tables (@Entity annotated classes) have been specified i.e. the database will hold data for both solutions.
  • .alllowMainThreadQueries has been used for brevity of the demo

To demonstrate the above the following code in an activity:-

const val SEPARATOR = "~~"
class MainActivity : AppCompatActivity() {
    lateinit var dbInstance: TheDatabase
    lateinit var dao: AllDao
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        dbInstance = TheDatabase.getInstance(this)
        dao = dbInstance.getAllDao()

        /* Used to represent the API data for both solution examples*/
        val apidata: List<User> = listOf(
            User("USER001", listOf("Hobby1","Hobby2","Hobby3")),
            User("USER002", listOf("Hobby4","Hobby5","Hobby1")),
            User("USER003", listOf("Hobby1","Hobby2","Hobby3","Hobby4","Hobby5","Hobby6","Hobby1" /*<<<<< DUP*/, "Hobby7"))
        )

        /* Solution 1 */
        for (u in apidata) {
            dao.insert(UserTable(u))
        }

        for (ut in dao.getAllUserTableRows()) {
            Log.d("DBINFOV1","UserTableRow name = ${ut.name} HobbyList = ${ut.hobbyList}")
        }


        /* Solution 2 (V2 many-many Hobbies) */
        for (u in apidata) {
            dao.insertUserWithHobbiesFromUser(u)
        }
        for (uwh in dao.getAllUserTableV2RowsWithTheHobbies()) {
            Log.d("DBINFOV2","User is ${uwh.userTableV2.name} it has ${uwh.hobby.size}; They are:-")
            for (h in uwh.hobby) {
                Log.d("DBINFOV2","\tHobby is ${h.hobby}")
            }
        }
    }
}
  • Each solution takes the apidata and stores it in the database and then retrieves the data writing the result to the log.

When run (only designed to run the once):-

2022-11-23 07:58:11.381 D/DBINFOV1: UserTableRow name = USER001 HobbyList = Hobby1~~Hobby2~~Hobby3
2022-11-23 07:58:11.381 D/DBINFOV1: UserTableRow name = USER002 HobbyList = Hobby4~~Hobby5~~Hobby1
2022-11-23 07:58:11.381 D/DBINFOV1: UserTableRow name = USER003 HobbyList = Hobby1~~Hobby2~~Hobby3~~Hobby4~~Hobby5~~Hobby6~~Hobby1~~Hobby7


2022-11-23 07:58:11.480 D/DBINFOV2: User is USER001 it has 3; They are:-
2022-11-23 07:58:11.480 D/DBINFOV2:     Hobby is Hobby1
2022-11-23 07:58:11.480 D/DBINFOV2:     Hobby is Hobby2
2022-11-23 07:58:11.480 D/DBINFOV2:     Hobby is Hobby3
2022-11-23 07:58:11.480 D/DBINFOV2: User is USER002 it has 3; They are:-
2022-11-23 07:58:11.480 D/DBINFOV2:     Hobby is Hobby1
2022-11-23 07:58:11.480 D/DBINFOV2:     Hobby is Hobby4
2022-11-23 07:58:11.480 D/DBINFOV2:     Hobby is Hobby5
2022-11-23 07:58:11.480 D/DBINFOV2: User is USER003 it has 7; They are:-
2022-11-23 07:58:11.480 D/DBINFOV2:     Hobby is Hobby1
2022-11-23 07:58:11.480 D/DBINFOV2:     Hobby is Hobby2
2022-11-23 07:58:11.481 D/DBINFOV2:     Hobby is Hobby3
2022-11-23 07:58:11.481 D/DBINFOV2:     Hobby is Hobby4
2022-11-23 07:58:11.481 D/DBINFOV2:     Hobby is Hobby5
2022-11-23 07:58:11.481 D/DBINFOV2:     Hobby is Hobby6
2022-11-23 07:58:11.481 D/DBINFOV2:     Hobby is Hobby7
  • If you note Hobby1 only appears once (i.e. it's duplication has been dropped and thus corrects the error) for USER003.

The database itself looks like (via App Inspection):-

enter image description here

  • Solution 1

enter image description here

and :-

enter image description here

and the mapping table :-

enter image description here

  • Solution 2
  • Related