RoomExistingSQLiteDBConverter - CodeProject
RoomExistingSQLiteDBConverter - CodeProject
TheOldFogie
Andorid's ROOM expects the SQLite Database schema to adhere to it's rules. It is the @Entity's that define the expected
schema.
As such converting the database is relatively simple in comparison to generating the correct @Entity's, especially if the
underlying rules are unknown; and hence why the App can be useful.
Android's ROOM does now provide a conversion utility, but it only converts the database, the task of creating the exact
@Entity's, is still required.
Note, the generated code lacks the import statements (this is intentional, as they can vary). As such every generated file should
be visited to resolve the imports (and if no package has been supplied the package statement as well).
4. A single @Database file that includes the entities definition (i.e. includes the @Entity classes) and abstact methods for the
invocation of the @Dao's.
Note, when first run, permission will be requested. If not provided no Database will be listed.
The REFRESH button will refresh the list (e.g. after adding a database).
The RoomDBConverterDBConversions directory is where the data from the conversion (the databases and the java code)
is placed.
4. Orange is more informational. (e.g. type affinity VARCHAR(100) will be changed to TEXT (it's derived type affinity as per
SQLite)).
5. Red indicates a more prominent change or issue. (e.g. a type affinity of DATETIME is changed to TEXT (Final) as the derived
affinity is NUMERIC which is not supported by Room).
Specifying the sub-directory of the RoomDBConverterDBConversions directory (this cannot be changed as it needs to be
specififc so that converted databases aren't located when retrieving the databases).
All data from a conversion will be placed into a sub-directory of the RoomDBConverterDBConversions. By default
the sub-directory used will be Convert_ suffixed with the database file name.
A package name.
If supplied, this can reduce the work involved of adding the package statements to the @Entity and @Dao code for
each table.
If not suppled then package statements will not be added to the resultant code, they will then have to be added to
each file i.e. the number of tables * 2 (1 per @Entity, 1 per @Dao).
Obviously the package name has to be correct.
Determining and supplying the package name is recommended, as it can be possible to inadvertently utililse the
wrong classes if using ALT + ENTER to resolve imports (import statements are not included in the generated code).
Specifying the mode (whether or not Database component names are enclosed)
Note, the coverter uses the name as is stored in sqlite_master and thus the name will not be enclosed. Using
SafeMode encloses the component names in `,s (grave accents). So invalid table names are valid. Unchecking
SafeMode may result in tables not being able to be created.
Note, the intention is to add a third ultra-safe mode in which case component names will be pre-prefixed (e.g. with
something like rc_).
The specification of the sub-sub-directory for the @Entity and additionaly the @Dao files.
By default java is used for both so @Entity and @Dao files are store together (this allows for a single copy say from
device-explorer).
The @Database file will be placed into the same directory as the @Entity files.
The @Database file is the capitalised Database Name, less the file extension, suffixed with Database and
.java; e.g. if the database name is RoomAssetConversion.db then the @Database file will
RoomAssetConversionDatabase.java
Conversion
If permisssion is not granted then no files/database will be found as read access to the directory prevents any being found.
If there are none then the database(s) to be converted should be copied into External Public Storage (typically this is sdcard
aka storage/emulated/0, however it can be device dependant (see https://developer.android.com/training/data-storage/files)
Here you can see that 7 databases have been located in various directories.
As can be seen from the above three additional sections are added:-
Deviations/notifications are :-
1. Indicates a room master table, this will be omitted from the converted database. (RED).
2. ROOM manages the room_master table, it may lead to issues if this table were converted/copied. It is also likely that there is
no neeed to convert such a database.
3. Room requires all entities (tables) to have a primary key. Will generate primary key from all columns. (RED).
4. Indicates a virtual table, this will be omitted from the converted database. (RED).
5. Limited support is provided for VIRTUAL tables, that is only for FTS3 and FTS4. If FTS3 or FTS4.
6.
7. USING supported module ???? (Orange) (where ???? is the module name, as above).
8. The VIRTUAL table will be converted. However, the other FTS tables, which are generated by the FTS module will be omitted
as per the deviation/notification :-
9. Indicates a FTS table, this will be generated, there will be no @Entity or @Dao files created for this table. (RED)
Columns View
Lists information about each column in the database. Notifications can appear with the respective attribute's value.
Notifications are :-
1. Indicates a room master table, this will be omitted from the converted database. (RED - with the Table attribute's
value).
2. The specified type affinity is not usable in Room, the derived type will be used (if not NUMERIC). (Orange - with the
Derived attribute's value).
3. If the Type Affinity used when defining the original/source table is not one of the four supported by ROOM (TEXT, INTEGER,
REAL or BLOB), then this notifications indicates that it will be changed.
4. NUMERIC type detected. Using TEXT and therefore object member will be String. (RED - with the FINAL attribute's
value).
5. Using String as the object type is the most easily converted option e.g. it could cater if DATETIME were the original type
affinity with timestamps or date/timestrings.
6. AUTOINCREMENT will be added. (RED - with the Autoincrement Coded attribute's value).
7. For automatically generated rowid's the @Entity includes @PrimaryKey(autoGenerate = true) and this
requires that AUTOINCREMENT is coded (as well as NOT NULL).
8. A default value has been detected. You may wish to set the object member to default to the value. (RED - with the
Default attribute's value).
Convert
Clicking the CONVERT button result in a dialog displaying the results of the conversion.
For example when converting the RoomAssetConversion.db (the Chinook Database), the result is :-
That is bar the WARNING the conversion was successfull.
The WARNING is due to 1 row not being copied in the employees table.
This is because the database has an internal foreign key on the reportsTo column and that NULL is used to indicate
reporting to nobody. This in conjunction with ROOM requiring that the indexes for Foreign Keys must have NOT NULL.
If there are issues then the dialog should display such issues.
Testing
Limited testing has been undertaken. Initally 3 Databases were used :-
For each an initial App was created with the main activity including methods to copy the database from the assets folder which is
invoked prior to the build of the ROOM database, and also methods using the copied database, invoked after the room build, to
access the data as a prrof of conversion.
ShopwiseDatabase mRoomDB;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/**
* The following code can be used to copy the database from the assets file before Room
* as an alternative to using the Room createFromAsset.
* Note that if used then the Room createFromAsset will do nothing
*/
//Start of code for copying database from the assets
copyDBFromAssets(this,ShopwiseDatabase.ASSETFILENAME,ShopwiseDatabase.DBNAME);
//Dump the table create SQL to the log for
SQLiteDatabase db =
SQLiteDatabase.openDatabase(this.getDatabasePath(ShopwiseDatabase.DBNAME).getPath(),null,SQLite
Database.OPEN_READWRITE);
Cursor csr = db.query("sqlite_master",new String[]
{"sql"},"type='table'",null,null,null,null);
while (csr.moveToNext()) {
Log.d("TABLESQL",csr.getString(0));
}
csr.close();
db.close();
//End of code for copying and dumping the pre-room copied database
// Build the Room database (note for testing/brevity run on main thread)
mRoomDB = Room.databaseBuilder(this,ShopwiseDatabase.class,ShopwiseDatabase.DBNAME)
//.createFromAsset(ChinookDatabase.DBNAME) //<<<<<<<<<< uses Room's Convert (will do
nothing if asset already copied)
.allowMainThreadQueries()
.build();
//Use each Table's getEvery????? method to extract all rows from the table (?????)
dumpObject((List)mRoomDB.getAislesDao().getEveryAisles());
dumpObject((List)mRoomDB.getAppValuesDao().getEveryAppvalues());
dumpObject((List)mRoomDB.getProductsDao().getEveryProducts());
dumpObject((List)mRoomDB.getProductUsageDao().getEveryProductusage());
dumpObject((List)mRoomDB.getRulesDao().getEveryRules());
dumpObject((List)mRoomDB.getShopListDao().getEveryShoplist());
dumpObject((List)mRoomDB.getShopsDao().getEveryShops());
dumpObject((List)mRoomDB.getStorageDao().getEveryStorage());
}
/**
* Dump a few extracted objects using the toString method (typically the default)
* @param objectListThe list of objects
*/
private void dumpObject(List<Object> objectList) {
int maxToDisplay = 5;
int displayed = 0;
Log.d("OBJECTDUMP","Dumping a maximum of " + String.valueOf(maxToDisplay) + " objects from
" + String.valueOf(objectList.size()) +".");
}
Log.d("OBJECTDUMP","Object as from class " + o.getClass().getName().toString() + " String
represntation is" + o.toString());
displayed++;
}
}
/**
* Copy the database from the assets folder, as an alternative to the ROOM createFromAsset
* @param context The context (for determining the database location and retrieving the
asset)
* @param assetFileName The path of the asset file within the assets folder
* @param databaseName The name of the database (typically the same but can be different
if not using createFromAsset)
*/
private void copyDBFromAssets(Context context, String assetFileName, String databaseName) {
if (checkIfDBExists(context,databaseName)) return;
File databaseFile = new File(context.getDatabasePath(databaseName).toString());
try {
InputStream is = context.getAssets().open(assetFileName);
OutputStream os = new FileOutputStream(databaseFile);
byte[] buffer = new byte[1024 * 32];
int length;
while ((length = is.read(buffer)) > 0) {
os.write(buffer,0,length);
}
os.flush();
os.close();
is.close();
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("Error copying asset database");
}
}
/**
* Check if the database exists and if not that any intermediate directories exist
* @param context The context (for determining the database path)
* @param databaseName The database name
* @return True if the database already exists, false if not
*/
private boolean checkIfDBExists(Context context, String databaseName) {
File dbfile = new File(context.getDatabasePath(databaseName).toString());
if (dbfile.exists()) return true;
if (!dbfile.getParentFile().exists()) {
dbfile.getParentFile().mkdirs();
}
return false;
}
}
In addition to the above a second file, the @Database file was manually created. For the ShopWise database the file
ShopwiseDatabase.java was :-
@Database(version = 1, entities = {
Aisles.class,
Appvalues.class,
Products.class,
Productusage.class,
Rules.class,
Shoplist.class,
Shops.class,
Storage.class
})
package your_package_name;
@Database(version=1,entities = {
Shops.class,
Aisles.class,
Products.class,
Productusage.class,
Shoplist.class,
Rules.class,
Appvalues.class,
Storage.class
})
public abstract class ShopWiseDB_201904202247Database extends RoomDatabase {
Note that this file would need to be tailored to suite the code above so that DBNAME has a value of Shopwise and that the
constant ASSETFILENAME is introduced with a value of ShopWiseDB_201904202247.bkp
All the other 16 files (an @Entity file and an @Dao file per table) were generated by the App, copied into the App using Android
Studio's Device Explorer, and then modified to generate the import statements.
In addition to writing the code the database file is copied to the App's assets folder.
The App when run produces the following output (not that SafeMode was used and hence component names are enclosed in ``) :-
and :-
For the King James Bible the output (fome similar code) was :-
Note that only the three tables bible, metadata and the VIRTUAL table bible_fts are created directly by the conversion.
bible_fts_?? tables are generated by the FTS3 module.
and
The CursorWindow full messages just indicating the the CursorWindow couldn't hold the row that was being added as it was
larger than the estimated row size and it would have been added to the next population of the CursorWindow.
The Chinook Database is similar other than that the 1 row as previously mentioned cannot be copied.
License
This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)