This is the class that i'm saving inside room database:
@Entity
data class Person(
val name : String = "Bruno",
val age : Int = 23,
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val hobbies : ArrayList<String> = arrayListOf("Basquete","Academia","Musica","Anatomia")
)
I already added type converters so it is saving successfully.
What i want is to query results by what the hobbies
list has. E.g:
select * from person where hobbies in ("Basquete")
I wanted to select all person objects that has "Basquete" inside the hobbies
list, but this query is returning empty. What am i doing wrong?
Their is no concept of a list in a row of a table, a column holds a single value.
Having a Type Converter will store the list of hobbies as a single value (column), as such IN will check the entire (the EXACT) value (the full list and whatever encoding is used, this dependant upon the Type Converter that converters the list to the single value).
As such it is likely that using IN
, is not going to be of use.
As an example the TypeConverter may convert to something along the lines of ["Basquete","Academia","Musica"]
(conversion to JSON string via com.google.code.gson dependcy)
To demonstrate using data loaded and using App Inspection then with
Now consider an adaptation of your query, it being SELECT *, hobbies in ("Basquete") AS TEST1, hobbies IN ('Basquete') AS TEST2, "Basquete" IN(hobbies) AS TEST3,'Basquete' IN (hobbies) AS TEST4 ,hobbies LIKE '%Basquete%' AS TEST5 FROM person WHERE person.id = 1;
Then via App Inspection, the result is
So you could use WHERE hobbies LIKE '%Basquete%'
%
char as the first character will result in an in-efficient scan for the data.However, if you are looking for multiple hobbies, then the complexity increases. e.g. WHERE hobbies LIKE '%Basquete%' OR hobbies LIKE '%Academia%'
(to find those with either, using AND
instead of OR
would return only those with both).
The more correct solution where IN
could be utilised would be to have a table that contains the hobbies and as the relationship would be a many-many relationship (a person could have many hobbies and people could have the same hobbies) have a third mapping table for the many-many relationship.
Example of the More Correct way
All the @Entity annotated classes and also a POJO for getting a Person with their list of hobbies:-
@Entity
data class Person(
val name : String = "Bruno",
val age : Int = 23,
@PrimaryKey(autoGenerate = true) val id: Int = 0,
//val hobbies : ArrayList<String> = arrayListOf("Basquete","Academia","Musica","Anatomia") /*<<<<<<<<<< no need */
/* Also no need for type converters */
)
/* The suggested (more correct) Hobby table */
@Entity(
indices = [
Index(value = ["hobbyName"], unique = true)
]
)
data class Hobby(
@PrimaryKey
var hobbyId: Long?=null,
var hobbyName: String /* UNIQUE Index so no duplicated hobby names */
)
/* The Mapping Table
Note also know as reference table, associative table and other names
*/
/* POJO for extracting a Person with thier list of Hobbies */
data class PersonWithHobbies(
@Embedded
var person: Person,
@Relation(
entity = Hobby::class,
parentColumn = "id",
entityColumn = "hobbyId",
associateBy = Junction(
value = PersonHobbyMap::class,
parentColumn = "personIdMap",
entityColumn = "hobbyIdMap"
)
)
var hobbies: List<Hobby>
)
/* This is the Mapping Table that maps people to hobbies */
@Entity(
primaryKeys = ["personIdMap","hobbyIdMap"],
/* Option but suggested foreign key constraint definitions to enforce and maintain referential integrity
*/
foreignKeys = [
/* For the reference to the Person */
ForeignKey(
entity = Person::class, /* The parent @Entity annotated class */
parentColumns = ["id"], /* The column in the parent that is referenced */
childColumns = ["personIdMap"], /* the column in this table that holds the reference to the parent */
onDelete = ForeignKey.CASCADE, /* will delete rows in the table if the parent is deleted */
onUpdate = ForeignKey.CASCADE /* will update the value, if the value (id) in the parent is changed */
),
/* For the reference to the Hobby */
ForeignKey(
entity = Hobby::class,
parentColumns = ["hobbyId"],
childColumns = ["hobbyIdMap"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
data class PersonHobbyMap(
var personIdMap: Long,
@ColumnInfo(index = true) /* more efficient to have index on the 2nd column (first is indexed as first part of the Primary key) */
var hobbyIdMap: Long
)
An @Dao annotated interface with functions to insert data and also to extract persons (with and without their hobbies) if they have any of the hobbies passed (a query for using the hobby id's and another for using the hobby names)
:-
@Dao
interface TheDaos {
/* Inserts */
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(person: Person): Long
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(hobby: Hobby): Long
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(personHobbyMap: PersonHobbyMap): Long
/* Query for retrieving the Person and their hobbies if they have hobbies according to the provided list of hobbyId's */
@Transaction
@Query("SELECT DISTINCT person.* FROM person JOIN personHobbyMap ON person.id = personHobbyMap.personIdMap JOIN hobby ON personHobbyMap.hobbyIdMap = hobby.hobbyId WHERE hobbyId IN(:hobbyIdList);")
fun getPersonsWithHobbiesIfHobbiesInListOfHobbyIds(hobbyIdList: List<Long>): List<PersonWithHobbies>
/* Query for retrieving the Person and their hobbies if they have hobbies according to the provided list of hobby names's */
@Transaction
@Query("SELECT DISTINCT person.* FROM person JOIN personHobbyMap ON person.id = personHobbyMap.personIdMap JOIN hobby ON personHobbyMap.hobbyIdMap = hobby.hobbyId WHERE hobbyName IN(:hobbyNameList);")
fun getPersonsWithHobbiesIfHobbiesInListOfHobbyNames(hobbyNameList: List<String>): List<PersonWithHobbies>
/* The equivalent of the above 2 queries BUT only gets the Person (without Hobbies) */
@Query("SELECT DISTINCT person.* FROM person JOIN personHobbyMap ON person.id = personHobbyMap.personIdMap JOIN hobby ON personHobbyMap.hobbyIdMap = hobby.hobbyId WHERE hobbyId IN(:hobbyIdList);")
fun getPersonsIfHobbiesInListOfHobbyIds(hobbyIdList: List<Long>): List<Person>
@Query("SELECT DISTINCT person.* FROM person JOIN personHobbyMap ON person.id = personHobbyMap.personIdMap JOIN hobby ON personHobbyMap.hobbyIdMap = hobby.hobbyId WHERE hobbyName IN(:hobbyNameList);")
fun getPersonsIfHobbiesInListOfHobbyNames(hobbyNameList: List<String>): List<Person>
/* NOTE
without DISTINCT or without only selecting the columns for the Person only,
if a Person has multiple matches then that person would be extracted multiple times.
*/
}
The @Database annotated class (note .allowMainThreadQueries
used for brevity and convenience):-
@Database(entities = [Person::class,Hobby::class,PersonHobbyMap::class], exportSchema = false, version = 1)
abstract class TheDatabase: RoomDatabase() {
abstract fun getTheDaos(): TheDaos
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
}
}
}
Finally activity code that puts it all together, adding some data and then querying the data selecting only the Person (with and then without their list of hobbies) :-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: TheDaos
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getTheDaos()
val h1 = dao.insert(Hobby(hobbyName = "Basquete"))
val h2 = dao.insert(Hobby(hobbyName = "Academia"))
val h3 = dao.insert(Hobby(hobbyName = "Musica"))
val h4 = dao.insert(Hobby(hobbyName = "Anatomia"))
val h5 = dao.insert(Hobby(hobbyName = "other"))
val h6 = dao.insert(Hobby(hobbyName = "another"))
val h7 = dao.insert(Hobby(hobbyName = "yet another"))
val p1 = dao.insert(Person(name = "Bruno", age = 23))
val p2 = dao.insert(Person(name = "Sarah", age = 21))
val p3 = dao.insert(Person(name = "Tom", age = 22))
val p4 = dao.insert(Person(name = "Mary", age = 20))
val p5 = dao.insert(Person(name = "Freda", age = 19))
/* Bruno has hobbies Basquete, Academia, Musica and Anatomia */
dao.insert(PersonHobbyMap(p1,h1))
dao.insert(PersonHobbyMap(p1,h2))
dao.insert(PersonHobbyMap(p1,h3))
dao.insert(PersonHobbyMap(p1,h4))
/* Sarah has hobbies Academia, Anatomia and another */
dao.insert(PersonHobbyMap(p2,h2))
dao.insert(PersonHobbyMap(p2,h4))
dao.insert(PersonHobbyMap(p2,h6))
/* Tom has hobbies Basquete, Musica, other and yet another */
dao.insert(PersonHobbyMap(p3,h1))
dao.insert(PersonHobbyMap(p3,h3))
dao.insert(PersonHobbyMap(p3,h5))
dao.insert(PersonHobbyMap(p4,h7))
/* Mary has hobbies other, another and yet another */
dao.insert(PersonHobbyMap(p4,h5))
dao.insert(PersonHobbyMap(p4,h6))
dao.insert(PersonHobbyMap(p4,h7))
/* Freda has no Hobbies */
val sb: StringBuilder = java.lang.StringBuilder()
/* Persons and their hobbies for those that have Basquete or Academia in their list of hobbies (hobbies to include via list of hobbyId's)*/
/* i.e. Bruno (both) and Sarah (Academia) and Tom (both) */
for(pwh in dao.getPersonsWithHobbiesIfHobbiesInListOfHobbyIds(listOf(h1,h2))) {
sb.clear()
for (h in pwh.hobbies) {
sb.append("\n\t${h.hobbyName}")
}
Log.d("DBINFO_TEST1","Person is ${pwh.person.name} and has ${pwh.hobbies.size} hobbies. They are:- ${sb}")
}
/* Persons and their hobbies for those that have Basquete or Musica in their list of hobbies (hobbies to include via list of Hobby names)*/
/* i.e. Bruno (both) and Tom (Musica) */
for(pwh in dao.getPersonsWithHobbiesIfHobbiesInListOfHobbyNames(listOf("Basquete","Musica"))) {
sb.clear()
for (h in pwh.hobbies) {
sb.append("\n\t${h.hobbyName}")
}
Log.d("DBINFO_TEST2","Person is ${pwh.person.name} and has ${pwh.hobbies.size} hobbies. They are:- ${sb}")
}
}
}
Result of running the above (i.e. output to the log) :-
2022-07-28 09:35:36.954 D/DBINFO_TEST1: Person is Bruno and has 4 hobbies. They are:-
Basquete
Academia
Musica
Anatomia
2022-07-28 09:35:36.954 D/DBINFO_TEST1: Person is Tom and has 3 hobbies. They are:-
Basquete
Musica
other
2022-07-28 09:35:36.954 D/DBINFO_TEST1: Person is Sarah and has 3 hobbies. They are:-
Academia
Anatomia
another
2022-07-28 09:35:36.958 D/DBINFO_TEST2: Person is Bruno and has 4 hobbies. They are:-
Basquete
Academia
Musica
Anatomia
2022-07-28 09:35:36.959 D/DBINFO_TEST2: Person is Tom and has 3 hobbies. They are:-
Basquete
Musica
other
In addition to utilising the IN
expression/clause, this recommended way of storing the data, although a little more complex, offers advantages (at least from a relational database perspective) such as:-