Do I have a way to search objects on Room Database - android

I am now building an Android App with Local Database
The table structure is like following (coming from API)
#Entity
data class Person(
name: Name,
... ... ...
... ... ...
)
data class Name(
legalName: String.
common: String
)
This is sql code I have tried to person with legal name
#Query("SELECT * FROM person WHERE name.legalName = :legalName")
suspend fun getPersonByName (legalName: String): Person?
This gave me compile error as we can't search by name.legalName on Room database
In addition, we have static name list of person (only legal name) in Homepage (No ID or other reasonable fields to perform search)
DO we have proper way to search Users with legalName field?

The #Entity annotation is used by Room to determine the underlying SQLite table schema. A class so annotated is an object but the individual fields/members of the object are stored as columns in the table which are not objects.
Such columns can never be anything other than specific types being either:-
integer type values (e.g. Int, Long .... Boolean) (column type of INTEGER)
string type values (e.g. String) (column type of TEXT)
decimal/floating point type values (e.g, Float, Double) (column type REAL)
bytestream type values (e.g. ByteArray) (column type BLOB)
null (column definition must not have NOT NULL constraint)
Thus, objects are NOT stored or storable directly SQLite has no concept/understanding of objects just columns grouped into tables.
In your case the name field is a Name object and Room will require 2 Type Converters:-
One that converts the object into one of the above that can represent the object (typically a json representation of the object)
The other to convert the stored data back into the Object.
This allowing an object to be represented in a single column.
As such to query a field/member of the object you need to consider how it is represented and searched accordingly.
There will not be a name.legalName column just a name column and the representation depends upon the TypConverter as then would the search (WHERE clause).
Now consider the following based upon your code:-
#Entity
data class Person(
#PrimaryKey
var id: Long?=null,
var name: Name,
#Embedded /* Alternative */
var otherName: Name
)
data class Name(
var legalName: String,
var common: String
)
PrimaryKey added as required by Room
#Embedded as an alternative that copies the fields/members (legalName and common as fields)
Thus the name column will require TypeConverters as per a class with each of the 2 annotated twith #TypeConverter (note singular), the class where the Type Converters are defined has to be defined (see the TheDatabase class below). So :-
class TheTypeConverters {
/* Using Library as per dependency implementation 'com.google.code.gson:gson:2.10.1' */
#TypeConverter
fun convertFromNameToJSONString(name: Name): String = Gson().toJson(name)
#TypeConverter
fun convertFromJSONStringToName(jsonString: String): Name = Gson().fromJson(jsonString,Name::class.java)
}
note that there are other Gson libraries that may offer better functionality.
The entities (just the one in this case) have to be defined in the #Database annotation for the abstract class that extends RoomDatabase(). so:-
#TypeConverters(value = [TheTypeConverters::class])
#Database(entities = [Person::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() /* For brevity convenience of the demo */
.build()
}
return instance as TheDatabase
}
}
}
The #TypeConverters annotation (plural) in addition to defining a class or classes where the TypeConverters are, also defines the scope (#Database being the most encompassing scope).
At this stage the project can be compiled (CTRL + F9) and the annotation processing will generate some code. Importantly TheDatabase_Impl in the java(generated) The name being the same as the #Database annotated class suffixed with _Impl. This includes a method createAllTables which is the SQL used when creatin the SQLite tables. The SQL for the person table is:-
CREATE TABLE IF NOT EXISTS `Person` (
`id` INTEGER,
`name` TEXT NOT NULL,
`legalName` TEXT NOT NULL,
`common` TEXT NOT NULL, PRIMARY KEY(`id`)
)
As can be seen the id column as the primary key, the name column for the converted representation of the name object and then the legal and common columns due to the name object being #Embedded via the otherName field.
Just to finish matters with the following #Dao annotated interface (allowing some data to be added):-
#Dao
interface TheDAOs {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(person: Person): Long
#Query("SELECT * FROM person")
fun getAllPersonRows(): List<Person>
}
And with MainActivity as:-
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()
dao.insert(Person(null, name = Name("Frederick Bloggs","Fred Bloggs"), otherName = Name("Frederick ","Fred Bloggs")))
dao.insert(Person(null, name = Name("Jane Doe","Jane Doe"), otherName = Name("Jane Doe","Jane Doe")))
}
}
and the project run and then App Inspection used to view the actual database then:-
The name column contains the string {"common":"Fred Bloggs","legalName":"Frederick Bloggs"}
So the WHERE clause to locate all legal names that start with Fred could be
WHERE instr(name,',\"legalName\":\"Fred')
or
WHERE name LIKE '%,\"legalName\":\"Fred%'
it should be noted that both due to the search being within a column requires a full scan.
Of course that assumes that there is no name that has the common name ,"legalName":"Fred or as part of the common name or some other part of entire string. i.e. it can be hard to anticipate what results may be in the future.
For the alternative #Embedded Name object, the legalName and common columns are more easily searched, the equivalent search for legal names starting with Fred could be
WHERE legalname LIKE 'Fred%'
There is no potential whatsoever for Fred appearing elsewhere meeting the criteria. The search just on the single column/value nothing else. Indexing the column would very likely improve the efficiency.
Amending the #Dao annotated interface TheDAOs to be:-
#Dao
interface TheDAOs {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(person: Person): Long
#Query("SELECT * FROM person WHERE instr(name,',\"legalName\":\"Fred')")
fun getPersonsAccordingToLegalNameInNameObject(): List<Person>
#Query("SELECT * FROM person WHERE legalName LIKE 'Fred%'")
fun getPersonsAccordingToLegalName(): List<Person>
}
And MainActivity to be:-
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()
dao.insert(Person(null, name = Name("Frederick Bloggs","Fred Bloggs"), otherName = Name("Frederick ","Fred Bloggs")))
dao.insert(Person(null, name = Name("Jane Doe","Jane Doe"), otherName = Name("Jane Doe","Jane Doe")))
logPersonList(dao.getPersonsAccordingToLegalNameInNameObject(),"RUN1")
logPersonList(dao.getPersonsAccordingToLegalName(),"RUN2")
}
private fun logPersonList(personList: List<Person>, suffix: String) {
for (p in personList) {
Log.d("DBINFO_${suffix}","Person ID is ${p.id} Name.legalName is ${p.name.legalName} Name.common is ${p.name.common} LegalName is ${p.otherName.legalName} Common is ${p.otherName.common}")
}
}
}
Then running (first time after install) the log contains:-
2023-01-14 11:26:03.738 D/DBINFO_RUN1: Person ID is 1 Name.legalName is Frederick Bloggs Name.common is Fred Bloggs LegalName is Frederick Common is Fred Bloggs
2023-01-14 11:26:03.740 D/DBINFO_RUN2: Person ID is 1 Name.legalName is Frederick Bloggs Name.common is Fred Bloggs LegalName is Frederick Common is Fred Bloggs
i.e. in this limited demo the expected results either way.
Note that Name.legalName and Name.common is not how the data is accessed, it is just text used to easily distinguish then similar values.

Related

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:
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?
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:-
field id would need a TypeConverter as the type CheckUnCheckItem is not an object type that can be directly handled by Room. So you would need two TypeConverters that could convert from a CheckUncheckItem to and from a type that can be handled by Room.
fields check and uncheck would need two TypeConverters (and it looks as though the Type Converters you have coded will handle the conversion).
fields url,text and size, as they are all String types do not need Type Converters as Room handles strings.
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(){
}
However, using the above along with your classes results in a build error as per:-
ListEntity.java:11: error: Cannot figure out how to save this field into database. You can consider adding a type converter for it. private final a.a.so74708202kotlinroomentitydesign.CheckUnCheckItem id = null;
as explained CheckUnCheckItem cannot be handled by Room.
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>
}
This will suffice to allow rows to be inserted into the database and for all the rows to be extracted from the database into a List<ListEntity).
From the built database you need to get an instance of the TheDAOs and thus the #Database annotated class could then be
:-
#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"-
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>
note that the insertAlternativeAndUncheckAndCheck does what it says (note that it is overly simple and could need some enhancements to expand upon the principle)
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}")
}
}
}
Note that the Alternative code is probably more along the lines of what you probably want according to the interpretation of the shown JSON.
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
it is suspected that BLACK/WHITE or RED/BLUE is superfluous in your interpretationof the JSON to data classes (and hence excluded in the alternative).
The database, via App Inspection (in regards to the alternative approach) is:-
and :-
i.e. only the actual data is store the BLOAT (field/type descriptions, separators, enclosing data) is not stored thus
the database will hold more data in less space.
the handling of the data will thus be more efficient (e.g. a buffer can hold more actual data instead of BLOAT).
querying the data such as for example searching for all BLUE's is directly a search for that, whilst with converted data you may have issues distinguishing between BLOAT and actual data
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.

Android room: query list items against string column

I have an list of strings:
val mylist = listOf("cat","flower")
and a table that has a string typed column named question
I can write the query to find questions that are exactly matched with one of list items:
#Query("SELECT * FROM objects WHERE question IN (:mylist)")
List<Object> queryObjects(List<String> mylist);
But in fact the question column data is not of single word type, but string. I need to find results that every one of the list items are in that strings .for example the record : is this a cat
The use of IN is basically an = test of the expression on the the left of the IN clause against the list of values on the right. That is only exact matches are considered.
However, what you want is multiple LIKE's with wild characters, and an OR between each LIKE e.g question LIKE '%cat%' OR question LIKE '%flower%' or perhaps CASE WHEN THEN ELSE END or perhaps a recursive common table expression (CTE).
The former two (LIKEs or CASEs) would probably have to be done via an #RawQuery where the LIKE/CASE clauses are built at run time.
The Recursive CTE option would basically build a list of words (but could get further complicated if, anything other than spaces, such as punctuation marks were included.)
Another option could be to consider Full Text Search (FTS). You may wish to refer to https://www.raywenderlich.com/14292824-full-text-search-in-room-tutorial-getting-started
Working Example LIKE's
Here's an example of implementing the simplest, multiple LIKEs clauses separated with ORs:-
Objects (the Entity):-
#Entity
data class Objects(
#PrimaryKey
val id: Long? = null,
val question: String
)
AllDAO (the Daos):-
#Dao
interface AllDAO {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(objects: Objects)
#RawQuery
fun getObjectsRawQuery(query: SupportSQLiteQuery): List<Objects>
fun getObjects(values: List<String>): List<Objects> {
var i = 0
val sb = StringBuilder().append("SELECT * FROM objects WHERE ")
for(v in values) {
if (i++ > 0) {
sb.append(" OR ")
}
sb.append(" question LIKE '%${v}%'")
}
sb.append(";")
return getObjectsRawQuery(SimpleSQLiteQuery(sb.toString()))
}
}
TheDatabase (not uses .allowMainThreadQueries for convenience and brevity):-
#Database(entities = [Objects::class], version = 1, exportSchema = false)
abstract class TheDatabase: RoomDatabase() {
abstract fun getAllDAO(): AllDAO
companion object {
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
}
}
}
Putting it all together, loading some test data and running some extracts:-
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()
dao.insert(Objects(question = "This is a cat."))
dao.insert(Objects(question = "This is a flower."))
dao.insert(Objects(question = "this is nothing."))
dao.insert(Objects(question = "The quick brown fox jumped over the lazy dog"))
logObjects(dao.getObjects(listOf("cat","dog")),"Extract1\t")
logObjects(dao.getObjects(listOf("flower","cat")),"Extract2\t")
logObjects(dao.getObjects(listOf("brown","nothing")),"Extract3\t")
}
fun logObjects(objects: List<Objects>,prefix: String) {
for (o in objects) {
Log.d("OBJECTINFO","$prefix Question is ${o.question} ID is ${o.id}")
}
}
}
Result
2022-04-18 04:58:05.471 D/OBJECTINFO: Extract1 Question is This is a cat. ID is 1
2022-04-18 04:58:05.471 D/OBJECTINFO: Extract1 Question is The quick brown fox jumped over the lazy dog ID is 4
2022-04-18 04:58:05.473 D/OBJECTINFO: Extract2 Question is This is a cat. ID is 1
2022-04-18 04:58:05.473 D/OBJECTINFO: Extract2 Question is This is a flower. ID is 2
2022-04-18 04:58:05.474 D/OBJECTINFO: Extract3 Question is this is nothing. ID is 3
2022-04-18 04:58:05.474 D/OBJECTINFO: Extract3 Question is The quick brown fox jumped over the lazy dog ID is 4
Note in the above no consideration has been given to handling an empty list (a failure would occur due to the syntax error of SELECT * FROM objects WHERE ;). That is the example is just intended to demonstrate the basic principle.

SQLiteException: no such table: database-notes (code 1 SQLITE_ERROR)

I'm trying to migrate a new database version. The only thing changed is an added column. I always get the following error:
android.database.sqlite.SQLiteException: no such table: database-notes (code 1 SQLITE_ERROR): , while compiling: ALTER TABLE 'database-notes' ADD COLUMN image TEXT
I don't understand why I get this exception, because my table is named database-notes as written in the .build() call.
This is my database class:
#Database(
version = 2,
entities = [Note::class],
exportSchema = true)
abstract class AppDatabase : RoomDatabase() {
abstract fun noteDao(): NoteDAO
companion object {
fun build(context: Context) = Room.databaseBuilder(context, AppDatabase::class.java, "database-notes")
.addMigrations(MIGRATION_1_2).build()
}
}
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE 'database-notes' ADD COLUMN image TEXT")
}
}
The database name was exactly the same in the previous version. I copied it to rule out typos.
What have I overlooked here? Thank you in advance!
because my table is named database-notes
It would appear not, due to the failure, and is probably a misunderstanding of the difference between the database name and table name(s).
A Database can have multiple tables. The database name is the name of the file itself (the container of the components such as tables, indexes, views and triggers).
In your code database-notes, as per the 3rd parameter to the Room.databaseBuilder is the name of the database (the file).
With Room the table names are derived from the classes that are both annotated with #Entity and provided, via the entities parameter of the #Database annotation. In your case the Note class.
The name of the table will be Note unless you use the tableName = parameter of the #Entity annotation to provide another name.
Example
If the following were your Note class :-
#Entity // No tableName parameter so the table name is the class name
data class Note(
#PrimaryKey
var noteId: Long? = null,
var noteText: String,
var image: String
)
Then the table name would be Note (the name of the class)
If the Note class were :-
#Entity(tableName = "notes") //<<<<< specifically names the table
data class Note(
#PrimaryKey
var noteId: Long? = null,
var noteText: String,
var image: String
)
The the table name would be notes (as specified by the tableName = parameter of the #Entity annotation).

how to populate intermediate table in Android

I have two tables with a many-to-many relationship. So I created an intermediate table, but I can't find a way to populate this table correctly because I can't set a correct list from my data.
I have a list of 'courses' : each 'course' can have one or several categories.
So my table looks like this :
|idcourses|title|date|categories|
|----|----|----|----|
|700|title1|01012021|[54]|
|701|title2|01022021|[54]|
|702|title3|01032021|[48]|
|868|title4|01042021|[47, 52, 54]|
If I try a map like this :
val myMap = coursesList.map { itcategory to it.idcourses}.distinct()
I have this kind of result :
([54], 700), ([54], 701), ([48], 702), ([47, 52, 54], 868)
The whole "[47, 52, 54]" is considered as one string but I want it to be split so I can have this :
([54], 700), ([54], 701), ([48], 702), ([47], 868), ([52], 868), ([54], 868)
Does anyone know how to achieve this ??
I believe that you may be trying to do this the wrong way as it appears that your intermediate table has a column where you are expecting a list of category id's.
You cannot have a column that is a list/array it has to be a single object.
However rather than try to fix that, what would typically be used for an intermediate table is a table that primarily has a single row per mapping. That is two columns that make up a mapping. Where the two columns are a composite primary key.
other columns that have data specific to the mapping can be used.
In your case one column to map/reference/relate/associate to the course and an second column to map the course.
For example, say you have the Course Table and the Category Table per:-
#Entity
data class Course(
#PrimaryKey
val idcourses: Long? = null,
val title: String,
val date: String
)
and
#Entity
data class Category(
#PrimaryKey
val idcategories: Long? = null,
val name: String
)
Then you could have the intermediate table as :-
#Entity(primaryKeys = ["idcoursesmap","idcategoriesmap"])
data class CourseCategoryMap(
val idcoursesmap: Long,
#ColumnInfo(index = true)
val idcategoriesmap: Long
)
the index on the idcategoriesmap will likely improve the efficiency. Room would also issue a warning.
you may wish to consider defining Foreign Key constraints to enforce referential integrity. None have been included for brevity.
This is sufficient for a many-many relationship.
You would probably want to retrieve Courses with the Categories so you would probably want a POJO for this such as:-
data class CourseWithCategories(
#Embedded
val course: Course,
#Relation(
entity = Category::class,
parentColumn = "idcourses",
entityColumn = "idcategories",
associateBy = Junction(
value = CourseCategoryMap::class,
parentColumn = "idcoursesmap",
entityColumn = "idcategoriesmap"
)
)
val categories: List<Category>
)
Here's some Dao's that would or may be wanted/useful:-
abstract class AllDao {
#Insert(onConflict = IGNORE) // Insert single Category
abstract fun insert(category: Category): Long
#Insert(onConflict = IGNORE) // Insert Single Course
abstract fun insert(course: Course): Long
#Insert(onConflict = IGNORE) // Insert Single CourseCategoryMap
abstract fun insert(courseCategoryMap: CourseCategoryMap): Long
/* Inserts many course category maps */
#Insert(onConflict = IGNORE)
abstract fun insert(courseCategoryMaps: List<CourseCategoryMap>): List<Long>
#Query("SELECT * FROM course WHERE course.title=:courseTitle")
abstract fun getCourseByTitle(courseTitle: String): Course
#Query("SELECT * FROM category WHERE category.name LIKE :categoryMask")
abstract fun getCategoriesByNameMask(categoryMask: String): List<Category>
/* For retrieving courses with all the courses categories */
#Transaction
#Query("SELECT * FROM course")
abstract fun getAllCoursesWithCategories(): List<CourseWithCategories>
#Transaction
#Query("")
fun insertManyCataegoriesForACourseByIds(idcourse: Long,categories: List<Long>) {
for (categoryId: Long in categories) {
insert(CourseCategoryMap(idcourse,categoryId))
}
}
// Anoher possibility
#Transaction
#Query("")
fun insertManyCategoriesForACourse(course: Course, categories: List<Category>) {
val categoryIds = ArrayList<Long>()
for (c: Category in categories) {
categoryIds.add(c.idcategories!!)
}
insertManyCataegoriesForACourseByIds(course.idcourses!!,categoryIds)
}
}
Demonstration
To demonstrate the above, a pretty standard class annotated with #Database :-
const val DATABASE_NAME = "the_database.db"
const val DATABASE_VERSION =1
#Database(entities = [Course::class,Category::class,CourseCategoryMap::class], exportSchema = false, version = DATABASE_VERSION)
abstract class TheDatabase: RoomDatabase() {
abstract fun getAllDao(): AllDao
companion object {
#Volatile
private var instance: TheDatabase? = null
fun getInstance(context: Context): TheDatabase {
if (instance == null) {
instance = Room.databaseBuilder(context,TheDatabase::class.java, DATABASE_NAME)
.allowMainThreadQueries()
.build()
}
return instance as TheDatabase
}
}
}
And activity code to replicate what it looks like your are attempting (but twice to show 2 ways of mapping, the second using category id's that are 20 greater then the first) :-
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()
dao.insert(Course(idcourses = 700,title = "title1", date = "01012021"))
dao.insert(Course(701,"title2","01022021"))
dao.insert(Course(702,"title3","01032021"))
dao.insert(Course(868,"title4","01042021"))
// add quite a few categories for demo
for(i in 30..300) {
dao.insert(Category(i.toLong(),"Category${i}"))
}
//example of what you are trying to do (first)
var currentCourse = dao.getCourseByTitle("title1")
dao.insertManyCataegoriesForACourseByIds(currentCourse.idcourses!!, listOf(54))
dao.insertManyCataegoriesForACourseByIds(dao.getCourseByTitle("title2").idcourses!!, listOf(54))
dao.insertManyCataegoriesForACourseByIds(dao.getCourseByTitle("title3").idcourses!!, listOf(48))
dao.insertManyCataegoriesForACourseByIds(dao.getCourseByTitle("title4").idcourses!!, listOf(47,52,54))
// second (does the same but uses categroyids 20 greater than the first)
val coursecategorymaplist = listOf<CourseCategoryMap>(
CourseCategoryMap(700,74),
CourseCategoryMap(701,74),
CourseCategoryMap(702,68),
CourseCategoryMap(868,67),
CourseCategoryMap(868,72),
CourseCategoryMap(868,74)
)
dao.insert(coursecategorymaplist)
// Extract results
for (cwc: CourseWithCategories in dao.getAllCoursesWithCategories()) {
Log.d("DBINFO","Course is ${cwc.course.title}, date is ${cwc.course.date} it has ${cwc.categories.size} categories they are:-")
for (c: Category in cwc.categories) {
Log.d("DBINFO","\tCategory is ${c.name}")
}
}
}
Results
The log includes (note double the number of categories):-
D/DBINFO: Course is title1, date is 01012021 it has 2 categories they are:-
D/DBINFO: Category is Category54
D/DBINFO: Category is Category74
D/DBINFO: Course is title2, date is 01022021 it has 2 categories they are:-
D/DBINFO: Category is Category54
D/DBINFO: Category is Category74
D/DBINFO: Course is title3, date is 01032021 it has 2 categories they are:-
D/DBINFO: Category is Category48
D/DBINFO: Category is Category68
D/DBINFO: Course is title4, date is 01042021 it has 6 categories they are:-
D/DBINFO: Category is Category47
D/DBINFO: Category is Category52
D/DBINFO: Category is Category54
D/DBINFO: Category is Category67
D/DBINFO: Category is Category72
D/DBINFO: Category is Category74
The Database
The Course Table :-
The Category Table (partial)
The CourseCategoryMap (intermediate table)

Android room select distinct table.calender from table

The problem faced is to get calendar field from table.
My actual Data class is:
#TypeConverters(CalendarConverters::class)
#Entity(tableName = MY_TABLE)
data class MyEpg(
val updatedAt: Calendar,
val epgName: String,
val epgStartTime: Calendar,
val epgEndTime: Calendar,
#ColumnInfo(name = "calendar")
val calendar: Calendar,
val chName: String
) {
#ColumnInfo(name = "id")
#PrimaryKey(autoGenerate = true)
var id: Long = 0
}
I've used typeconverter as:
object CalendarConverters {
#TypeConverter
#JvmStatic
fun fromTimestamp(value: Long?): Calendar? = value?.let { value ->
GregorianCalendar().also { calendar ->
calendar.timeInMillis = value
}
}
#TypeConverter
#JvmStatic
fun toTimestamp(timestamp: Calendar?): Long? = timestamp?.timeInMillis
}
The query I used is:
#Query("select distinct ${MY_TABLE}.calendar from $MY_TABLE order by ${MY_TABLE}.calendar asc")
suspend fun getCalendars(): List<Calendar>
Error Shown in build view is
The query returns some columns [calendar] which are not used by
java.lang.Object. You can use #ColumnInfo annotation on the fields to
specify the mapping. You can suppress this warning by annotating the
method with #SuppressWarnings(RoomWarnings.CURSOR_MISMATCH). Columns
returned by the query: calendar. Fields in java.lang.Object: .
public abstract java.lang.Object getCalendars(#org.jetbrains.annotations.NotNull()
error: Not sure how to convert a Cursor to this method's return type
(java.lang.Object).
public abstract java.lang.Object getCalendars(#org.jetbrains.annotations.NotNull()
Note:
I've noticed that type converter to convert to long is being used while converting from long to date is not being used.
There is no any problem on getting any single fields or the complete object except for the single calendar item.
I reproduced your code but I'm not getting that error. However, I have some observations for you:
I noticed that as table name you use MY_TABLE but in the query you use My_TABLE with "lowercase y". Is this a typo?
You can avoid the use of #JvmStatic by replacing your object CalendarConverters to simple class. Object works with singleton pattern.
I'm supposing that the cause could be that you have invalid data inserted in the table MyEpg table, so then, when you do a request using your query, the column calendar contains a null value. That's could be the reason why you receive the next:
error: Not sure how to convert a Cursor to this method's return type (java.lang.Object). public abstract java.lang.Object getCalendars(#org.jetbrains.annotations.NotNull()
You could solve the problem by adding a nullable indicator to the return type:
#Query("select distinct ${MY_TABLE}.calendar from $MY_TABLE order by ${MY_TABLE}.calendar asc")
suspend fun getCalendars(): List<Calendar?>
Maybe you've forgotten to apply your TypeConverter (or have chosen wrong place to put the annotation)? There are two ways to do that in your case:
Apply it on Database-level:
#Database(entities = arrayOf(YourEntity::class), version = 1)
#TypeConverters(CalendarConverters::class)
abstract class AppDatabase : RoomDatabase() {
...................
Apply it on Entity level:
#Entity(tableName = "your_table")
#TypeConverters(CalendarConverters::class)
data class YourEntity (
and Dao level:
#TypeConverters(CalendarConverters::class)
suspend fun getCalendars(): List<Calendar>

Categories

Resources