I'm trying to migrate a new database version. The only thing changed is an added column. I always get the following error:
android.database.sqlite.SQLiteException: no such table: database-notes (code 1 SQLITE_ERROR): , while compiling: ALTER TABLE 'database-notes' ADD COLUMN image TEXT
I don't understand why I get this exception, because my table is named database-notes as written in the .build() call.
This is my database class:
#Database(
version = 2,
entities = [Note::class],
exportSchema = true)
abstract class AppDatabase : RoomDatabase() {
abstract fun noteDao(): NoteDAO
companion object {
fun build(context: Context) = Room.databaseBuilder(context, AppDatabase::class.java, "database-notes")
.addMigrations(MIGRATION_1_2).build()
}
}
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE 'database-notes' ADD COLUMN image TEXT")
}
}
The database name was exactly the same in the previous version. I copied it to rule out typos.
What have I overlooked here? Thank you in advance!
because my table is named database-notes
It would appear not, due to the failure, and is probably a misunderstanding of the difference between the database name and table name(s).
A Database can have multiple tables. The database name is the name of the file itself (the container of the components such as tables, indexes, views and triggers).
In your code database-notes, as per the 3rd parameter to the Room.databaseBuilder is the name of the database (the file).
With Room the table names are derived from the classes that are both annotated with #Entity and provided, via the entities parameter of the #Database annotation. In your case the Note class.
The name of the table will be Note unless you use the tableName = parameter of the #Entity annotation to provide another name.
Example
If the following were your Note class :-
#Entity // No tableName parameter so the table name is the class name
data class Note(
#PrimaryKey
var noteId: Long? = null,
var noteText: String,
var image: String
)
Then the table name would be Note (the name of the class)
If the Note class were :-
#Entity(tableName = "notes") //<<<<< specifically names the table
data class Note(
#PrimaryKey
var noteId: Long? = null,
var noteText: String,
var image: String
)
The the table name would be notes (as specified by the tableName = parameter of the #Entity annotation).
Related
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.
I have a database in Android with Room from which I have deleted a column. I was doing the migration, and I saw that it was not as simple as doing a DROP of the deleted column.
Then I have seen that I have to take a series of steps, creating a provisional table that will later be the new table with the deleted column, but the problem is that this table contains a field that is a String Array that I don't know how to declare in SQL.
#Entity(tableName = "recipe_table")
data class RecipesDb(
#PrimaryKey
#ColumnInfo(name = "id")
val id: Long,
#ColumnInfo(name = "name")
val name: String,
#ColumnInfo(name = "category")
val category: List<String>,
#ColumnInfo(name = "isRecommended")
val isRecommended: Boolean,
#ColumnInfo(name = "images")
val images: List<String>,
#ColumnInfo(name = "ingredients")
val ingredients: List<String>,
#ColumnInfo(name = "date")
val date: Long,
#ColumnInfo(name = "time")
val time: Int,
#ColumnInfo(name = "difficult")
val difficult: String,
#ColumnInfo(name = "originalUrl")
val originalURL: String? = null,
#ColumnInfo(name = "author")
val author: String,
#ColumnInfo(name = "siteName")
val siteName: String
)
And now I have removed the ingredients column. I wanted to do something like this:
private val MIGRATION_3_2 = object : Migration(3,2) {
override fun migrate(database: SupportSQLiteDatabase) {
//Drop column isn't supported by SQLite, so the data must manually be moved
with(database) {
execSQL("CREATE TABLE Users_Backup (id INTEGER, name TEXT, PRIMARY KEY (id))")
execSQL("INSERT INTO Users_Backup SELECT id, name FROM Users")
execSQL("DROP TABLE Users")
execSQL("ALTER TABLE Users_Backup RENAME to Users")
}
}
}
But when I declare the new temporary table User_Backup, I have no idea how to specify that one of the fields is an Array. In the end I was able to do it with Room's AutoMigrations and creating an interface, but I would like to know how to do it this way as well.
The simple way is to compile the code (Ctrl+F9) with the changed #Entity annotated classes in the list of entities of the #Database annotation.
Then look at the generated java (visible via the Android View in Android Studio). There will be a class that is the same name as the #Database annotated class but suffixed with _Impl.
In this class there will be a method that is named createAllTables, This includes the SQL that room uses for creating the tables.
Just copy and paste the appropriate SQL and then change the table name, this will not only use the correct type but also apply the correct column constraints that Room expects.
I would suggest
Adding an execSQL("DROP TABLE IF EXISTS the_backup_table_name;") before you create a table (just in case it already exists)
And instead of using execSQL("DROP TABLE Users") to use execSQL("DROP TABLE IF EXISTS the_original_table_name")
Personally I always RENAME the table name of the original, then RENAME the new table and then finally DROP the renamed original.
I would use:-
private val MIGRATION_3_2 = object : Migration(3,2) {
override fun migrate(database: SupportSQLiteDatabase) {
//Drop column isn't supported by SQLite, so the data must manually be moved
with(database) {
execSQL("DROP TABLE IF EXISTS Users_Backup")
execSQL("CREATE TABLE IF NOT EXISTS ....) //<<<<< SEE NOTES BELOW, the SQL MUST BE CHANGED.
execSQL("INSERT INTO Users_Backup SELECT id, name FROM Users")
execSQL("ALTER TABLE Users RENAME TO Old_Users")
execSQL("ALTER TABLE Users_Backup RENAME to Users")
execSQL("DROP TABLE IF EXISTS Old_users")
}
}
}
note .... indicates that the SQL is copied from the generated java and that the table name is changed from Users to Users_Backup
The first line will drop the Uers_backup just in case it happens to exist, it's just a little less likely to fail under unusual circumstances.
Rather than dropping the Users table before the RENAME of the Users_Backup to Users. The 4th execSQL changes the name of the Users table, so should there be an issue with changing the Users_Backup table to be the Users table, then the original Uers table is available as Old_users.
When all has been complted then the original Users table, now named Old_Users is then dropped.
These are all just a little safer/secure.
I have changed a column type from Float to Int, how can I migrate the change so I won't lost old entries, instead just to convert all of them to the Int.
You need to add a Migration that will create the table in it's new form and copy the data from it's old form.
The SQL for the new form can be ascertained by looking at the generated java (visible from the Android View of Android Studio). Look at the class that is named the same as the class that is annotated with #Database but suffixed with _Impl, and then find the SQL in the method named createAllTables
You could then use the following
:-
DROP, just in case the intermediate old table e.g. DROP TABLE IF EXISTS table_old;
RENAME the original table using SQL based upon ALTER TABLE the_table RENAME TO the_table_old;
Create the new table using the SQL as obtained above
Copy the data using SQL based upon INSERT INTO the_table SELECT * FROM the_table_old;
DROP the now defunct old table e.g. DROP TABLE IF EXISTS table_old;;
Demo
As an example where the entity is (was commented out) :-
#Entity(tableName = "jourTable")
class Note(
#ColumnInfo(name = "title") val jourTitle:String,
#ColumnInfo(name = "description") val jourDescription:String,
#ColumnInfo(name = "date") val jourDate:String,
#ColumnInfo(name = "image", typeAffinity = ColumnInfo.BLOB) val jourImage: Bitmap?, //<<<<< will use the TypeConverter
//#ColumnInfo(name = "altImage") val jourAltImage: ByteArray //<<<<< will not use the TypeConverter
#ColumnInfo(name = "altImage") val jourAltImage: Int
) {
#PrimaryKey(autoGenerate = true)var id=0
}
i.e. commented out jourAltImage was ByteArray now to be Int (INTEGER type in SQL)
and the generated java is obtained via :-
The the #Database annotated class (TheDatabase) has :-
#TypeConverters(ImageConverter::class)
#Database(entities = [Note::class], version = 2 /*<<<<<<<<<< INCREASE FROM 1 to 2 (or as required)*/, exportSchema = false)
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,"the_database.db")
.allowMainThreadQueries()
.addMigrations(MIG_1_2) //<<<<<<<<<< ADD the migration
.build()
}
return instance as TheDatabase
}
/*<<<<<<<<<< The Migration >>>>>>>>>> */
val MIG_1_2 = object: Migration(1,2){
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("DROP TABLE IF EXISTS jourTable_old;")
db.execSQL("ALTER TABLE jourTable RENAME TO jourTable_old ")
/* SQL ON NEXT LINE COPIED FROM GENERATED JAVA */
db.execSQL("CREATE TABLE IF NOT EXISTS `jourTable` (`title` TEXT NOT NULL, `description` TEXT NOT NULL, `date` TEXT NOT NULL, `image` BLOB, `altImage` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)")
db.execSQL("INSERT INTO jourTable SELECT * FROM jourTable_old")
db.execSQL("DROP TABLE IF EXISTS jourTable_old")
}
}
}
}
The when run App Inspection shows (it had 1 row) and the app adds 1 new row when run :-
as cab seen the altimage column is now INTEGER (was BLOB) and the 1 row has been retained.
I'm trying to have a prepopulated database with my Android app but I'm finding with the database inspector that the table is empty. I found it was working before I added a "favourite" column but now it doesn't work (also removing "favourites" still doesn't make it work, so I had changed other things too).
#Entity
#Parcelize
data class Quote (
#PrimaryKey(autoGenerate = true) val id: Int,
val quote: String,
val author: String,
val genre: String,
val favourite: Int
) : Parcelable
#Database(entities = [Quote::class], version = 10)
abstract class QuoteDatabase : RoomDatabase() {
abstract fun quoteDao(): QuoteDao
}
private lateinit var INSTANCE: QuoteDatabase
fun getDatabase(context: Context) : QuoteDatabase {
synchronized(QuoteDatabase::class.java) {
if (!::INSTANCE.isInitialized) {
INSTANCE = Room.databaseBuilder(context.applicationContext,
QuoteDatabase::class.java,
"quotes")
.createFromAsset("quotes.db")
.fallbackToDestructiveMigration()
.build()
}
return INSTANCE
}
}
// Exported Schema
"tableName": "Quote",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `quote` TEXT NOT NULL, `author` TEXT NOT NULL, `genre` TEXT NOT NULL, `favourite` INTEGER NOT NULL)"
It looks like my schema is okay and I had even exported the schema from Room and it looks like it matches my current table schema. My file is also directly in "assets/quotes.db" (doesn't have any subfolders like "database"). The DB is being generated within the emulator from device file inspector. But database inspector is showing nothing. Even when I copy the file from the device and open it with DB Browser there's nothing in there. And of course, my prepopulated database has data in it.
What's going wrong?
I've two columns (name and description) in FTS virtual table and I want to prioritize the name column in FTS using matchinfo function, but with room in android I'm getting following error:
Error retrieving data from table.: unable to use function matchinfo in the requested context (code 1 SQLITE_ERROR)
Here is my Query:
#Query("""select workout_cache.id, workout_cache.met, workout_cache.name, workout_cache.workoutDescription, matchinfo(workout_cache_fts, 'pcs') as mi
from workout_cache join workout_cache_fts on workout_cache.id = workout_cache_fts.id
where workout_cache_fts match :text group by workout_cache.id""")
abstract suspend fun query(text: String): List<WorkoutCacheEntityWithMatchInfo>
My WorkoutCacheEntityWithMatchInfo class
class WorkoutCacheEntityWithMatchInfo(
#Embedded
val workout: WorkoutCacheEntity,
#ColumnInfo(name = "mi")
val matchInfo: ByteArray
)
UPDATE: It's working fine when I don't use join clause.
It's silly. I just removed the group by clause and it worked.
Here updated Dao:
#Query("""select workout_cache.id, workout_cache.met, workout_cache.name, workout_cache.workoutDescription, matchinfo(workout_cache_fts, 'pcs') as mi
from workout_cache join workout_cache_fts on workout_cache.id = workout_cache_fts.id
where workout_cache_fts match :text""")
abstract suspend fun query(text: String): List<WorkoutCacheEntityWithMatchInfo>