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):-
- Solution 1
and :-
and the mapping table :-
- Solution 2