How to export Room Database to .CSV - android

How can I export my Room Database to a .CSV file. I would like it to be saved to device storage. I searched everything and no answer was suitable. I hope there is a way for this.

You cannot just save a database as a CSV. However the database, if fully checkpointed, is just a file. If not fully checkpointed then it (unless write-ahead logging as been disabled) would be three files.
The database itself consists of various parts, a header (first 100 bytes of the file) and then blocks of data for the various components. Most of these dependant upon the schema (the tables), there are also system tables
sqlite_master is a table that holds the schema
if autogenerate = true is used for a integer type primary key then there is also the sqlite_sequence table
room itself has the room_master_table in which room stores a hash, this being compared against a compiled hash based upon the Room's expected schema.
To save all that data as a CSV, would be complex (and needless as you can just copy the database file(s)).
If what you want is a CSV of the app's data, then that would depend upon the tables. If you a single table then extracting the data as a CSV would be relatively simple but could be complicated if the data includes commas.
If there are multiple tables, then you would have to distinguish the data for the tables.
Again the simplest way, if just securing the data is to copy the file.
However as an example based upon :-
A database that has 3 tables (apart from the system tables)
PostDataLocal (see below for columns)
GroupDataLocal
AdminDataLocal
an existing answer has been adapted for the example
Then:-
The following in an #Dao annotated interface (namely AllDao) :-
#Query("SELECT postId||','||content FROM postDataLocal")
fun getPostDataLocalCSV(): List<String>
#Query("SELECT groupPostIdMap||','||groupId||','||groupName FROM groupDataLocal")
fun getGroupDataLocalCSV(): List<String>
#Query("SELECT adminGroupIdMap||','||userId||','||adminName||','||avatar FROM adminDataLocal")
fun getAdminDataLocalCSV(): List<String>
And the following function where dao is an AllDao instance previously instantiated :-
private fun createCSV() {
val sb = StringBuilder()
var afterFirst = false
sb.append("{POSTDATALOCAL}")
for (s in dao.getPostDataLocalCSV()) {
if(afterFirst) sb.append(",")
afterFirst = true
sb.append(s)
}
afterFirst = false
sb.append("{GROUPDATALOCAL}")
for (s in dao.getGroupDataLocalCSV()) {
if (afterFirst) sb.append(",")
afterFirst = true
sb.append(s)
}
afterFirst = false
sb.append("{ADMINDATALOCAL}")
for (s in dao.getAdminDataLocalCSV()) {
if ((afterFirst)) sb.append(",")
afterFirst = true
sb.append(s)
}
Log.d("CSV_DATA","CSV is :-\n\t$sb")
}
And then in an activity (where dao has been instantiated) the following:-
createCSV()
Then, when the database contains the following data (extracted via App Inspection) :-
PostDataLocal
GroupDataLocal
AdminDataLocal
The result written to the log (as could be written to a file rather than the log) is :-
D/CSV_DATA: CSV is :-
{POSTDATALOCAL}1,Post001,2,Post002,3,Post003{GROUPDATALOCAL}1,1,Group001 (Post001),1,2,Group002 (Post001),1,3,Group003 (Post001),2,4,Group004 (Post002),2,5,Group005 (Post002),3,6,Group006 (Post003){ADMINDATALOCAL}1,1,Admin001,admin001.gif,1,2,Admin002,admin002.gif,1,3,Admin003,admin003.gif,2,4,Admin004,admin004.gif,2,5,Admin005,admin005.gif,3,6,Admin006,admin006.gif,4,7,Admin007,admin007.gif,5,8,Admin008,admin008.gif,6,9,Admin009,admin009.gif,6,10,Admin010,admin010.gif
Note how headers have been included to distinguish between the tables
of course no consideration has been given to the inclusion of commas in the data (the above is intended to just show that in-principle you can generate a CSV representation of the data relatively easily)
Additional
Here's a more automated version in which you don't need to create the #Query annotated functions, rather it interrogates sqlite_master to extract the tables and the uses the table_info pragma to ascertain the columns, building the respective SQL.
As such it should cater for any Room database.
It also allows for the replacement of commas in the data with an indicator of a comma that could then be replaced when processing the CSV.
The supportive (secondary/invoked by the primary) function being :-
private fun getTableColumnNames(tableName: String, suppDB: SupportSQLiteDatabase): List<String> {
val rv = arrayListOf<String>()
val csr = suppDB.query("SELECT name FROM pragma_table_info('${tableName}')",null)
while (csr.moveToNext()) {
rv.add(csr.getString(0))
}
csr.close()
return rv.toList()
}
And the Primary function :-
private fun AutoCreateCSV(): String {
val replaceCommaInData = "{COMMA}" /* commas in the data will be replaced by this */
val rv = StringBuilder()
val sql = StringBuilder()
var afterFirstTable = false
var afterFirstColumn = false
var afterFirstRow = false
val suppDb = db.getOpenHelper().writableDatabase
var currentTableName: String = ""
val csr = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE('sqlite_%') AND name NOT LIKE('room_%') AND name NOT LIKE('android_%')", null)
while (csr.moveToNext()) {
sql.clear()
sql.append("SELECT ")
currentTableName = csr.getString(0)
if (afterFirstTable) rv.append(",")
afterFirstTable = true
afterFirstColumn = false
rv.append("{$currentTableName},")
for (columnName in getTableColumnNames(currentTableName,suppDb)) {
if (afterFirstColumn) sql.append("||','||")
afterFirstColumn = true
sql.append("replace(`$columnName`,',','$replaceCommaInData')")
}
sql.append(" FROM `${currentTableName}`")
val csr2 = db.query(sql.toString(),null)
afterFirstRow = false
while (csr2.moveToNext()) {
if (afterFirstRow) rv.append(",")
afterFirstRow = true
rv.append(csr2.getString(0))
}
csr2.close()
}
csr.close()
return rv.toString()
}
Using the same data and as the primary function returns a String the following code Log.d("CSV_DATA2",AutoCreateCSV()) results in :-
D/CSV_DATA2: {PostDataLocal},1,Post001,2,Post002,3,Post003,{GroupDataLocal},1,1,Group001 (Post001),1,2,Group002 (Post001),1,3,Group003 (Post001),2,4,Group004 (Post002),2,5,Group005 (Post002),3,6,Group006 (Post003),{AdminDataLocal},1,1,Admin001,admin001.gif,1,2,Admin002,admin002.gif,1,3,Admin003,admin003.gif,2,4,Admin004,admin004.gif,2,5,Admin005,admin005.gif,3,6,Admin006,admin006.gif,4,7,Admin007,admin007.gif,5,8,Admin008,admin008.gif,6,9,Admin009,admin009.gif,6,10,Admin010,admin010.gif
and if the data includes a comma e.g. Post001 is changed to be the value Post001, <<note the comma in the data>>
Then :-
D/CSV_DATA2: {PostDataLocal},1,Post001{COMMA} <<note the comma in the data>>,2,Post002,3 ....
this additional solution also fixes a little bug in the first where some separating commas were omitted between the header and the data.

Get all your data as a list from room and use this library
https://github.com/doyaaaaaken/kotlin-csv
It works well, here is my usage
private fun exportDatabaseToCSVFile(context: Context, list: List<AppModel>) {
val csvFile = generateFile(context, getFileName())
if (csvFile != null) {
exportDirectorsToCSVFile(csvFile, list)
} else {
//
}
}
private fun generateFile(context: Context, fileName: String): File? {
val csvFile = File(context.filesDir, fileName)
csvFile.createNewFile()
return if (csvFile.exists()) {
csvFile
} else {
null
}
}
private fun getFileName(): String = "temp.csv"
fun exportDirectorsToCSVFile(csvFile: File, list: List<AppModel>) {
csvWriter().open(csvFile, append = false) {
// Header
writeRow(listOf("row1", "row2", "row3"))
list.forEachIndexed { index, appModel ->
writeRow(listOf(getRow1, getRow2, getRow3))
}
shareCsvFile(csvFile)
}
}
private fun shareCsvFile(csvFile: File) {
// share your file, don't forget adding provider in your Manifest
}

Related

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 do I prepopulate 2 tables in my room database? (kotlin)

I have 2 db files in asset. I have to prepopulate 2 tables
#Database(entities = [Quotes::class, Anime::class], version = 1, exportSchema = true)
Here I have tried something but it isn't working
#Provides
#Singleton
fun provideDatabase(
app: Application,
)= Room.databaseBuilder(app, QuotesDatabase::class.java, "quotes_database")
.createFromAsset("quotess.db")
.createFromAsset("animes.db")
.fallbackToDestructiveMigration()
.build()
You have a few options.
The simplest way would be to combine the two into a single asset. This could be done using one of the SQLite tools (SQliteStudio, DBeaver, Navicat for SQLite, DB Browser for SQLite).
You could, without using createFromAsset allow Room to build the database and the open each asset in turn and copy the data in the onCreate callback. When the onCreate callback is invoked, the database has been created along with the tables and it is passed to the callback as a SupportSQLiteDatabase. You could then copy each asset to a suitable location (databases directory), open the asset as an SQLiteDatabase, for each table, extract the data into a Cursor and then load the data from the Cursor into the SupportSQliteDatabase. You can the close the two SQLiteDatabase and then delete them.
A third option, would be to, prior to building the Room database, create the database (according to the SQL that can be found in the generated java), copying the two asset to a suitable location (databases directory) attaching both to the created database, copying the data from both to the respective tables detach the two asset databases and delete them. Then when building the Room database it will exist and be opened.
I don't believe that you need to, but you may have to set the user version of the created database.
Here's an in-principle (untested) example of the second option that you may find useful.
:-
#Database(entities = [Quotes::class,Anime::class], exportSchema = false, version = 1)
abstract class QuotesDatabase: RoomDatabase() {
abstract fun getAllDao(): AllDao
companion object {
#Volatile
private var instance: QuotesDatabase?=null
private var quotesAssetFileName = "quotess.db" /* CHANGE AS REQUIRED */
private var animeAssetFileName = "animes.db" /* CHANGE AS REQUIRED */
private var quotesTableName = "quotes_table" /* CHANGE AS REQUIRED */
private var animeTablename = "anime_table" /* CHANGE AS REQUIRED */
fun getInstance(context: Context): QuotesDatabase {
if (instance==null) {
instance = Room.databaseBuilder(context,QuotesDatabase::class.java,"quotes_database")
.addCallback(cb)
.build()
}
return instance as QuotesDatabase
}
val cb = object: Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
db.beginTransaction() /* MAY NOT BE ABLE TO BE USED - IF NOT REMOVE (as well as similar below)*/
copyAsset(Application().applicationContext, quotesAssetFileName)
val asset1db = SQLiteDatabase.openDatabase(Application().getDatabasePath(quotesAssetFileName).path,null,0)
populateFromCursor(asset1db.rawQuery("SELECT * FROM $quotesTableName",null), quotesTableName,db)
copyAsset(Application().applicationContext, animeAssetFileName)
val asset2db = SQLiteDatabase.openDatabase(Application().getDatabasePath(animeAssetFileName).path,null,0)
populateFromCursor(asset2db.rawQuery("SELECT * FROM $animeTablename",null), animeTablename,db)
db.setTransactionSuccessful() /* MAY NOT BE ABLE TO BE USED - IF NOT REMOVE (as well as similar below)*/
db.endTransaction() /* MAY NOT BE ABLE TO BE USED - IF NOT REMOVE (as well as similar below)*/
deleteAssetCopy(asset1db)
deleteAssetCopy(asset2db)
}
}
/* Populates the Room database using the extracted data (Cursor) from the asset copy database */
#SuppressLint("Range")
fun populateFromCursor(csr: Cursor, tableName: String, db: SupportSQLiteDatabase) {
val cv = ContentValues()
while (csr.moveToNext()) {
for (c in csr.columnNames) {
cv.put(c,"'" + csr.getString(csr.getColumnIndex(c))+"'")
}
db.insert(tableName,OnConflictStrategy.IGNORE,cv)
}
csr.close()
}
/* Copies the asset to the asset database */
fun copyAsset(context: Context, assetFileName: String) {
val asset = context.assets.open(assetFileName)
val db = context.getDatabasePath(assetFileName)
val os: OutputStream = db.outputStream()
asset.copyTo(os)
}
/* Deletes the copied assets database */
fun deleteAssetCopy(db: SQLiteDatabase) {
File(db.path).delete()
}
}
}

How to access value outside of foreach on Android Studio, Kotlin, Room Database

I am writing a code for the access room database. I want to use some values outside of foEeach. for example:
my DAO query is:
#Query("SELECT * FROM table_users WHERE id = :id ")
fun getSingleUserDetails(id: Int): List<UsersEntity>
and accessing on MainActivity:
val db = AppDB.getApplicationDatabase(this)
db.boxLocationsDao().getSingleUserDetails(rowId).forEach {
val rowId = it.id
val userName = it.name }
In this case I can use all values within the forEach {}, but now I want to use the "userName" (or any other values) outside of forEach's "{}". How can I do that?
It's quite simple you can create a variable rowId and userName before forEach{} and store value inside forEach{} so you can use stored value outside forEach.
I have attached the code below.
var rowId:Int = 0
var userName:String = ""
val db = AppDB.getApplicationDatabase(this)
db.boxLocationsDao().getSingleUserDetails(rowId).forEach {
rowId = it.id
userName = it.name
}
//use **rowId** and **userName** variables here(outside foreEach{})

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%';) :-

Requery & creating database from sql dump: how to?

I'm trying to use requery https://github.com/requery/requery library with Kotlin and SQLite backend. I have a sql dump, which I want to write to sqlite database in the first launch of an application, and then I want to map data classes to database entities with requery.
Here is data source initialization with table creation:
if (!(DataStorage.isDbInitialized(context))) {
val db = writableDatabase
val inputStream = context?.resources?.openRawResource(R.raw.dump)
val reader = BufferedReader(InputStreamReader(inputStream))
val builder = StringBuilder()
var line : String?
var end = false
while (!end) {
line = reader.readLine()
if(line == null) {
end = true
} else {
builder.append(line)
}
}
db.execSQL(builder.toString())
onCreate(db)
DataStorage.setDbInitialized(context)
}
I have to derive this class from both SqlitexDatabaseSource and CommonDataSource to use with Kotlin. SQL query execuled successfully, but when I trying to select all objects from database, this request returns zero sized list:
val result : Result<Feat> = (application as MainApp).dataStore.select(Feat::class).get()
result.each {
Log.d("TAG", it.name)
}
DTO created as described in documentation:
https://github.com/Syjgin/PathfinderFeats/blob/master/app/src/main/java/com/syjgin/pathfinderfeats/model/Feat.kt
Is it possible to initialize requery data with sql dump, or I have to create DTO for each row and submit it via insert method?

Categories

Resources