How to saparate room databases by months? - android

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.

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 can I persist user data on updating/migrating a database?

My database with products (name, price) gets initialized using createFromAsset. This works for static data, however it contains a column Favorite for marking a product as favorite.
The asset I initialize the database with could look like :
Name
Price
Favorite
Product A
5,99
no
Product B
6,99
no
I want to update the database; change a product's price and add a new product. However, I want the Favorite column to keep the value set by the user. If user marked "Product B" favorite and I change its price and add a Product C, this is what the database should look like after migration:
Name
Price
Favorite
Product A
5,99
no
Product B
1,99
yes
Product C
6,99
no
How to achieve this using Android Room? The only workaround I found :
Use fallbackToDestructiveMigration.
Update asset .db file so that it includes Product C.
Update database version.
-> Old database gets deleted, user sees Product C and updated price on Product B.
#Database(
entities = arrayOf(Product::class),
version = 2,
exportSchema = true
)
fun getDatabase(context: Context, scope: CoroutineScope): ProductDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
ProductDatabase ::class.java,
"product_database"
).createFromAsset("database/products.db")
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
instance
}
}
However, this resets the Favorite column. I also tried AutoMigration, but that leaves the existing database unaltered so the user doesn't see Product C and updated Product B's price.
How would I solve this? Do I need to store the favorites in a separate database?
I don't know much about room migrations, but you can save a map of ProductID -> isFavorite to SharedPreferences or local storage.
Now that I think about it, you don't even need a map. Just a list of favorite products IDs, so next time you load the app, you set up the favorites column.
However, I think you should put the favorite productIDs in a separate table.
I think there might be something about room migration rules (like there is for Realm DB), but I don't know enough of that to tell you. Good luck!
Here's an adaptation of this answer
As an overview the core addition is the applyUpdatedAssetData function.
The function is called the when the single instance of the #Database annotated class is retrieved and importantly before the Room databaseBuilder builds the database.
It first checks to see if the actual database exists, if not then it does nothing and Room will copy the Asset.
Otherwise if then checks the version of the asset against the version of the actual database. If the actual database is at a lower version then it:-
Creates a copy of the asset as an SQliteDatabase and additionally opens the actual database as an SQliteDatabase. The rows are extracted from the asset into a Cursor. The Cursor is traversed and tries to update the Name and the price (importantly not the favourite), it then tries to insert the row.
If no matching row exists then it obviously is not updated and will be inserted.
If the row is a matching row (id or name in the example) then it will be updated and not inserted.
Here's a full working example (only briefly tested, but based very much on a tested use as per the link above).
Note unlike the link the code is all in Kotlin
The Database components including the functions used by the applyUpdateAssetData function:-
const val DATABASE_NAME = "products.db"
const val ASSET_NAME = DATABASE_NAME
const val ASSET_COPY_DATABASE_NAME = "asset_copy_$DATABASE_NAME"
#Entity(
indices = [
Index("name", unique = true)
]
)
data class Product(
#PrimaryKey
var id: Long?=null,
var name: String,
var price: Double,
var favourite: Boolean
)
#Dao
interface ProductDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(product: Product): Long
#Query("SELECT * FROM Product")
fun getAllProducts(): List<Product>
#Query("UPDATE product SET favourite =NOT favourite WHERE id=:id OR name=:name")
fun toggleFavourite(id: Long, name: String)
#Query("UPDATE product SET favourite=1 WHERE id=:id OR name=:name")
fun setFavourite(id: Long, name: String)
}
#Database(entities = [Product::class], version = DATABASE_VERSION)
abstract class ProductDatabase: RoomDatabase() {
abstract fun getProductDao(): ProductDao
companion object {
private var instance: ProductDatabase?=null
fun getInstance(context: Context): ProductDatabase {
if (instance==null) {
applyUpdatedAssetData(context) /*<<<<<<<<<< */
instance=Room.databaseBuilder(context,ProductDatabase::class.java, DATABASE_NAME)
.allowMainThreadQueries()
.createFromAsset(DATABASE_NAME)
.addMigrations(Migration1to2,Migration2to3)
.build()
}
return instance as ProductDatabase
}
/* Dummy Migrations */
object Migration1to2: Migration(1,2) {
override fun migrate(database: SupportSQLiteDatabase) {
Log.d("MIGRATIONINFO","Migration Invoked FROM=${this.startVersion} TO=${this.endVersion}")
}
}
object Migration2to3: Migration(2,3) {
override fun migrate(database: SupportSQLiteDatabase) {
Log.d("MIGRATIONINFO","Migration Invoked FROM=${this.startVersion} TO=${this.endVersion}")
}
}
// etc
/**
* Apply changes (update existing rows or add new rows )
*/
#SuppressLint("Range")
fun applyUpdatedAssetData(context: Context) {
if (!doesDatabaseExist(context, DATABASE_NAME)) return
val assetVersion = getAssetVersion(context, ASSET_NAME)
val actualVersion = getDBVersion(context.getDatabasePath(DATABASE_NAME))
Log.d("VERSIONINFO","Asset Version is $assetVersion. Actual DB Version is $actualVersion Code Db Version is ${DATABASE_VERSION}")
if (actualVersion < assetVersion) {
Log.d("APPLYASSET","As the asset version is greater than the actual version then apply data from the asset.")
getCopyOfDatabaseFromAsset(context, ASSET_NAME, context.getDatabasePath(
ASSET_COPY_DATABASE_NAME).path)
val assetDb = SQLiteDatabase.openDatabase(
context.getDatabasePath(ASSET_COPY_DATABASE_NAME).path,
null,
SQLiteDatabase.OPEN_READWRITE
)
val db = SQLiteDatabase.openDatabase(context.getDatabasePath(DATABASE_NAME).path,null,SQLiteDatabase.OPEN_READWRITE)
val assetCursor = assetDb.query("Product",null,null,null,null,null,null)
val cv = ContentValues()
db.beginTransaction()
/* Apply updates and or insert new data */
while (assetCursor.moveToNext()) {
cv.clear()
/*
First prepare to update existing data i.e. just the name and price columns,
id and favourites will be unchanged.
If row doesn't exists then nothing to update (NO ERROR)
*/
cv.put("name",assetCursor.getString(assetCursor.getColumnIndex("name")))
cv.put("price",assetCursor.getDouble(assetCursor.getColumnIndex("price")))
db.update("product",cv,"id=?", arrayOf(assetCursor.getString(assetCursor.getColumnIndex("id"))))
/*
Now get the id and favourite and try to insert
if id exists then insert will be ignored
*/
cv.put("id",assetCursor.getLong(assetCursor.getColumnIndex("id")))
cv.put("favourite",assetCursor.getInt(assetCursor.getColumnIndex("favourite")))
db.insert("product",null,cv)
}
/* Cleanup */
assetCursor.close()
db.setTransactionSuccessful()
db.endTransaction()
db.close()
assetDb.close()
deleteAssetCopy(context, ASSET_COPY_DATABASE_NAME)
}
}
/* Test to see if the database exists */
private fun doesDatabaseExist(context: Context, databaseName: String): Boolean {
return File(context.getDatabasePath(databaseName).path).exists()
}
/* Copy the asset into the databases folder */
private fun getCopyOfDatabaseFromAsset(context: Context, assetName: String, copyName: String) {
val assetFile = context.assets.open(assetName)
val copyDb = File(context.getDatabasePath(copyName).path)
if (!copyDb.parentFile!!.exists()) {
copyDb.parentFile!!.mkdirs()
} else {
if (copyDb.exists()) {
copyDb.delete()
}
}
assetFile.copyTo(FileOutputStream(copyDb),8 * 1024)
}
/* delete the copied asset */
private fun deleteAssetCopy(context: Context, copyName: String) {
if (File(context.getDatabasePath(copyName).path).exists()) {
File(context.getDatabasePath(copyName).path).delete()
}
}
/* SQLite database header values */
private val SQLITE_HEADER_DATA_LENGTH = 100 /* Size of the database header */
private val SQLITE_HEADER_USER_VERSION_LENGTH = 4 /* Size of the user_version field */
private val SQLITE_HEADER_USER_VERSION_OFFSET = 60 /* offset of the user_version field */
/* Get the SQLite user_version from the existing database */
private fun getDBVersion(f: File): Int {
var rv = -1
val buffer = ByteArray(SQLITE_HEADER_DATA_LENGTH)
val istrm: InputStream
try {
istrm = FileInputStream(f)
istrm.read(buffer, 0, buffer.size)
istrm.close()
rv = getVersionFromBuffer(buffer)
} catch (e: IOException) {
e.printStackTrace()
}
return rv
}
/* Get the SQLite user_version from the Asset database */
private fun getAssetVersion(context: Context, asset: String): Int {
var rv = -1
val buffer = ByteArray(SQLITE_HEADER_DATA_LENGTH)
val istrm: InputStream
try {
istrm = context.assets.open(asset)
istrm.read(buffer, 0, buffer.size)
istrm.close()
rv = getVersionFromBuffer(buffer)
} catch (e: IOException) {
e.printStackTrace()
}
return rv
}
/**
* Extract the SQlite user_version from the database header
*/
fun getVersionFromBuffer(buffer: ByteArray): Int {
val rv = -1
if (buffer.size == SQLITE_HEADER_DATA_LENGTH) {
val bb: ByteBuffer = ByteBuffer.wrap(
buffer,
SQLITE_HEADER_USER_VERSION_OFFSET,
SQLITE_HEADER_USER_VERSION_LENGTH
)
return bb.int
}
return rv
}
}
}
For testing then the following in MainActivity :-
const val DATABASE_VERSION = 2
lateinit var db: ProductDatabase
lateinit var dao: ProductDao
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = ProductDatabase.getInstance(this)
dao = db.getProductDao()
logAllProducts("_RUN")
if (DATABASE_VERSION == 1) {
dao.setFavourite(-99999 /* obviously not an id that exists */,"Product B")
logAllProducts("_TOG")
}
}
fun logAllProducts(tagSuffix: String) {
for (p in dao.getAllProducts()) {
Log.d(
"PRDCTINFO$tagSuffix",
"ID is ${p.id} " +
"NAME is ${p.name}" +
" PRICE is ${p.price}" +
" FAVOURITE is ${p.favourite}" +
" DBVERSION=${db.openHelper.writableDatabase.version}"
)
}
}
}
And in the assets folder :-
where the respective V1 or V2 would be used to overwrite products.db accordingly.
First Run (using products.db in asset as at version 1 i.e. productsV1.db copied to replace products.db )
D/PRDCTINFO_RUN: ID is 1 NAME is Product A PRICE is 5.99 FAVOURITE is false DBVERSION=1
D/PRDCTINFO_RUN: ID is 2 NAME is Product B PRICE is 6.99 FAVOURITE is false DBVERSION=1
D/PRDCTINFO_TOG: ID is 1 NAME is Product A PRICE is 5.99 FAVOURITE is false DBVERSION=1
D/PRDCTINFO_TOG: ID is 2 NAME is Product B PRICE is 6.99 FAVOURITE is true DBVERSION=1
Second run (App just rerun)
D/PRDCTINFO_RUN: ID is 1 NAME is Product A PRICE is 5.99 FAVOURITE is false DBVERSION=1
D/PRDCTINFO_RUN: ID is 2 NAME is Product B PRICE is 6.99 FAVOURITE is true DBVERSION=1
D/PRDCTINFO_TOG: ID is 1 NAME is Product A PRICE is 5.99 FAVOURITE is false DBVERSION=1
D/PRDCTINFO_TOG: ID is 2 NAME is Product B PRICE is 6.99 FAVOURITE is true DBVERSION=1
as can be seen B is true before and after
Third Run - increase to use updated V2 asset and DATABASE_VERSION changed to 2
2022-09-17 10:52:58.183 D/VERSIONINFO: Asset Version is 2. Actual DB Version is 1 Code Db Version is 2
2022-09-17 10:52:58.184 D/APPLYASSET: As the asset version is greater than the actual version then apply data from the asset.
2022-09-17 11:06:09.717 D/VERSIONINFO: Asset Version is 2. Actual DB Version is 1 Code Db Version is 2
2022-09-17 11:06:09.717 D/APPLYASSET: As the asset version is greater than the actual version then apply data from the asset.
2022-09-17 11:06:09.777 E/SQLiteDatabase: Error inserting favourite=0 id=1 name=Product A price=5.99
android.database.sqlite.SQLiteConstraintException: UNIQUE constraint failed: Product.id (code 1555 SQLITE_CONSTRAINT_PRIMARYKEY)
at ....
2022-09-17 11:06:09.778 E/SQLiteDatabase: Error inserting favourite=0 id=2 name=Product B price=1.99
android.database.sqlite.SQLiteConstraintException: UNIQUE constraint failed: Product.id (code 1555 SQLITE_CONSTRAINT_PRIMARYKEY)
at ....
2022-09-17 11:06:09.897 D/MIGRATIONINFO: Migration Invoked FROM=1 TO=2
2022-09-17 11:06:09.967 D/PRDCTINFO_RUN: ID is 1 NAME is Product A PRICE is 5.99 FAVOURITE is false DBVERSION=2
2022-09-17 11:06:09.968 D/PRDCTINFO_RUN: ID is 2 NAME is Product B PRICE is 1.99 FAVOURITE is true DBVERSION=2
2022-09-17 11:06:09.970 D/PRDCTINFO_RUN: ID is 3 NAME is Product C PRICE is 7.99 FAVOURITE is false DBVERSION=2
As can be seen:-
The respective versions (3 of them) have been logged as expected, and thus that the change has been detected.
That there were trapped UNIQUE constraints AS EXPECTED i.e. the inserts of the two existing rows were ignored (albeit that the trapped errors were logged)
That the dummy Migration was invoked as expected and did nothing.
That B still has true for the Favourite
The the Price for B has been changed
That the new Product C has been added

how to get today ,past or future date data from room database in android?

how to get today ,past or future date data from room database in android?
below is the model class and there is a task_date field I have taken with Date object.
Model class
#Entity
data class Task(
#PrimaryKey
val tid: Long?,
#ColumnInfo(name = "title") val title: String?,
#ColumnInfo(name = "task_date") val task_date: Date?,
#ColumnInfo(name = "task_hour") val task_hour: Int?,
#ColumnInfo(name = "task_minute") val task_minute: Int?,
#ColumnInfo(name = "task_cat") val task_cat: String?,
#ColumnInfo(name = "task_repeat") val task_repeat: String?,
) {
override fun toString(): String {
return "Task(tid=$tid, title=$title, task_date=$task_date, task_hour=$task_hour, task_minute=$task_minute, task_cat=$task_cat, task_repeat=$task_repeat)"
}
}
Below There is query code
i am passing Date() today date to get today inserted data list
#Query("SELECT * FROM task WHERE task_date = :targetDate")
fun getUpcomingTask(targetDate: Date): List<Task>
Data insertion code is here
val task = Task(
Utils.getUniqueId(),
bindingActivity.inputTaskTitle.text.toString(),
Date(),
selectedHour, selectedMinute,
bindingActivity.mySpinnerDropdown.text.toString(),
Constant.REPEAT.NONE
)
Converter Class
class Converters {
#TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
#TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time
}
}
And the final one DataBase class
#Database(entities = [Task::class], version = 1, exportSchema = false)
#TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun taskDao(): TaskDao
}
I have shown the code implementation I never worked with date object
so need your help to learn this date implementation Moreover , I also
want to retrieve data like upcoming data and past data .Please also
give your valuable advice to learn more with date
.
Thank You.
Your issue is with exactly what is being stored and passed due to the conversion of the date to a long.
That is the long is accurate down to a millisecond, so when query for tasks that equals the "date" it doesn't know that you only want the date part so it will be virtually impossible to get a task of the very millisecond.
Consider the following data (based upon using your code) for 3 tasks, it will look like:-
ignore all but the task_date column
ALL 3 rows were inserted immediately after each other at 14:10 on 2022-07-08, yet they all have different values as the value includes the milliseconds.
So SELECT * FROM task WHERE task_date = :targetDate will not probably never get any records.
Todays
However consider this query SELECT * FROM task WHERE task_date / (1000 /* drop millis*/ * 60 /* drop seconds */ * 60 /* drop minutes */ * 24 /* drop hours */) = :targetDate / 86400000
Note comments explain how the 86400000 was derived (it's the same value applied to both sides of the comparison). i.e. stripping of every but the date from the stored long value.
Future and Past
For Future and the past it wouldn't matter about the milliseconds so you could then use SELECT * FROM task WHERE task_date > :targetDate (for Future) and SELECT * FROM task WHERE task_date < :targetDate (for past)

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.

How to create a TypeConverter that converts LocalDate to format that Room can understand/save?

I am using Room. I need guidence on how to convert LocalDate from java.time to a format (may be Long, TimeStamp, sql.Date or I do not even know what else) so that I can save a date to database.
I have a book entity:
#Entity
data class Book(
#ColumnInfo(name = "date") val date: LocalDate,
#ColumnInfo(name = "count") val count: Int,
#PrimaryKey(autoGenerate = true)
val id: Int? = null
)
I have also created Book DAO:
#Dao
interface BookDao {
#Query("SELECT * FROM book WHERE :date >= book.date")
fun getBooks(date: LocalDate): List<Book>
}
Now, I am not sure how to create converter that converts LocalDate to . . . (again, I do not even know to what I should convert my LocalDate. Is it Long, TimeStamp, sql.Date or anything else).
I thought I should be converting LocalDate to sql.Date so that Room can save it. So, I created my converter like this:
Converters.kt
class Converters {
#TypeConverter
fun convertLocalDateToSqlDate(localDate: LocalDate?): Date? {
return localDate?.let { Date.valueOf(localDate.toString()) }
}
#TypeConverter
fun convertSqlDateToLocalDate(sqlDate: Date?): LocalDate? {
val defaultZoneId = systemDefault()
val instant = sqlDate?.toInstant()
return instant?.atZone(defaultZoneId)?.toLocalDate()
}
}
But, I am getting error:
error: Cannot figure out how to save this field into database. You can consider adding a type converter for it.
private final java.time.LocalDate date = null;
The issue you have is that a TypeConverter should convert from/to a set of specific types that Room supports for the insertion/extraction of Data.
Some of the more common ones are Int, Long Byte, Byte[], Float, Double, Decimal, String, BigInt, BigDecimal.
there may be other types but the types that can be used are limited
So the message is saying please convert your LocalDate or Date as it cannot handle them.
I'd suggest storing dates as unix dates (unless you want microsecond accuracy when you can store the time with milliseconds (this can be a little awkward when using date/time functions in sqlite and may need casts)) i.e. as a Long or Int.
In doing so you:
will minimise the amount of storage used to store the values,
will be able to take advantage of the SQLite Date and Time functions,
can easily extract them into very flexible formats,
you can extract them as is and then use class functions to format convert them,
you will not need TypeConverters for the above.
Here's an example showing some of the functionality, storing them as unix timestamps
First a version of your Book Entity utilising a default for the date :-
#Entity
data class Book(
#ColumnInfo(name = "date", defaultValue = "(strftime('%s','now','localtime'))")
val date: Long? = null,
#ColumnInfo(name = "count")
val count: Int,
#PrimaryKey(autoGenerate = true)
val id: Int? = null
)
see SQLITE Date and Time Functions for explanation of the above. In short the above allows you to insert a row and have the date and time set to the current date time
see the insertBook function for doing inserting with just the count provided.
To accompany the Book Entity the Book Dao :-
#Dao
interface BookDao {
#Insert /* insert via a book object */
fun insert(book: Book): Long
#Query("INSERT INTO BOOK (count) VALUES(:count)")
/* insert supplying just the count id and date will be generated */
fun insertBook(count: Long): Long
#Query("SELECT * FROM book")
/* get all the books as they are stored into a list of book objects*/
fun getAllFromBook(): List<Book>
/* get the dates as a list of dates in yyyy-mm-dd format using the SQLite date function */
#Query("SELECT date(date, 'unixepoch', 'localtime') AS mydateAsDate FROM book")
fun getDatesFromBook(): List<String>
/* get the date + 7 days */
#Query("SELECT date(date,'unixepoch','localtime','+7 days') FROM book")
fun getDatesFromBook1WeekLater(): List<String>
}
see the comments (and activity and results)
The activity (pretty standard BookDatabase class so not included)
class MainActivity : AppCompatActivity() {
lateinit var db: BookDatabase
lateinit var dao: BookDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = BookDatabase.getInstance(this)
dao = db.getBookDao()
/* Insert two rows */
dao.insert(Book(System.currentTimeMillis() / 1000,10)) /* don't want millisecs */
dao.insertBook(20) /* use the date columns default value (similar to the above) */
/* get and log the date objects */
for(b: Book in dao.getAllFromBook()) {
Log.d("BOOKINFO_BOOK", " Date = ${b.date} Count = ${b.count} ID = ${b.id}")
}
/* get the dates in yyy-mm-dd format */
for(s: String in dao.getDatesFromBook()) {
Log.d("BOOKINFO_DATE",s)
}
/* get the dates but with a week added on */
for(s: String in dao.getDatesFromBook1WeekLater()) {
Log.d("BOOKINFO_1WEEKLATER",s)
}
}
}
The Log after running :-
2021-04-16 14:28:50.225 D/BOOKINFO_BOOK: Date = 1618547330 Count = 10 ID = 1
2021-04-16 14:28:50.225 D/BOOKINFO_BOOK: Date = 1618583330 Count = 20 ID = 2
2021-04-16 14:28:50.226 D/BOOKINFO_DATE: 2021-04-16
2021-04-16 14:28:50.227 D/BOOKINFO_DATE: 2021-04-17
2021-04-16 14:28:50.228 D/BOOKINFO_1WEEKLATER: 2021-04-23
2021-04-16 14:28:50.228 D/BOOKINFO_1WEEKLATER: 2021-04-24
(note that the adjustment for local time (albeit it incorrectly used and thus jumping forward))
first 2 rows data as stored in the Book object and the database
next 2 rows the date converted to YYYY-MM-DD format by SQLite Date/Time function
last 2 rows the date a week after the stored date converted to YYYY-MM-DD
The Database as per Database Inspector :-
if you want to use converters, you can follow the documentation's way. In your example, you try to convert to SQLdate, but Android suggests to Long.
class Converters {
#TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
#TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time?.toLong()
}
}
and then add the #TypeConverters annotation to the AppDatabase.
With LocalDate it should be something like this:
class Converters {
#TypeConverter
fun fromTimestamp(value: Long?): LocalDate? {
return value?.let { LocalDate.ofEpochDay(it) }
}
#TypeConverter
fun dateToTimestamp(date: LocalDate?): Long? {
val zoneId: ZoneId = ZoneId.systemDefault()
return date?.atStartOfDay(zoneId)?.toEpochSecond()
}
}
source

Categories

Resources