Android room: query list items against string column - android

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.

Related

Do I have a way to search objects on Room Database

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.

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)

How to saparate room databases by months?

I want to make separated room databases due to my needs which is showing the data by months. For example: I need to show the expenses of April month so I need to export a database that represent April month's expenses and use it just for this month. Is there any solution for this? Here is my database:
Expense.kt
#Entity(tableName = "expenses_table")
data class Expense (
#PrimaryKey(autoGenerate = true)
val id: Int,
val expenseDate: String,
val expenseType: String,
val expenseCost: Int
)
ExpenseDao.kt
#Dao
interface ExpenseDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun addExpense(expense: Expense)
#Query("SELECT * FROM expenses_table ORDER BY id ASC")
fun readAllData(): LiveData<List<Expense>>
}
ExpenseDatabase.kt
#Database(entities = [Expense::class], version = 1, exportSchema = false)
abstract class ExpenseDatabase: RoomDatabase() {
abstract fun expenseDao(): ExpenseDao
companion object {
#Volatile
private var INSTANCE: ExpenseDatabase? = null
fun getDatabase(context: Context): ExpenseDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance
}
synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
ExpenseDatabase::class.java,
"expense_table"
).build()
INSTANCE = instance
return instance
}
}
}
}
I want to make separated room databases due to my needs which is showing the data by months.
The need for getting data by months does not equate to the need to have separate databases. However, the following is an example that just requires a few modifications to your ExpenseDatabase class :-
#Database(entities = [Expense::class], version = 1, exportSchema = false)
abstract class ExpenseDatabase: RoomDatabase() {
abstract fun expenseDao(): ExpenseDao
companion object {
#Volatile
private var INSTANCE: ExpenseDatabase? = null
fun getDatabase(context: Context, /* ADDED >>>>>*/yearMonthPrefix: String, /* ADDED >>>>>*/ swap: Boolean = false): ExpenseDatabase {
val tempInstance = INSTANCE
if (tempInstance != null && !swap) {
return tempInstance
}
synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
ExpenseDatabase::class.java,
/* CHANGED >>>>>*/ "${yearMonthPrefix}_expense_table")
.build()
INSTANCE = instance
return instance
}
}
}
}
From the information you have provided. The simplest and probably most efficient solution to your problem is to have a single database where all expenses are stored in the expenses_table and a query is used to extract the expenses for the month.
The important factor here is the expenseDate column/field and the suitability of the format of the stored data. If you use an SQLite recognised format such as YYYY-MM-DD then this format is known/understood by the SQLite Date/Time functions.
If so you could then use the following to get a list of the Expense's for the current month.
#Query("SELECT * FROM expenses_table WHERE strftime('%Y%m',expenseDate) = strftime('%Y%m','now') ORDER BY id ASC")
fun readCurrentMonthsData(): LiveData<List<Expense>>
this taking advantage of the SQLite strftime function and the now time value
The following is a variation where you pass the year and month as a string and can thus get any month's data for any year:-
#Query("SELECT * FROM expenses_table WHERE substr(expenseDate,1,7)=:datepart ORDER BY id ASC")
fun readMonthsData(datepart: String): LiveData<List<Expense>>
this uses the SQLite substr function
DEMO
Consider the following ( .allowMainTrhreadQueries added to the buildDatabase to allow demo to use the main thread) :-
Note includes the queries (demo versions that return List<Expense> as opposed to LiveData<List<Expense>> for convenience and brevity)
class MainActivity : AppCompatActivity() {
lateinit var db: ExpenseDatabase
lateinit var dao: ExpenseDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = ExpenseDatabase.getDatabase(this, "202201")
dao = db.expenseDao()
dao.addExpenseDemo(Expense(0,"2022-01-01","Type",100))
dao.addExpenseDemo(Expense(0,"2022-01-11","Type",100))
dao.addExpenseDemo(Expense(0,"2022-01-21","Type",100))
dao.addExpenseDemo(Expense(0,"2022-01-31","Type",100))
/* Swap to February Dataabase */
db = ExpenseDatabase.getDatabase(this,"202202",true)
dao = db.expenseDao()
dao.addExpenseDemo(Expense(0,"2022-02-01","Type",100))
dao.addExpenseDemo(Expense(0,"2022-02-02","Type",100))
dao.addExpenseDemo(Expense(0,"2022-02-03","Type",100))
for (e: Expense in dao.readCurrentMonthsDataDemo()) {
Log.d("EXPENSEINFO001","Expense ID is ${e.id} Date is ${e.expenseDate} etc.")
}
/* None will be located as only 2022-02 rows in database */
for (e: Expense in dao.readMonthsDataDemo("2022-01")) {
Log.d("EXPENSEINFO002","Expense ID is ${e.id} Date is ${e.expenseDate} etc.")
}
/* Swap to January Database */
db = ExpenseDatabase.getDatabase(this,"202201", true)
dao = db.expenseDao()
/* None will be located as only 2022-01 rows in database */
for (e: Expense in dao.readCurrentMonthsDataDemo()) {
Log.d("EXPENSEINFO003","Expense ID is ${e.id} Date is ${e.expenseDate} etc.")
}
for (e: Expense in dao.readMonthsDataDemo("2022-01")) {
Log.d("EXPENSEINFO004","Expense ID is ${e.id} Date is ${e.expenseDate} etc.")
}
}
}
Demo Results (included in the log) :-
D/EXPENSEINFO001: Expense ID is 1 Date is 2022-02-01 etc.
D/EXPENSEINFO001: Expense ID is 2 Date is 2022-02-02 etc.
D/EXPENSEINFO001: Expense ID is 3 Date is 2022-02-03 etc.
D/EXPENSEINFO004: Expense ID is 1 Date is 2022-01-01 etc.
D/EXPENSEINFO004: Expense ID is 2 Date is 2022-01-11 etc.
D/EXPENSEINFO004: Expense ID is 3 Date is 2022-01-21 etc.
D/EXPENSEINFO004: Expense ID is 4 Date is 2022-01-31 etc.
The databases via App Inspection :-
And via Device File Explorer :-
Note that although the actual database files are only 4k each that the data in the -wal file will be applied (not all of it but at least 12K (at least 4K per table)). So multiple database files will waste a relatively high amount of the file space per database.
swapping databases will also result additional overheads.
That would not be an ideal solution. Even if you find a solution imagine after an year you will be having 12 different databases.
I will suggest you to query the database according to your need.

How to ensure unique constraint over multiple columns in Room, even if one of them is null?

I have a Room database in my application with one table containing received and sent messages. Inside of the table, the messages are just differentiated by the phone number, being null for the backend-server (since a server has no phone number) and the phone number of the user for the sent messages. (Entered on app installation, just as Whatsapp.)
To sync the table with the backend, I introduced a new column, containing the backend id of the messages on the server. Since the server seperates sent and received messages (due to different information contained in the tables backend), the id of a sent message and the id of a received message can be equal, only distinguishable by the corresponding phone number. (Either null or own)
So I created a unique constraint over both columns: backend_id & phone number.
#Entity(indices = [Index(value = ["backend_id", "senderNumber"], unique = true)])
data class Message(
var senderNumber: String?,
var message: String?,
var backend_id: String? = null,
var time : Date? = Date(),
var status : Status = Status.PENDING
) : ListItem(time), Serializable {
#PrimaryKey(autoGenerate = true) var id : Long? = null
}
But trying it out with some messages, I had to realize, that the database gladly accepts equal backend_ids, if the phone number is null. To make sure this was not an accident, I even wrote a UnitTest:
#RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
lateinit var db : MyDatabase
lateinit var dao : MessageDao
#Before
fun createDb() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = Room.inMemoryDatabaseBuilder(
context, MyDatabase::class.java).build()
dao = db.messageDao()
}
#After
#Throws(IOException::class)
fun closeDb() {
db.close()
}
#Test(expected = Exception::class)
fun check_unique_constraint_is_violated() {
// Context of the app under test.
val message = Message(senderNumber = null, backend_id = "1", time = Date(), message = "Hello")
dao.insertAll(message)
dao.insertAll(message)
val allMessages = dao.getAll()
assertTrue(allMessages.size==2)
assertTrue(allMessages[0].backend_id==allMessages[1].backend_id)
}
}
This test fails, since it doesn´t throw any exception. Debugging it shows, that the Room database also doesn´t catch the exception silently, since both messages (being the same) are being inserted successfully, resulting in 2 messages.
So my question is: How can I ensure, that the result is unique over both columns, even if one of them is null? It seems a bit weird to me, that you can pass-by uniqueness, just by inserting null for one of the columns. It worked, when I only checked the backend_id in the index, throwing exceptions, when a sent and a received message had the same id. (But I obviously don´t want that.)
In case Database and Dao have any relevance to the solution:
Database:
#Database(entities = [Message::class], version = 1)
#TypeConverters(Converters::class)
abstract class MyDatabase : RoomDatabase() {
override fun init(configuration: DatabaseConfiguration) {
super.init(configuration)
//Create and execute some trigger, limiting the entries on the latest 50
}
abstract fun messageDao() : MessageDao
companion object {
private var db: MyDatabase? = null
private fun create(context : Context) : MyDatabase {
return Room.databaseBuilder(context, MyDatabase::class.java, "dbname").build()
}
fun getDB(context : Context) : MyDatabase {
synchronized(this) {
if(db==null) {
db = create(context)
}
return db!!
}
}
}
}
MessageDao:
#Dao
interface MessageDao {
#Query("SELECT * FROM Message")
fun getAll() : List<Message>
#Insert
fun insertAll(vararg messages: Message) : List<Long>
}
In SQLite (and others that conform to SQL-92) null is considered different to any other null and hence your issue.
As such you should not be using null. You can overcome this setting the default value to a specific value that indicates a no supplied value.
For example you could use:-
#NotNull
var backend_id: String = "0000000000"
0000000000 could be any value that suits your purpose.
"" could also be used.
Altenative
An alternative approach could be to handle the null in the index such as :-
#Entity(indices = [Index(value = ["coalesce(backend_id,'00000000')", "senderNumber"], unique = true)])
HOWEVER, Room will issue an error message because it doesn't determine that the backend_id column is the column being indexed and thus issues a compilation error e.g. :-
error: coalesce(backend_id,'00000000') referenced in the index does not exists in the Entity.
Therefore you would need to add the index outside of Room's creation of tables. You could do this via the onCreate or onOpen callback. Noting that onCreate is only called once when the database is first created and that onOpen is called every time the app is run.
The safest (data wise) but slightly less efficient is to use the onOpen callback.
Here's an example that creates the index (applying it to both columns, considering that both backend_id and senderNumber columns can be null).
This being done when building the database :-
....
.addCallback(object :RoomDatabase.Callback() {
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
db.execSQL(
"CREATE UNIQUE INDEX IF NOT EXISTS message_unique_sn_beid " +
"ON message (coalesce(backend_id,''),coalesce(senderNumber,''));")
}
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
}
})
.build()
....
The index name would be message_unique_sn_beid
Results using the Alternative
Basing the Message Entity on your (but with fewer columns) and an Insert Dao of :-
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(message: Message): Long
using the following (and with the index added via the onOpen callback) the when running :-
dao.insert(Message(null,"1234567890","blah"))
dao.insert(Message(null,"0123456789","blah","0123456789"))
dao.insert(Message(null,"1234567890","blah"))
dao.insert(Message(null,"1234567890","blah",null))
dao.insert(Message(null,null,message = "blah",backend_id = "9876543210"))
dao.insert(Message(null,null,message = "blah",backend_id = "9876543210"))
1st and 2nd rows will be added, 3rd and 4th rows will be ignored due to UNIQUE conflict 5th (3rd row in table) will be added, 6th will be ignored due to UNIQUE conflict.
Using Android Studio's Database Inspector:-
1. The message table :-
2. Looking at the sqlite_master (the schema) at items starting with mess (i.e. running SQL SELECT * FROM sqlite_master WHERE name LIKE 'mess%';) :-

How to get values of LiveData from repository in viewmodel?

Quiz app, categories have questions and questions have answers.
I have two queries in the DAO.
In the first one, I get all the categories from the data:
#Query("SELECT * from category_table ORDER BY category_id")
fun getAllCategories(): LiveData<List<Category>>
In the second one, I get a list of questions and answers by category id:
#Query("SELECT * FROM question_table WHERE parent_category_id = :categoryId ")
fun getQuestionsWithAnswersByCategoryId(categoryId: Long): LiveData<List<QuestionWithAnswers>>
Repository:
val getAllCategories: LiveData<List<Category>> = quizDao.getAllCategories()
fun getQuestionsWithAnswersByCategoryId(id: Long): LiveData<List<QuestionWithAnswers>> {
return quizDao.getQuestionWithAnswers(id)
}
This is my viewmodel:
val getAllCategories: LiveData<List<Category>>
var questionById: LiveData<List<QuestionWithAnswers>>
init {
getAllCategories = repository.getAllCategories
questionByCategoryId = repository.getQuestionWithAnswersByCategoryId(???????)
}
The problem is that I don't know the categories' id beforehand and need to obtain them from the database.
when I try to obtain a category id in the viewmodel like this:
var categoryId = getAllCategories.value[0].categoryId
it returns null.
Is there any way to obtain categories id which wrapped in livedata inside a viewmodel?

Categories

Resources