If we move from SQLite to Room database, will the database remain intact.
Will all the entries in database remain intact.
Schema remain same.
Yes if you do everything correctly.
Please refer to: https://developer.android.com/training/data-storage/room/sqlite-room-migration
All the data can be preserved. However, the actual schema will likely have to be changed as Room has limitations on column types.
Room only accepts column types that are specifically INTEGER, REAL, TEXT or BLOB.
However, SQLite has flexible column types and uses a simple set of rules to assign a type affinity.
e.g. CREATE TABLE mytable (mycolumn CHAR(10)....), even though the type affinity, according to the rules, is TEXT, Room will not accept CHAR(10). So the schema would have to be changed and the migration of the database would have to reflect this.
Room also expects some constraints especially NOT NULL to be exact. The NOT NULL constraint in room is often implied e.g. in Java a primitive will be NOT NULL implicitly on Kotlin, which has no primitives, always objects, takes whether or not the type can be null i.e. a ? that specifies nullability.
It can be a nightmare to ascertain exactly what Room expects. However, it need not be as Room can assist.
After
a) creating the proposed #Entity (a table) annotated classes, and
b) defining the #Entity annotated classes in the entities parameter of the #Database annotation, and
c) then successfully compiling the project (CTRL + F9)
then Room processing will have generated some Java code (even if using Kotlin). This will be located in the java(generated) folder/directory (easily viewable from the Android View). In the generated java class, that is the same name as the #Database annotated class but suffixed with _Impl, there will be a method name createAllTables. This is the SQL according to Room's expectations and can be used for the conversion of the database from the old schema to the new schema.
Demo
Consider the following App (pre Room conversion) :-
The DBHelper class that extends SQLiteOpenHelper:-
class DBHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "the_database.db";
private static final int DATABASE_VERSION = 1;
public static final String USER_TABLE_NAME = "_user";
public static final String NOTE_TABLE_NAME = "_note";
public static final String USER_ID_COL = "_user_id";
public static final String USER_NAME_COL = "_name";
public static final String USER_EMAIL_COL = "_email";
public static final String USER_PASSWORD_COL = "_password";
public static final String USER_TABLE_CREATE_SQL = "CREATE TABLE IF NOT EXISTS " + USER_TABLE_NAME +
"(" +
USER_ID_COL + " INTEGER PRIMARY KEY" +
"," + USER_NAME_COL + " CHAR(64)" +
"," + USER_EMAIL_COL + " EMAIL " +
"," + USER_PASSWORD_COL + " PASSWORDTYPE " +
");";
/* Note purposely using column types that will have to be converted for Room. */
public static final String NOTE_ID_COL = "_note_id";
public static final String NOTE_COL_TIMESTAMP = "_timestamp";
public static final String NOTE_COL_TEXT = "_text";
public static final String NOTE_COL_USER_REF = "_user_id_reference";
public static final String NOTE_TABLE_CREATE_SQL = "CREATE TABLE IF NOT EXISTS " + NOTE_TABLE_NAME +
"(" +
NOTE_ID_COL + " INTEGER PRIMARY KEY " +
"," + NOTE_COL_TIMESTAMP + " MyTimeStampType" +
"," + NOTE_COL_TEXT + " MEMO " +
"," + NOTE_COL_USER_REF + " REFERENCE_PARENT REFERENCES " + USER_TABLE_NAME +"(" + USER_ID_COL + ")" +
");";
DBHelper(Context context) {
super(context,DATABASE_NAME,null,DATABASE_VERSION);
}
#Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(USER_TABLE_CREATE_SQL);
db.execSQL(NOTE_TABLE_CREATE_SQL);
}
#Override
public void onUpgrade(SQLiteDatabase db, int i, int i1) {}
public long insertUser(Long id, String name, String email, String password) {
ContentValues cv = new ContentValues();
if (id != null) {
cv.put(USER_ID_COL,id);
}
cv.put(USER_NAME_COL,name);
cv.put(USER_EMAIL_COL,email);
cv.put(USER_PASSWORD_COL,password);
return this.getWritableDatabase().insert(USER_TABLE_NAME,null,cv);
}
public long insertUser(String name, String email, String password) {
return insertUser(null,name,email,password);
}
public long insertNote(Long id, String text, long userId) {
ContentValues cv = new ContentValues();
if (id != null) {
cv.put(NOTE_ID_COL,id);
}
cv.put(NOTE_COL_TIMESTAMP,System.currentTimeMillis());
cv.put(NOTE_COL_TEXT,text);
cv.put(NOTE_COL_USER_REF,userId);
return this.getWritableDatabase().insert(NOTE_TABLE_NAME,null,cv);
}
public long insertNote(String text, long userId) {
return insertNote(null,text,userId);
}
}
and to use the above MainActivity :-
public class MainActivity extends AppCompatActivity {
DBHelper dbHelper;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
dbHelper = new DBHelper(this);
addSomeData();
}
void addSomeData() {
long fredid = dbHelper.insertUser("Fred","fred#email.com","fred12345678");
if (fredid > 0) {
dbHelper.insertNote("Lorem ipsum dolor sit amet", fredid);
dbHelper.insertNote("consectetur adipiscing elit", fredid);
dbHelper.insertNote("sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", fredid);
}
long maryid = dbHelper.insertUser(100L,"Mary","mary#coldmail.moc","mary87654321");
if (maryid > 0) {
dbHelper.insertNote("Ut enim ad minim veniam", maryid);
dbHelper.insertNote("quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", maryid);
dbHelper.insertNote("Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", maryid);
dbHelper.insertNote(999L,"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", maryid);
}
}
}
And using App Inspection shows:-
and :-
and, using a query:-
So the App has obviously worked and loaded data. You can also see that SQLite's Type Affinity has been at play. That is the column types, according to App inspection, for the columns that had acceptable but non-standard types have been determined accordingly. You can also see that the pre-determined values are in the schema (e.g. MyTimeStampType is that not NUMERIC in sqlite_master).
The Conversion
The first step is to code the database components according to Room.
An #Entity class for the User table:-
#Entity(tableName = DBHelper.USER_TABLE_NAME)
class User {
#PrimaryKey /* MUST ALWAYS HAVE A PRIMARY KEY */
#ColumnInfo( name = DBHelper.USER_ID_COL) /* Optional */
Long userId=null;
#ColumnInfo(name = DBHelper.USER_NAME_COL)
String userName;
#ColumnInfo(name = DBHelper.USER_EMAIL_COL)
String userEmail;
#ColumnInfo(name = DBHelper.USER_PASSWORD_COL)
String userPassword;
}
table name and column names used from the original but not required (via #Entity and #ColumnInfo annotation respectively).
An #Entity annotated class for the Note table (including the Foreign Key):-
#Entity(
tableName = DBHelper.NOTE_TABLE_NAME,
foreignKeys = {
#ForeignKey(
entity = User.class,
parentColumns = {DBHelper.USER_ID_COL},
childColumns = {DBHelper.NOTE_COL_USER_REF}
)
}
)
class Note {
#PrimaryKey
#ColumnInfo(name = DBHelper.NOTE_ID_COL)
Long noteId=null;
#ColumnInfo(name = DBHelper.NOTE_COL_TIMESTAMP)
long noteTimestamp=System.currentTimeMillis();
#ColumnInfo(name = DBHelper.NOTE_COL_TEXT)
String noteText;
#ColumnInfo(name = DBHelper.NOTE_COL_USER_REF)
long noteUserRef;
}
The #Database annotated abstract class (basic to enable compilation and generation of the generated java):-
#Database(entities = {User.class,Note.class}, exportSchema = false, version = DBHelper.DATABASE_VERSION)
abstract class TheDatabase extends RoomDatabase {
}
Compile (Ctrl + F9) and:-
The SQL for that User and Note tables is what Room expects to find.
As the original had methods for inserting rows into each table then the equivalents has to also be coded. So an #Dao annotated interface or interfaces (all in one for brevity):-
#Dao
interface AllDAOs {
#Insert(onConflict = OnConflictStrategy.IGNORE)
long insertUser(User user);
#Insert(onConflict = OnConflictStrategy.IGNORE)
long insertNote(Note note);
}
To enable use of the interface then an amendment to the #Database annotated class. For the demo to actually demonstrate then a singleton will be used so a method for getting the single instance. So:-
#Database(entities = {User.class,Note.class}, exportSchema = false, version = DBHelper.DATABASE_VERSION)
abstract class TheDatabase extends RoomDatabase {
abstract AllDAOs getAllDAOs();
private static TheDatabase instance;
public static TheDatabase getInstance(Context context) {
if (instance==null) {
instance= Room.databaseBuilder(context,TheDatabase.class,DBHelper.DATABASE_NAME)
.allowMainThreadQueries() /* For brevity of the demo */
.build();
}
return instance;
}
}
Now if compiled (will check the DAO methods), the it compiles BUT there is a warning:-
warning: _user_id_reference column references a foreign key but it is not part of an index. This may trigger full table scans whenever parent table is modified so you are highly advised to create an index that covers this column. - a.a.so75484142javaconverttoroom.Note
So a little change to the Note class
#ColumnInfo(name = DBHelper.NOTE_COL_USER_REF, index = true)
long noteUserRef;
Note it is suggested that any change is followed by a compile and that the build log is checked. This can simplify ascertaining what the cause of an issue is.
It is highly likely, due to the relationship between User and Note that access to a User with it's Notes would be required. So to be able to extract such an object then a class (NOT annotated with #Entity i.e. a POJO).
To also demonstrate a few other extracts a few other methods. One to get a List of all Users. Another to get a single User according to the user's id.
So AllDAOs becomes:-
#Dao
interface AllDAOs {
#Insert(onConflict = OnConflictStrategy.IGNORE)
long insertUser(User user);
#Insert(onConflict = OnConflictStrategy.IGNORE)
long insertNote(Note note);
#Transaction
#Query("SELECT * FROM " + DBHelper.USER_TABLE_NAME)
List<UserWithNotes> getAllUsersWithNotes();
#Query("SELECT * FROM " + DBHelper.USER_TABLE_NAME)
List<User> getAllUsers();
#Query("SELECT * FROM " + DBHelper.USER_TABLE_NAME + " WHERE " + DBHelper.USER_ID_COL + "=:userId")
User getUserByUserId(long userId);
}
AND the compile is successful with no warnings.
Note the #Transaction this is as Room will build java that retrieves all of the children when using #Relation.
At this stage the conversion/migration can be considered.
The assumption is that this is an App that has been distributed and has been in use and that the App user's data needs to be retained and is unique to each user. The assumption is that the database version in the App is 1 (if not then respective increases need to be made).
The conversion will be initiated by an attempt to access the database (NOT when an instance of the TheDatabase class is obtained). i.e. the database will be under Room's control (there are alternatives such as perform the migration prior to Room getting control).
To use/demonstrate MainActivity will be changed to utilise Room. So:-
public class MainActivity extends AppCompatActivity {
//DBHelper dbHelper;
TheDatabase db;
AllDAOs dao;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//dbHelper = new DBHelper(this);
//addSomeData();
db = TheDatabase.getInstance(this);
dao = db.getAllDAOs();
/* NOTE Database has not yet been accessed */
for (UserWithNotes uwn: dao.getAllUsersWithNotes()) {
StringBuilder sb = new StringBuilder();
for (Note n: uwn.notes) {
sb.append("\n\tNote TS=" + n.noteTimestamp + " Text is " + n.noteText);
}
Log.d("DBINFO","User is " + uwn.user.userName + " email is " + uwn.user.userEmail + ". There are " + uwn.notes.size() + " notes. They are");
}
}
}
Now if an attempt is made to run the activity (*DO NOT DO THIS) then it will fail with:-
2023-02-18 09:38:12.741 24921-24921/a.a.so75484142javaconverttoroom E/AndroidRuntime: FATAL EXCEPTION: main
Process: a.a.so75484142javaconverttoroom, PID: 24921
java.lang.RuntimeException: Unable to start activity ComponentInfo{a.a.so75484142javaconverttoroom/a.a.so75484142javaconverttoroom.MainActivity}: java.lang.IllegalStateException: Pre-packaged database has an invalid schema: _user(a.a.so75484142javaconverttoroom.User).
Expected:
TableInfo{name='_user', columns={_user_id=Column{name='_user_id', type='INTEGER', affinity='3', notNull=false, primaryKeyPosition=1, defaultValue='undefined'}, _email=Column{name='_email', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='undefined'}, _name=Column{name='_name', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='undefined'}, _password=Column{name='_password', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='undefined'}}, foreignKeys=[], indices=[]}
Found:
TableInfo{name='_user', columns={_user_id=Column{name='_user_id', type='INTEGER', affinity='3', notNull=false, primaryKeyPosition=1, defaultValue='undefined'}, _name=Column{name='_name', type='CHAR(64)', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='undefined'}, _email=Column{name='_email', type='EMAIL', affinity='1', notNull=false, primaryKeyPosition=0, defaultValue='undefined'}, _password=Column{name='_password', type='PASSWORDTYPE', affinity='1', notNull=false, primaryKeyPosition=0, defaultValue='undefined'}}, foreignKeys=[], indices=[]}
What can be ascertained is that the Room has found the database but that it is not what was expected. However, there is no indication that a migration is expected.
So public static final int DATABASE_VERSION = 2; could be used.
Now if run (DO NOT DO THIS), then the failure is:-
java.lang.IllegalStateException: A migration from 1 to 2 was required but not found. Please provide the necessary Migration path via RoomDatabase.Builder.addMigration(Migration ...) or allow for destructive migrations via one of the RoomDatabase.Builder.fallbackToDestructiveMigration* methods.
i.e. Room has yet again found the database, but sees that it was at version 1 but should be version 2 and to use a Migration.
So a Migration can be added to basically alter the tables accordingly. However, as said earlier, deciphering what needs to be changed (as per the first failure) is not the easiest of tasks. Hence, to make life easy, use of the SQL from the generated java.
So the TheDatabase class can now be:-
#Database(entities = {User.class,Note.class}, exportSchema = false, version = DBHelper.DATABASE_VERSION)
abstract class TheDatabase extends RoomDatabase {
abstract AllDAOs getAllDAOs();
private static TheDatabase instance;
public static TheDatabase getInstance(Context context) {
if (instance==null) {
instance= Room.databaseBuilder(context,TheDatabase.class,DBHelper.DATABASE_NAME)
.allowMainThreadQueries() /* For brevity of the demo */
.addMigrations(migratedFromVersion1toVersion2)
.build();
}
return instance;
}
static Migration migratedFromVersion1toVersion2 = new Migration(1,2) {
#Override
public void migrate(#NonNull SupportSQLiteDatabase db) {
String originalSuffix = "_original";
db.execSQL("ALTER TABLE " + DBHelper.USER_TABLE_NAME + " RENAME TO " + DBHelper.USER_TABLE_NAME+originalSuffix);
db.execSQL("ALTER TABLE " + DBHelper.NOTE_TABLE_NAME + " RENAME TO " + DBHelper.NOTE_TABLE_NAME+originalSuffix);
/* SQL below copied from the generated java */
db.execSQL("CREATE TABLE IF NOT EXISTS `_user` (`_user_id` INTEGER, `_name` TEXT, `_email` TEXT, `_password` TEXT, PRIMARY KEY(`_user_id`))");
db.execSQL("CREATE TABLE IF NOT EXISTS `_note` (`_note_id` INTEGER, `_timestamp` INTEGER NOT NULL, `_text` TEXT, `_user_id_reference` INTEGER NOT NULL, PRIMARY KEY(`_note_id`), FOREIGN KEY(`_user_id_reference`) REFERENCES `_user`(`_user_id`) ON UPDATE NO ACTION ON DELETE NO ACTION )");
/* Now to get the original data */
db.execSQL("INSERT INTO " + DBHelper.USER_TABLE_NAME + " SELECT * FROM " + DBHelper.USER_TABLE_NAME+originalSuffix);
db.execSQL("INSERT INTO " + DBHelper.NOTE_TABLE_NAME + " SELECT * FROM " + DBHelper.NOTE_TABLE_NAME+originalSuffix);
/* Create the additional Index (done after the inserts as more efficient) */
db.execSQL("CREATE INDEX IF NOT EXISTS `index__note__user_id_reference` ON `_note` (`_user_id_reference`)");
/* Cleanup */
db.execSQL("DROP TABLE IF EXISTS " + DBHelper.NOTE_TABLE_NAME+originalSuffix);
db.execSQL("DROP TABLE IF EXISTS " + DBHelper.USER_TABLE_NAME+originalSuffix);
}
};
}
And when run the log includes:-
2023-02-18 10:17:20.003 26161-26161/a.a.so75484142javaconverttoroom D/DBINFO: User is Fred email is fred#email.com. There are 3 notes. They are
Note TS=1676667551898 Text is Lorem ipsum dolor sit amet
Note TS=1676667551898 Text is consectetur adipiscing elit
Note TS=1676667551899 Text is sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
2023-02-18 10:17:20.004 26161-26161/a.a.so75484142javaconverttoroom D/DBINFO: User is Mary email is mary#coldmail.moc. There are 4 notes. They are
Note TS=1676667551900 Text is Ut enim ad minim veniam
Note TS=1676667551900 Text is quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Note TS=1676667551901 Text is Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Note TS=1676667551901 Text is Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
i.e. the database is obviously now usable(converted) to Room and retains the original data
Related
I'm having a little bit of a headache with Room and migration with a pre-populated database.
EXPLANATION
I'm currently using Room and a pre-populated database. With the first version (version 1) the database loads fine and everything works correctly.
The problem is that at this point I'm in need to add three new tables to the database with data in them. So I started updating the version 1 database that I had and created all the tables and rows with data in them that I needed.
The first time that I tried, I pushed directly the new .sqlite database into the assets folder and changed the version number to 2, but of course, Room gave the error that it needs to know how to handle migration 1_2 so I added a migration rule
.addMigrations(new Migration(1,2) {
#Override
public void migrate(#NonNull SupportSQLiteDatabase database) {
database.execSQL("CREATE TABLE ...);
database.execSQL("CREATE TABLE ...);
database.execSQL("CREATE TABLE ...);
}
...
Thinking that maybe if I tell Room to create these tables it will then connect to the new database in assets and populate the tables accordingly.
But that of course didn't work and by looking at the database inspector it was clear that the tables were present but they were empty.
A SOLUTION I DON'T REALLY LIKE
After tinkering around for a little bit in the end what I found that worked is to have a copy of the updated database, navigate in it (I'm currently using DB Browser for SQLite), get the SQL query for the newly populated rows, and format a database.execSQL statement accordingly to insert the new data into the tables:
.addMigrations(new Migration(1,2) {
#Override
public void migrate(#NonNull SupportSQLiteDatabase database) {
database.execSQL("CREATE TABLE ...);
database.execSQL("CREATE TABLE ...);
database.execSQL("CREATE TABLE ...);
database.execSQL("INSERT INTO ...");
database.execSQL("INSERT INTO ...");
database.execSQL("INSERT INTO ...");
database.execSQL("INSERT INTO ...");
}
...
I find this an "acceptable" solution for cases in which we're working on rows that contain small data but in my case, I'm handling rows with very long strings and this creates a series of inconveniences:
the SQL statements that are extracted from the database data need to be well formatted: ' symbols need to be handled as well as " that could be present in the long strings as well as line breaks;
consistency between the database and the insert statements for the rows needs to be kept;
QUESTION
Mind that fallbackToDestructiveMigration() is not an acceptable option since the database in Version 1 has user-created data in it and it needs to be kept between migrations.
So, is there a solution that allows me to directly push the new .sqlite database into assets without writing tons and tons of INSERT and CREATE TABLE statements and let Room handle the new data within it automatically and also while keeping the old tables data?
Thank you for your time!
Perhaps consider
Place the new database into the assets folder suitable for a new install of the App so the createFromAsset would copy this version 2 database for a new install.
In the migration copy the asset to a the database folder with a different database name.
in the migration create the new tables.
still in the migration, for each new table, extract all of the data from the differently named new database then use the Cursor to insert the data into the existing database.
still in the migration, close the differently name database and delete the file.
Here's the migration code for something along those lines (no schema change, just new pre-populated data) and it's Kotlin not Java from a recent answer:-
val migration1_2 = object: Migration(1,2) {
val assetFileName = "appdatabase.db"
val tempDBName = "temp_" + assetFileName
val bufferSize = 1024 * 4
#SuppressLint("Range")
override fun migrate(database: SupportSQLiteDatabase) {
val asset = contextPassed?.assets?.open(assetFileName) /* Get the asset as an InputStream */
val tempDBPath = contextPassed?.getDatabasePath(tempDBName) /* Deduce the file name to copy the database to */
val os = tempDBPath?.outputStream() /* and get an OutputStream for the new version database */
/* Copy the asset to the respective file (OutputStream) */
val buffer = ByteArray(bufferSize)
while (asset!!.read(buffer,0,bufferSize) > 0) {
os!!.write(buffer)
}
/* Flush and close the newly created database file */
os!!.flush()
os.close()
/* Close the asset inputStream */
asset.close()
/* Open the new database */
val version2db = SQLiteDatabase.openDatabase(tempDBPath.path,null,SQLiteDatabase.OPEN_READONLY)
/* Grab all of the supplied rows */
val v2csr = version2db.rawQuery("SELECT * FROM user WHERE userId < ${User.USER_DEMARCATION}",null)
/* Insert into the actual database ignoring duplicates (by userId) */
while (v2csr.moveToNext()) {
database.execSQL("INSERT OR IGNORE INTO user VALUES(${v2csr.getLong(v2csr.getColumnIndex("userId"))},'${v2csr.getString(v2csr.getColumnIndex("userName"))}')",)
}
/* close cursor and the newly created database */
v2csr.close()
version2db.close()
tempDBPath.delete() /* Delete the temporary database file */
}
}
Note when testing the above code. I initially tried ATTACH'ing the new (temp) database. This worked and copied the data BUT either the ATTACH or DETACH (or both) prematurely ended the transaction that the migration runs in, resulting in Room failing to then open the database and a resultant exception.
If this wasn't so then with the new database attached a simple INSERT INTO main.the_table SELECT * FROM the_attached_schema_name.the_table; could have been used instead of using the cursor as the go-between.
without writing tons and tons of INSERT and CREATE TABLE statements
INSERT dealt with above.
The CREATE SQL could, in a similar way, be extracted from the new asset database, by using:-
`SELECT name,sql FROM sqlite_master WHERE type = 'table' AND name in (a_csv_of_the_table_names (enclosed in single quotes))`
e.g. SELECT name,sql FROM sqlite_master WHERE type = 'table' AND name IN ('viewLog','message');;
results in (for an arbitrary database used to demonstrate) :-
name is the name of the table and sql then sql that was used to create the tables.
alternately the SQL to create the tables can be found after compiling in the generated java (visible from the Android View) in the class that has the same name as the class annotated with #Database but suffixed with _Impl. There will be a method called createAlltables which has the SQL to create all the tables (and other items) e.g. (again just an arbitrary example) :-
note the red stricken-through lines are for the room_master table, ROOM creates this and it is not required in the asset (it's what room uses to check to see if the schema has been changed)
Working Example
Version 1 (preparing for the migration to Version 2)
The following is a working example. Under Version 1 a single table named original (entity OriginalEnity) with data (5 rows) via a pre-populated database is used, a row is then added to reflect user suplied/input dat. When the App runs the contents of the table are extracted and written to the log :-
D/DBINFOoriginal: Name is name1 ID is 1 - DB Version is 1
D/DBINFOoriginal: Name is name2 ID is 2 - DB Version is 1
D/DBINFOoriginal: Name is name3 ID is 3 - DB Version is 1
D/DBINFOoriginal: Name is name4 ID is 4 - DB Version is 1
D/DBINFOoriginal: Name is name5 ID is 5 - DB Version is 1
D/DBINFOoriginal: Name is App User Data ID is 6 - DB Version is 1
Database Inspector showing :-
Version 2
The 3 new Entities/Tables added (newEntity1,2 and 3 table names new1, new2 and new3 respectively) same basic structure.
After creating the Entities and compiling the SQL, as per the createAlltables method in the java generated was extracted from the TheDatabase_Impl class (including the 3 additional indexes) :-
This SQL was then used in the SQLite tool to create the new tables and populate them with some data :-
/* FOR VERSION 2 */
/* Create statments copied from TheDatabase_Impl */
DROP TABLE IF EXISTS new1;
DROP TABLE IF EXISTS new2;
DROP TABLE IF EXISTS new3;
CREATE TABLE IF NOT EXISTS `new1` (`new1_id` INTEGER, `new1_name` TEXT, PRIMARY KEY(`new1_id`));
CREATE INDEX IF NOT EXISTS `index_new1_new1_name` ON `new1` (`new1_name`);
CREATE TABLE IF NOT EXISTS `new2` (`new2_id` INTEGER, `new2_name` TEXT, PRIMARY KEY(`new2_id`));
CREATE INDEX IF NOT EXISTS `index_new2_new2_name` ON `new2` (`new2_name`);
CREATE TABLE IF NOT EXISTS `new3` (`new3_id` INTEGER, `new3_name` TEXT, PRIMARY KEY(`new3_id`));
CREATE INDEX IF NOT EXISTS `index_new3_new3_name` ON `new3` (`new3_name`);
INSERT OR IGNORE INTO new1 (new1_name) VALUES ('new1_name1'),('new1_name2');
INSERT OR IGNORE INTO new2 (new2_name) VALUES ('new2_name1'),('new2_name2');
INSERT OR IGNORE INTO new3 (new3_name) VALUES ('new3_name1'),('new3_name2');
The database saved and copied into the assets folder (original renamed) :-
Then the Migration code (full database helper), which :-
is driven simply by a String[] of the table names
copies the asset (new database) an opens it via the SQLite API
creates the tables, indexes and triggers according to the asset (must match the schema generated by room (hence copying sql from generated java previously))
it does this by extracting the respective SQL from the sqlite_master table
populates the newly created Room tables by extracting the data from the asset database into a Cursor and then inserting into the Room database (not the most efficient way BUT Room runs the Migration in a transaction)
is:-
#Database(entities = {
OriginalEntity.class, /* on it's own for V1 */
/* ADDED NEW TABLES FOR V2 */NewEntity1.class,NewEntity2.class,NewEntity3.class
},
version = TheDatabase.DATABASE_VERSION,
exportSchema = false
)
abstract class TheDatabase extends RoomDatabase {
public static final String DATABASE_NAME = "thedatabase.db";
public static final int DATABASE_VERSION = 2; //<<<<<<<<<< changed */
abstract AllDao getAllDao();
private static volatile TheDatabase instance = null;
private static Context currentContext;
public static TheDatabase getInstance(Context context) {
currentContext = context;
if (instance == null) {
instance = Room.databaseBuilder(context, TheDatabase.class, DATABASE_NAME)
.allowMainThreadQueries() /* for convenience run on main thread */
.createFromAsset(DATABASE_NAME)
.addMigrations(migration1_2)
.build();
}
return instance;
}
static Migration migration1_2 = new Migration(1, 2) {
#Override
public void migrate(#NonNull SupportSQLiteDatabase database) {
/* Copy the asset into the database folder (with different name) */
File assetDBFile = getNewAssetDatabase(currentContext,DATABASE_NAME);
/* Open the assetdatabase */
SQLiteDatabase assetDB = SQLiteDatabase.openDatabase(assetDBFile.getPath(),null,SQLiteDatabase.OPEN_READWRITE);
/* Build (create and populate) the new ROOM tables and indexes from the asset database */
buildNewTables(
new String[]{
NewEntity1.TABLE_NAME,
NewEntity2.TABLE_NAME,
NewEntity3.TABLE_NAME},
database /* ROOM DATABASE */,
assetDB /* The copied and opened asset database as an SQliteDatabase */
);
/* done with the asset database */
assetDB.close();
assetDBFile.delete();
}
};
private static void buildNewTables(String[] tablesToBuild, SupportSQLiteDatabase actualDB, SQLiteDatabase assetDB) {
StringBuilder args = new StringBuilder();
boolean afterFirst = false;
for (String tableName: tablesToBuild) {
if (afterFirst) {
args.append(",");
}
afterFirst = true;
args.append("'").append(tableName).append("'");
}
/* Get SQL for anything related to the table (table, index, trigger) to the tables and build it */
/* !!!!WARNING!!!! NOT TESTED VIEWS */
/* !!!!WARNING!!!! may not cope with Foreign keys as conflicts could occur */
Cursor csr = assetDB.query(
"sqlite_master",
new String[]{"name","sql", "CASE WHEN type = 'table' THEN 1 WHEN type = 'index' THEN 3 ELSE 2 END AS sort"},
"tbl_name IN (" + args.toString() + ")",
null,
null,null, "sort"
);
while (csr.moveToNext()) {
Log.d("CREATEINFO","executing SQL:- " + csr.getString(csr.getColumnIndex("sql")));
actualDB.execSQL(csr.getString(csr.getColumnIndex("sql")));
}
/* Populate the tables */
/* !!!!WARNING!!!! may not cope with Foreign keys as conflicts could occur */
/* no set order for the tables so a child table may not be loaded before it's parent(s) */
ContentValues cv = new ContentValues();
for (String tableName: tablesToBuild) {
csr = assetDB.query(tableName,null,null,null,null,null,null);
while (csr.moveToNext()) {
cv.clear();
for (String columnName: csr.getColumnNames()) {
cv.put(columnName,csr.getString(csr.getColumnIndex(columnName)));
actualDB.insert(tableName, OnConflictStrategy.IGNORE,cv);
}
}
}
csr.close();
}
private static File getNewAssetDatabase(Context context, String assetDatabaseFileName) {
String tempDBPrefix = "temp_";
int bufferSize = 1024 * 8;
byte[] buffer = new byte[bufferSize];
File assetDatabase = context.getDatabasePath(tempDBPrefix+DATABASE_NAME);
InputStream assetIn;
OutputStream assetOut;
/* Delete the AssetDatabase (temp DB) if it exists */
if (assetDatabase.exists()) {
assetDatabase.delete(); /* should not exist but just in case */
}
/* Just in case the databases folder (data/data/packagename/databases)
doesn't exist create it
This should never be the case as Room DB uses it
*/
if (!assetDatabase.getParentFile().exists()) {
assetDatabase.mkdirs();
}
try {
assetIn = context.getAssets().open(assetDatabaseFileName);
assetOut = new FileOutputStream(assetDatabase);
while(assetIn.read(buffer) > 0) {
assetOut.write(buffer);
}
assetOut.flush();
assetOut.close();
assetIn.close();
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("Error retrieving Asset Database from asset " + assetDatabaseFileName);
}
return assetDatabase;
}
}
The code in the Activity is :-
public class MainActivity extends AppCompatActivity {
TheDatabase db;
AllDao dao;
private static final String TAG = "DBINFO";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/* Original */
db = TheDatabase.getInstance(this);
dao = db.getAllDao();
OriginalEntity newOE = new OriginalEntity();
newOE.name = "App User Data";
dao.insert(newOE);
for(OriginalEntity o: dao.getAll()) {
Log.d(TAG+OriginalEntity.TABLE_NAME,"Name is " + o.name + " ID is " + o.id + " - DB Version is " + TheDatabase.DATABASE_VERSION);
}
/* Added for V2 */
for (NewEntity1 n: dao.getAllNewEntity1s()) {
Log.d(TAG+NewEntity1.TABLE_NAME,"Names is " + n.name + " ID is " + n.id + " - DB Version is " + TheDatabase.DATABASE_VERSION);
}
for (NewEntity2 n: dao.getAllNewEntity2s()) {
Log.d(TAG+NewEntity2.TABLE_NAME,"Names is " + n.name + " ID is " + n.id + " - DB Version is " + TheDatabase.DATABASE_VERSION);
}
for (NewEntity3 n: dao.getAllNewEntity3s()) {
Log.d(TAG+NewEntity3.TABLE_NAME,"Names is " + n.name + " ID is " + n.id + " - DB Version is " + TheDatabase.DATABASE_VERSION);
}
}
}
see Version 1 section and comments in the code for V1 run.
The resultant output to the log for the V2 run (initial) is
:-
2021-10-11 13:02:50.939 D/CREATEINFO: executing SQL:- CREATE TABLE `new1` (`new1_id` INTEGER, `new1_name` TEXT, PRIMARY KEY(`new1_id`))
2021-10-11 13:02:50.941 D/CREATEINFO: executing SQL:- CREATE TABLE `new2` (`new2_id` INTEGER, `new2_name` TEXT, PRIMARY KEY(`new2_id`))
2021-10-11 13:02:50.942 D/CREATEINFO: executing SQL:- CREATE TABLE `new3` (`new3_id` INTEGER, `new3_name` TEXT, PRIMARY KEY(`new3_id`))
2021-10-11 13:02:50.942 D/CREATEINFO: executing SQL:- CREATE INDEX `index_new1_new1_name` ON `new1` (`new1_name`)
2021-10-11 13:02:50.943 D/CREATEINFO: executing SQL:- CREATE INDEX `index_new2_new2_name` ON `new2` (`new2_name`)
2021-10-11 13:02:50.944 D/CREATEINFO: executing SQL:- CREATE INDEX `index_new3_new3_name` ON `new3` (`new3_name`)
2021-10-11 13:02:51.006 D/DBINFOoriginal: Name is name1 ID is 1 - DB Version is 2
2021-10-11 13:02:51.006 D/DBINFOoriginal: Name is name2 ID is 2 - DB Version is 2
2021-10-11 13:02:51.006 D/DBINFOoriginal: Name is name3 ID is 3 - DB Version is 2
2021-10-11 13:02:51.006 D/DBINFOoriginal: Name is name4 ID is 4 - DB Version is 2
2021-10-11 13:02:51.006 D/DBINFOoriginal: Name is name5 ID is 5 - DB Version is 2
2021-10-11 13:02:51.006 D/DBINFOoriginal: Name is App User Data ID is 6 - DB Version is 2
2021-10-11 13:02:51.006 D/DBINFOoriginal: Name is App User Data ID is 7 - DB Version is 2
2021-10-11 13:02:51.010 D/DBINFOnew1: Names is new1_name1 ID is 1 - DB Version is 2
2021-10-11 13:02:51.010 D/DBINFOnew1: Names is new1_name2 ID is 2 - DB Version is 2
2021-10-11 13:02:51.012 D/DBINFOnew2: Names is new2_name1 ID is 1 - DB Version is 2
2021-10-11 13:02:51.012 D/DBINFOnew2: Names is new2_name2 ID is 2 - DB Version is 2
2021-10-11 13:02:51.013 D/DBINFOnew3: Names is new3_name1 ID is 1 - DB Version is 2
2021-10-11 13:02:51.013 D/DBINFOnew3: Names is new3_name2 ID is 2 - DB Version is 2
Note that the user data has been retained (1st App User Data ...., 2nd is added when the activity is run).
The Dao (AllDao) is :-
#Dao
abstract class AllDao {
/* Original Version 1 Dao's */
#Insert(onConflict = OnConflictStrategy.IGNORE)
abstract long insert(OriginalEntity originalEntity);
#Insert(onConflict = OnConflictStrategy.IGNORE)
abstract long[] insert(OriginalEntity ... originalEntities);
#Query("SELECT * FROM original")
abstract List<OriginalEntity> getAll();
/* New Version 2 Dao's */
#Insert(onConflict = OnConflictStrategy.IGNORE)
abstract long insert(NewEntity1 newEntity1);
#Insert(onConflict = OnConflictStrategy.IGNORE)
abstract long insert(NewEntity2 newEntity2);
#Insert(onConflict = OnConflictStrategy.IGNORE)
abstract long insert(NewEntity3 newEntity3);
#Query("SELECT * FROM " + NewEntity1.TABLE_NAME)
abstract List<NewEntity1> getAllNewEntity1s();
#Query("SELECT * FROM " + NewEntity2.TABLE_NAME)
abstract List<NewEntity2> getAllNewEntity2s();
#Query("SELECT * FROM " + NewEntity3.TABLE_NAME)
abstract List<NewEntity3> getAllNewEntity3s();
}
The Entities are :-
#Entity(tableName = OriginalEntity.TABLE_NAME)
class OriginalEntity {
public static final String TABLE_NAME = "original";
public static final String COL_ID = TABLE_NAME +"_id";
public static final String COL_NAME = TABLE_NAME + "_name";
#PrimaryKey
#ColumnInfo(name = COL_ID)
Long id = null;
#ColumnInfo(name = COL_NAME, index = true)
String name;
}
and for V2 :-
#Entity(tableName = NewEntity1.TABLE_NAME)
class NewEntity1 {
public static final String TABLE_NAME = "new1";
public static final String COl_ID = TABLE_NAME + "_id";
public static final String COL_NAME = TABLE_NAME + "_name";
#PrimaryKey
#ColumnInfo(name = COl_ID)
Long id = null;
#ColumnInfo(name = COL_NAME, index = true)
String name;
}
and :-
#Entity(tableName = NewEntity2.TABLE_NAME)
class NewEntity2 {
public static final String TABLE_NAME = "new2";
public static final String COl_ID = TABLE_NAME + "_id";
public static final String COL_NAME = TABLE_NAME + "_name";
#PrimaryKey
#ColumnInfo(name = COl_ID)
Long id = null;
#ColumnInfo(name = COL_NAME, index = true)
String name;
}
and :-
#Entity(tableName = NewEntity3.TABLE_NAME)
class NewEntity3 {
public static final String TABLE_NAME = "new3";
public static final String COl_ID = TABLE_NAME + "_id";
public static final String COL_NAME = TABLE_NAME + "_name";
#PrimaryKey
#ColumnInfo(name = COl_ID)
Long id = null;
#ColumnInfo(name = COL_NAME, index = true)
String name;
}
Finally Test new App install (i.e. no Migration but created from asset)
When run the output to the log is (no user supplied/input data) :-
2021-10-11 13:42:48.272 D/DBINFOoriginal: Name is name1 ID is 1 - DB Version is 2
2021-10-11 13:42:48.272 D/DBINFOoriginal: Name is name2 ID is 2 - DB Version is 2
2021-10-11 13:42:48.272 D/DBINFOoriginal: Name is name3 ID is 3 - DB Version is 2
2021-10-11 13:42:48.272 D/DBINFOoriginal: Name is name4 ID is 4 - DB Version is 2
2021-10-11 13:42:48.272 D/DBINFOoriginal: Name is name5 ID is 5 - DB Version is 2
2021-10-11 13:42:48.272 D/DBINFOoriginal: Name is App User Data ID is 6 - DB Version is 2
2021-10-11 13:42:48.275 D/DBINFOnew1: Names is new1_name1 ID is 1 - DB Version is 2
2021-10-11 13:42:48.275 D/DBINFOnew1: Names is new1_name2 ID is 2 - DB Version is 2
2021-10-11 13:42:48.276 D/DBINFOnew2: Names is new2_name1 ID is 1 - DB Version is 2
2021-10-11 13:42:48.276 D/DBINFOnew2: Names is new2_name2 ID is 2 - DB Version is 2
2021-10-11 13:42:48.277 D/DBINFOnew3: Names is new3_name1 ID is 1 - DB Version is 2
2021-10-11 13:42:48.277 D/DBINFOnew3: Names is new3_name2 ID is 2 - DB Version is 2
Note
Room encloses names of items (tables, columns) in grave accents's, This makes invalid column names valid e.g 1 not enclosed is invalid 1 enclosed is valid. Use of otherwise invalid names may, although I suspect not, cause issues (I haven't tested this aspect). SQLite itself strips the grave accents when storing the name e.g :-
CREATE TABLE IF NOT EXISTS `testit` (`1`);
SELECT * FROM sqlite_master WHERE name = 'testit';
SELECT * FROM testit;
results in :-
i.e. the grave accents are kept when the SQL is stored, hence the generated CREATE's are safe.
and :-
i.e. the grave accents have been stripped and the column is named just 1, which may cause an issue (but likely not) when traversing the columns in the cursor.
I have one db table class where I did change related to indices. Previously indices was like :
#Entity(indices = {#Index(value = {"jobNumber", "jobId"},
unique = true)})
But I changed it to
#Entity(indices = {
#Index(value = "jobNumber", unique = true), #Index(value = "jobId", unique = true)
})
But when I tried migration it's giving me issue like :
caused by: java.lang.IllegalStateException: Migration didn't properly handle Job
I need to go with second format only. I tried to add migration but seems not working. Here is my code :
public static final Migration MIGRATION_4_5 = new Migration(4, 5) {
#Override
public void migrate(SupportSQLiteDatabase database) {
migrateJobTableForIndices(database);
}
};
private static void migrateJobTableForIndices(SupportSQLiteDatabase database) {
//create new table
database.execSQL(
"CREATE TABLE Job_new (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, jobId INTEGER NOT NULL, " +
"jobNumber TEXT)");
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS index_Job_new_jobNumber_jobId ON Job_new (jobNumber, jobId)");
// Copy the data
database.execSQL(
"INSERT INTO Job_new (id, jobId, jobNumber) " +
"SELECT id, jobId, jobNumber " +
"FROM Job");
// Remove the old table
database.execSQL("DROP TABLE Job");
// Change the table name to the correct one
database.execSQL("ALTER TABLE Job_new RENAME TO Job");
}
Is there any way to add migration for updated indices format. Any reference is really appreciated
As Room generates index names. You could export the scheme to see what is happening. By adding the following to your build.gradle:
android {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
...
}
Now for the migration.
Beforehand you have to turn the foreign_keys off:
database.execSQL("PRAGMA foreign_keys=OFF;")
You have to drop the existing index. For example:
database.execSQL("DROP INDEX IF EXISTS `index_Job_jobNumber_jobId`")
Then I would suggest renaming the old table.
ALTER TABLE Job RENAME TO Job_old
Create the new table Job.
Then create the index, which you will find in the exported schema location. In this example, it is in your $projectDir/schemas. From the files there you could see and copy how Room creates the indexes and add the creation of these indexes after the table creation.
Migrate the data.
Drop the old table.
Turn the foreign_keys on:
database.execSQL("PRAGMA foreign_keys=ON;")
Hi in my senario there is a table with 4 columns and im trying to create another table with connection to the first table but i dont whay im getting this error in logcat
2019-10-28 01:04:00.853 29812-29812/com.test.fastfoodfinder E/SQLiteDatabase: Error inserting notes_main=testeststststststststs
android.database.sqlite.SQLiteException: no such table: notes (code 1 SQLITE_ERROR): , while compiling: INSERT INTO notes(notes_main) VALUES (?)
so i have created a class for my data base and this is what i have done
public class RestaurantDBHelper extends SQLiteOpenHelper {
private final static String DATABASE_NAME = "FastFood_DataBase.db";
private final static int DATABASE_VERSION = 1;
private final static String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME +
"(" + COLUMN_RESTAURANT_ID + " INTEGER PRIMARY KEY ," +
COLUMN_RESTAURANT_NAME + " TEXT, " +
COLUMN_RESTAURANT_ADDRESS + " TEXT, " +
COLUMN_RESTAURANT_TYPE + " INTEGER, " +
COLUMN_RESTAURANT_IMAGE + " INTEGER);";
private final static String CREATE_TABLE_NOTES = "CREATE TABLE " + TABLE_NAME_NOTES +
"(" + COLUMN_NOTES_ID + " INTEGER PRIMARY KEY, "
+ COLUMN_NOTES + " TEXT," + "FOREIGN KEY (" + COLUMN_NOTES_ID + ") REFERENCES " + TABLE_NAME +"(restaurant_id) ON DELETE CASCADE)";
public final static String DELETE_TABLE = "DROP TABLE IF EXISTS " + TABLE_NAME;
public RestaurantDBHelper(#Nullable Context context){
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
#Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("PRAGMA FOREIGN_KEYS = ON;");
db.execSQL(CREATE_TABLE);
db.execSQL(CREATE_TABLE_NOTES);
}
#Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL(DELETE_TABLE);
onCreate(db);
}
public void addRestaurant(Restaurant restaurant) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COLUMN_RESTAURANT_NAME, restaurant.getName());
values.put(COLUMN_RESTAURANT_ADDRESS, restaurant.getAddress());
values.put(COLUMN_RESTAURANT_TYPE,restaurant.getType());
values.put(COLUMN_RESTAURANT_IMAGE, restaurant.getType());
db.insert(TABLE_NAME, null, values);
db.close();
}
public void addNotes (Restaurant restaurant) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COLUMN_NOTES,restaurant.getNote());
db.insert(TABLE_NAME_NOTES,null,values);
db.close();
}
and
public class RestaurantContract {
public static class EntryRestaurants {
public final static String TABLE_NAME = "restaurants";
public final static String COLUMN_RESTAURANT_ID = "restaurant_id";
public final static String COLUMN_RESTAURANT_NAME = "restaurant_name";
public final static String COLUMN_RESTAURANT_ADDRESS = "restaurant_address";
public final static String COLUMN_RESTAURANT_TYPE = "restaurant_type";
public final static String COLUMN_RESTAURANT_IMAGE = "restaurant_image_type";
public final static String COLUMN_RESTAURANT_NOTE_ID = "note_id";
public final static String TABLE_NAME_NOTES = "notes";
public final static String COLUMN_NOTES_ID = "notes_id";
public final static String COLUMN_NOTES = "notes_main";
public final static int RESTAURANT_TYPE_DELIVERY = 1;
public final static int RESTAURANT_TYPE_SITDOWN = 2;
public final static int RESTAURANT_TYPE_TAKEAWAY = 3;
}
}
im kind a new in android so any help would be appreciated,thanks
I believe that your issue is with the onCreate method. This ONLY runs when the database is created, it does not run every time the App is run.
The easiest solution, assuming that you do not need to keep any existing data, is to either delete the App's data or to uninstall the App. After doing either rerun the App and the new table will be created as the onCreate method will then run.
Furthermore it is no use turning FOREIGN KEYS on in the onCreate method. FOREIGN KEYS need to be turned on every time the App is run. To fix this, override the onConfigure method and then use db.setForeignKeyConstraintsEnabled(true);
this is just a convenient alternative to using db.execSQL("PRAGMA FOREIGN_KEYS = ON;");, so if you prefer you could use this when overriding the onConfigure method.
e.g. add this method to the RestaurantDBHelper class :-
#Override
public void onConfigure(SQLiteDatabase db) {
super.onConfigure(db);
db.setForeignKeyConstraintsEnabled(true);
}
However, you will then have issues when trying to add notes as the child will be set to null and thus their will not be a link/map/association/reference between the added note and the restaurant.
You need to use something like :-
public long addNote(String note, long restaurantId) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COLUMN_NOTES,note);
values.put(COLUMN_NOTES_ID,restaurantId);
return db.insert(TABLE_NAME_NOTES,null,values);
}
BUT then you may then have an issue as to determining the id of the restaurant.
BUT then you may then encounter a further issue in that you could only have one note per restaurant as the column used to reference the restaurant is defined as INTEGER PRIMARY KEY and is therefore is a UNIQUE column (the same value can only be used once (an exception is null as a null is considered to be unique to another null)).
If the requirement is for one note per restaurant then there is no need for the separate table the relationship is a one to one relationship so the value can be stored in the restaurant table.
If you want a restaurant to have multiple notes (one to many relationship) then you should not make the column INTEGER PRIMARY KEY, INTEGER would suffice. Then a number of notes could reference the same restaurant.
If you wanted a note to be able to be be applied to a number of restaurants then you'd use a third mapping/line/reference/associative table (other name probably also exist). Such a table would have two columns one to reference the restaurant and the other to reference the note. You would then have a many to many relationship between restaurants and notes (a note could be used by many restaurants and a restaurant could use many notes).
You may find The 3 Types of Relationships in Database Design helpful.
You have enabled foreign key constraints and thus must have a unique primary key for the foreign key to reference.
https://sqlite.org/foreignkeys.html#fk_indexes
says
If the database schema contains foreign key errors that require looking at more than one table definition to identify, then those errors are not detected when the tables are created. Instead, such errors prevent the application from preparing SQL statements that modify the content of the child or parent tables in ways that use the foreign keys. Errors reported when content is changed are "DML errors" and errors reported when the schema is changed are "DDL errors". So, in other words, misconfigured foreign key constraints that require looking at both the child and parent are DML errors. The English language error message for foreign key DML errors is usually "foreign key mismatch" but can also be "no such table" if the parent table does not exist. Foreign key DML errors are reported if:
The parent table does not exist, or
The parent key columns named in the foreign key constraint do not exist, or
The parent key columns named in the foreign key constraint are not the primary key of the parent table and are not subject to a unique constraint using collating sequence specified in the CREATE TABLE, or
The child table references the primary key of the parent without specifying the primary key columns and the number of primary key columns in the parent do not match the number of child key columns.
Given that you never give the restaurant_id a value when you inserting a restaurant then the primary key of the restaurants is probably always null and thus not unique
(Yes according to https://www.sqlitetutorial.net/sqlite-primary-key/ to make the current version of SQLite compatible with the earlier version, SQLite allows the primary key column to contain NULL values. )
So I would say the solution is to create restaurant entries with a unique primary key value when you insert a restaurant or get the database to generate a unique value creating the the restaurant table with the line:-
private final static String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME +
"(" + COLUMN_RESTAURANT_ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
....
One way to confirm this is download FastFood_DataBase.* files using Device Explorer in your app's database directory and then open it up in https://sqlitebrowser.org/ on your computer to confirm the contents.
I tried to add a trigger to limit the number of rows in my Room DB,
but the trigger is not getting created- as I see in the DB and in the generated JSON file that automatically created.
Also, migration code is not getting reached as well.
My code for creating the DB is:
#Database(entities ={Sensor.class, Meter.class, MeterHistory.class, SensorHistory.class,
ConnectivityHistory.class, GraphTypesHistory.class,
SensorSettingsHistory.class, UserInterfaceSettings.class, Temperature.class, LogSettingsHistory.class, BatteryUsage.class,
FunctionsHistory.class, MathChannelData.class}, version = 5)
#TypeConverters(UserUsageManager.DateConverter.class)
public abstract class UserUsageDB extends RoomDatabase {
public abstract UserUsageDao userUsageDao();
private static volatile UserUsageDB userUsageDBInstance;//singleton
static UserUsageDB getDatabase(final Context context) {
if (userUsageDBInstance == null) {
synchronized (UserUsageDB.class) {
if (userUsageDBInstance == null) {
userUsageDBInstance = Room.databaseBuilder(context.getApplicationContext(),
UserUsageDB.class, "user_usage_database")
.addCallback(LIMIT_CALLBACK)
.addMigrations(MIGRATION_1_2)
.fallbackToDestructiveMigration()
.build();
}
}
}
return userUsageDBInstance;
}
and the code of the migration and trigger callback:
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
#Override
public void migrate(SupportSQLiteDatabase database) {
// Create the new meters table
database.execSQL("CREATE TABLE meters_new ( id TEXT NOT NULL, name TEXT NOT NULL, PRIMARY KEY(id))");
// Copy the data
database.execSQL("INSERT INTO meters_new ( id, name) SELECT id, name FROM meters");
// Remove the old table
database.execSQL("DROP TABLE meters");
// Change the table name to the correct one
database.execSQL("ALTER TABLE meters_new RENAME TO meters");
// Create the new sensors table
database.execSQL("CREATE TABLE sensors_new (id TEXT NOT NULL, name TEXT NOT NULL, type TEXT NOT NULL, PRIMARY KEY(id))");
// Copy the data
database.execSQL("INSERT INTO sensors_new ( id, name) SELECT id, name FROM sensors");
// Remove the old table
database.execSQL("DROP TABLE sensors");
// Change the table name to the correct one
database.execSQL("ALTER TABLE sensors_new RENAME TO sensors");
}
};
//create a trigger to limit row count of tables
static final RoomDatabase.Callback LIMIT_CALLBACK = new RoomDatabase.Callback(){
#Override
public void onCreate(#NonNull SupportSQLiteDatabase db) {
super.onCreate(db);
db.execSQL("Create Trigger IF NOT EXISTS Trigr_Limit_battery_usage_Delete \n" +
"AFTER Insert On battery_usage \n" +
" BEGIN \n"+
" DELETE from battery_usage\n"+
" where ts in (select ts from battery_usage order by date limit -1 offset 10);"+
" END");
db.execSQL("Create Trigger IF NOT EXISTS Trigr_Limit_sensor_settings_history_Delete \n" +
"AFTER Insert On sensor_settings_history \n" +
" BEGIN \n"+
" DELETE from sensor_settings_history\n"+
" where ts in (select ts from sensor_settings_history order by date limit -1 offset 10);"+
" END");
}
#Override
public void onOpen(#NonNull SupportSQLiteDatabase db) {
super.onOpen(db);
}
};
Can anyone help me out?
Thanks!!
but the trigger is not getting created
You are Overriding the onCreate method, this only gets called when the database is actually created (first time the App is installed). You want to Override the onOpen method to add the Triggers.
migration code is not getting reached as well.
Migration is from version 1 to 2 as per (new Migration(1, 2)), you have version 5 coded in the App as per version = 5 . Your migration will only be called when the version stored in the database is 1 and the version coded in the App is 2. You may find Understanding migrations with Room useful. Or perhaps Migrating Room databases
I've been trying to migrate my app to Room. I'm struggling with a particular table that can't be migrated directly because of the way it has been created.
The fields were created with datatype BOOL and BYTE instead of INTEGER.
I've already failed trying:
Change my entity fields to Int/Boolean/Byte with the same error
Creating a TypeConverter to save it as Boolean/Byte
Adding typeAffinity as UNDEFINED in #ColumnInfo of my entity that is affinity = 1
My databaseSQL creation sentence:
CREATE TABLE IF NOT EXISTS myTable (_id INTEGER PRIMARY KEY AUTOINCREMENT,
my_first_field BOOL NOT NULL DEFAULT 0,
my_second_field BYTE NOT NULL DEFAULT 0)
My Entity:
#Entity(tableName = "myTable")
data class MyTable(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "_id")
var id: Int,
#ColumnInfo(name = "my_first_field")
var myFirstField: Boolean = false,
#ColumnInfo(name = "my_second_field")
var mySecondField: Byte = false
)
The error I'm constantly getting is:
Expected:
TableInfo{name='my_table', columns={_id=Column{name='_id', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=1}, my_first_field=Column{name='my_first_field', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0}, my_second_field=Column{name='my_second_field', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0}}, foreignKeys=[], indices=[]}
Found:
TableInfo{name='my_table', columns={_id=Column{name='_id', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=1}, my_first_field=Column{name='my_first_field', type='BOOL', affinity='1', notNull=true, primaryKeyPosition=0}, my_second_field=Column{name='my_second_field', type='BYTE', affinity='1', notNull=true, primaryKeyPosition=0}}, foreignKeys=[], indices=[]}
Is there any way to make straight forward without creating a migration strategy?
I believe you could, before building the room database:-
Check to see if anything needs to be done e.g. by using :-
SELECT count() FROM sqlite_master WHERE name = 'myTable' AND instr(sql,' BOOL ') AND instr(sql,' BYTE ');
and then checking the result.
If it is 0 do nothing else (although to be safe you could only use DROP TABLE IF EXISTS oldmyTable when it is 0).
ONLY If the above returns 1 then :-
drop the renamed original table (see below and also above) just in case it exists :-
DROP TABLE IF EXISTS oldmyTable;
define another table using
CREATE TABLE IF NOT EXISTS myOtherTable (_id INTEGER PRIMARY KEY AUTOINCREMENT,
my_first_field INTEGER NOT NULL DEFAULT 0,
my_second_field INTEGER NOT NULL DEFAULT 0)
i.e. the expected schema
populate the new table using
INSERT INTO myOtherTable SELECT * FROM myTable;
rename mytable using :-
ALTER TABLE mytable RENAME TO oldmyTable;
rename myOtherTable using the original name :-
ALTER TABLE myOtherTable RENAME TO mytable;
drop the renamed original table (obviously only when tested) :-
DROP TABLE IF EXISTS oldmyTable;
You may wish to omit this until you are sure that the migration has worked.
The net result is that the table should be as is expected.
With regards to the comment :-
Problem is that I have like 16-20 tables to migrate.
The you could use something like :-
public static int preMigrateAdjustment(SQLiteDatabase mDB) {
String original_rename_prefix = "old";
String tempname_suffix = "temp";
String newsql_column = "newsql";
String[] columns = new String[]{
"name",
"replace(replace(sql,' BOOL ',' INTEGER '),' BYTE ',' INTEGER ') AS " + newsql_column
};
int count_done = 0;
String whereclause = "name LIKE('" +
original_rename_prefix +
"%') AND type = 'table'";
Cursor csr = mDB.query("sqlite_master",null,whereclause,null,null,null,null);
while (csr.moveToNext()) {
mDB.execSQL("DROP TABLE IF EXISTS " + csr.getString(csr.getColumnIndex("name")));
}
whereclause = "type = 'table' AND (instr(sql,' BOOL ') OR instr(sql,' BYTE '))";
csr = mDB.query(
"sqlite_master",
columns,
whereclause,
null,null,null,null
);
while (csr.moveToNext()) {
String base_table_name = csr.getString(csr.getColumnIndex("name"));
String newsql = csr.getString(csr.getColumnIndex(newsql_column));
String temp_table_name = base_table_name + tempname_suffix;
String renamed_table_name = original_rename_prefix+base_table_name;
mDB.execSQL(newsql.replace(base_table_name,temp_table_name));
mDB.execSQL("INSERT INTO " + temp_table_name + " SELECT * FROM " + base_table_name);
mDB.execSQL("ALTER TABLE " + base_table_name + " RENAME TO " + renamed_table_name);
mDB.execSQL("ALTER TABLE " + temp_table_name + " RENAME TO " + base_table_name);
count_done++;
}
whereclause = "name LIKE('" +
original_rename_prefix +
"%') AND type = 'table'";
csr = mDB.query("sqlite_master",null,whereclause,null,null,null,null);
while (csr.moveToNext()) {
mDB.execSQL("DROP TABLE IF EXISTS " + csr.getString(csr.getColumnIndex("name")));
}
csr.close();
return count_done;
}
Note that this isn't fool proof e.g. if you happened to have tables that already start with old, then these would be dropped.
The above assumes a second run to actually drop the renamed original tables.
Additional
Looking into this and actually testing (in this case using 5 tables) with identical schema after resolving the BOOL BYTE types an additional issue comes to light in that coding
_id INTEGER PRIMARY KEY AUTOINCREMENT
results in notNull = false, whilst coding
#PrimaryKey(autoGenerate = true)
private long _id;
results in notNull=true
As such as quick fix that assumes that AUTOINCREMENT NOT NULL isn't coded the line in the preMigrateAdjustment has been changed from :-
mDB.execSQL((newsql.replace(base_table_name,temp_table_name)));
to :-
mDB.execSQL((newsql.replace(base_table_name,temp_table_name)).replace("AUTOINCREMENT","AUTOINCREMENT NOT NULL"));
Working Demo
Creating and Populating the old (pre-room) tables.
Creating and populating the old tables is done within the Database Helper OrginalDBHelper.java :-
public class OriginalDBHelper extends SQLiteOpenHelper {
public static final String DBNAME = "mydb";
public static final int DBVERSION = 1;
int tables_to_create = 5; //<<<<<<<<<< 5 sets of tables
SQLiteDatabase mDB;
public OriginalDBHelper(Context context) {
super(context, DBNAME, null, DBVERSION);
mDB = this.getWritableDatabase();
}
#Override
public void onCreate(SQLiteDatabase db) {
for (int i=0;i < tables_to_create;i++) {
db.execSQL("CREATE TABLE IF NOT EXISTS myTable" + String.valueOf(i) + "X (_id INTEGER PRIMARY KEY AUTOINCREMENT,\n" +
" my_first_field BOOL NOT NULL DEFAULT 0,\n" +
" my_second_field BYTE NOT NULL DEFAULT 0)"
);
db.execSQL("INSERT INTO myTable" + String.valueOf(i) + "X (my_first_field,my_second_field) VALUES(0,0),(1,0),(1,1),(0,1)");
}
}
#Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
The pre-migration conversion of the tables
i.e. adjust the schema to suit room) PreMigrationAdjustment.java
public class PreMigrationAdjustment {
public static int preMigrateAdjustment(SQLiteDatabase mDB) {
String original_rename_prefix = "old";
String tempname_suffix = "temp";
String newsql_column = "newsql";
String[] columns = new String[]{
"name",
"replace(replace(sql,' BOOL ',' INTEGER '),' BYTE ',' INTEGER ') AS " + newsql_column
};
int count_done = 0;
String whereclause = "name LIKE('" +
original_rename_prefix +
"%') AND type = 'table'";
Cursor csr = mDB.query("sqlite_master",null,whereclause,null,null,null,null);
while (csr.moveToNext()) {
mDB.execSQL("DROP TABLE IF EXISTS " + csr.getString(csr.getColumnIndex("name")));
}
whereclause = "type = 'table' AND (instr(sql,' BOOL ') OR instr(sql,' BYTE '))";
csr = mDB.query(
"sqlite_master",
columns,
whereclause,
null,null,null,null
);
while (csr.moveToNext()) {
String base_table_name = csr.getString(csr.getColumnIndex("name"));
String newsql = csr.getString(csr.getColumnIndex(newsql_column));
String temp_table_name = base_table_name + tempname_suffix;
String renamed_table_name = original_rename_prefix+base_table_name;
mDB.execSQL((newsql.replace(base_table_name,temp_table_name)).replace("AUTOINCREMENT","AUTOINCREMENT NOT NULL"));
//mDB.execSQL((newsql.replace(base_table_name,temp_table_name)));
mDB.execSQL("INSERT INTO " + temp_table_name + " SELECT * FROM " + base_table_name);
mDB.execSQL("ALTER TABLE " + base_table_name + " RENAME TO " + renamed_table_name);
mDB.execSQL("ALTER TABLE " + temp_table_name + " RENAME TO " + base_table_name);
count_done++;
}
whereclause = "name LIKE('" +
original_rename_prefix +
"%') AND type = 'table'";
csr = mDB.query("sqlite_master",null,whereclause,null,null,null,null);
while (csr.moveToNext()) {
mDB.execSQL("DROP TABLE IF EXISTS " + csr.getString(csr.getColumnIndex("name")));
}
csr.close();
return count_done;
}
}
WARNING this is too simple to be used without consideration of it's flaws and is for demonstration only.
The Entities for room
only 1 of the 5 shown for brevity i.e. myTable0X.java
Obviously these have to be carefully written to match the pre-room tables.
#Entity()
public class myTable0X {
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "_id")
private long id;
#ColumnInfo(name = "my_first_field")
private boolean my_first_field;
#ColumnInfo(name = "my_second_field")
private boolean my_second_field;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public boolean isMy_first_field() {
return my_first_field;
}
public void setMy_first_field(boolean my_first_field) {
this.my_first_field = my_first_field;
}
public boolean isMy_second_field() {
return my_second_field;
}
public void setMy_second_field(boolean my_second_field) {
this.my_second_field = my_second_field;
}
}
A single DAO interface DAOmyTablex.java
#Dao
public interface DAOmyTablex {
#Query("SELECT * FROM myTable0X")
List<myTable0X> getAllFrommyTable0();
#Query("SELECT * FROM myTable1X")
List<myTable1X> getAllFrommyTable1();
#Query("SELECT * FROM myTable2X")
List<myTable2X> getAllFrommyTable2();
#Query("SELECT * FROM myTable3X")
List<myTable3X> getAllFrommyTable3();
#Query("SELECT * FROM myTable4X")
List<myTable4X> getAllFrommyTable4();
#Insert
long[] insertAll(myTable0X... myTable0XES);
#Insert
long[] insertAll(myTable1X... myTable1XES);
#Insert
long[] insertAll(myTable2X... myTable2XES);
#Insert
long[] insertAll(myTable3X... myTable3XES);
#Insert
long[] insertAll(myTable4X... myTable4XES);
#Delete
int delete(myTable0X mytable0X);
#Delete
int delete(myTable1X mytable1X);
#Delete
int delete(myTable2X mytable2X);
#Delete
int delete(myTable3X mytable3X);
#Delete
int delete(myTable4X mytable4X);
}
The Database mydb.java
#Database(entities = {myTable0X.class, myTable1X.class, myTable2X.class, myTable3X.class, myTable4X.class},version = 2)
public abstract class mydb extends RoomDatabase {
public abstract DAOmyTablex dbDAO();
}
note that all 5 Entities have been utilised.
note that as the current database version is 1, room requires the version number to be increased hence version = 2
Putting it all together MainActivity.java
This consists of 3 core Stages
Building the pre-room database.
Converting the tables to suit room.
Opening (handing over) the database via room.
When the app starts it will automatically do stages 1 and 2 a button has been added that when clicked will then undertake stage 3 (just the once).
Finally, data is extracted from the tables (this actually opens the Room database)
and data from one of the tables is output to the log.
public class MainActivity extends AppCompatActivity {
OriginalDBHelper mDBHlpr;
Button mGo;
mydb mMyDB;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mGo = this.findViewById(R.id.go);
mGo.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
goForIt();
}
});
mDBHlpr = new OriginalDBHelper(this);
Log.d("STAGE1","The original tables");
dumpAllTables();
Log.d("STAGE2", "Initiaing pre-mirgration run.");
Log.d("STAGE2 A RESULT",
String.valueOf(
PreMigrationAdjustment.preMigrateAdjustment(mDBHlpr.getWritableDatabase()
)
) + " tables converted."
); //<<<<<<<<<< CONVERT THE TABLES
Log.d("STAGE2 B","Dumping adjusted tables");
dumpAllTables();
Log.d("STAGE2 C","Second run Cleanup");
Log.d("STAGE2 DRESULT",
String.valueOf(
PreMigrationAdjustment.preMigrateAdjustment(mDBHlpr.getWritableDatabase()
)
) + " tables converted."
); //<<<<<<<<<< CONVERT THE TABLES
dumpAllTables();
Log.d("STAGE3","Handing over to ROOM (when button is clicked)");
}
private void goForIt() {
if (mMyDB != null) return;
mMyDB = Room.databaseBuilder(this,mydb.class,OriginalDBHelper.DBNAME).addMigrations(MIGRATION_1_2).allowMainThreadQueries().build();
List<myTable0X> mt0 = mMyDB.dbDAO().getAllFrommyTable0();
List<myTable1X> mt1 = mMyDB.dbDAO().getAllFrommyTable1();
List<myTable2X> mt2 = mMyDB.dbDAO().getAllFrommyTable2();
List<myTable3X> mt3 = mMyDB.dbDAO().getAllFrommyTable3();
List<myTable4X> mt4 = mMyDB.dbDAO().getAllFrommyTable4();
for (myTable0X mt: mt0) {
Log.d("THIS_MT","ID is " + String.valueOf(mt.getId()) + " FIELD1 is " + String.valueOf(mt.isMy_first_field()) + " FIELD2 is " + String.valueOf(mt.isMy_second_field()));
}
// etc.......
}
private void dumpAllTables() {
SQLiteDatabase db = mDBHlpr.getWritableDatabase();
Cursor c1 = db.query("sqlite_master",null,"type = 'table'",null,null,null,null);
while (c1.moveToNext()) {
Log.d("TABLEINFO","Dmuping Data for Table " + c1.getString(c1.getColumnIndex("name")));
Cursor c2 = db.query(c1.getString(c1.getColumnIndex("name")),null,null,null,null,null,null);
DatabaseUtils.dumpCursor(c2);
c2.close();
}
c1.close();
}
public final Migration MIGRATION_1_2 = new Migration(1, 2) {
#Override
public void migrate(SupportSQLiteDatabase database) {
/**NOTES
//Tried the pre-migration here BUT SQLiteDatabaseLockedException: database is locked (code 5 SQLITE_BUSY)
//Cannot use SupportSQLiteDatabase as that locks out access to sqlite_master
//PreMigrationAdjustment.preMigrateAdjustment(mDBHlpr.getWritableDatabase()); //Initial run
//PreMigrationAdjustment.preMigrateAdjustment(mDBHlpr.getWritableDatabase()); //Cleanup run
*/
}
};
}
As room will consider a migration underway a Migration object has the migration method overridden by a method that does nothing.
As per the comments attempts were made to utilise the migration, the issue is that the database is locked by room and that the the SupportSQliteDatabase passed to the migration method doesn't allow access to sqlite_master.
Result
The result (just the STAGE???? output) is :-
2019-05-19 13:18:12.227 D/STAGE1: The original tables
2019-05-19 13:18:12.244 D/STAGE2: Initiaing pre-mirgration run.
2019-05-19 13:18:12.281 D/STAGE2 A RESULT: 5 tables converted.
2019-05-19 13:18:12.281 D/STAGE2 B: Dumping adjusted tables
2019-05-19 13:18:12.303 D/STAGE2 C: Second run Cleanup
2019-05-19 13:18:12.304 D/STAGE2 DRESULT: 0 tables converted.
2019-05-19 13:18:12.331 D/STAGE3: Handing over to ROOM (when button is clicked)
The finals rows being :-
2019-05-19 13:20:03.090 D/THIS_MT: ID is 1 FIELD1 is false FIELD2 is false
2019-05-19 13:20:03.090 D/THIS_MT: ID is 2 FIELD1 is true FIELD2 is false
2019-05-19 13:20:03.090 D/THIS_MT: ID is 3 FIELD1 is true FIELD2 is true
2019-05-19 13:20:03.090 D/THIS_MT: ID is 4 FIELD1 is false FIELD2 is true