The app I'm building currently has a Workout database.
Workout DB has a Workout table and a WoroutSets table.
The data in these two tables is inserted(saved) through user input.
And that's where some data is stored.
By the way, I want to put pre-populated data called WorkoutList into this Workout DB.
I consulted the docs for this.
I exported Workout.db and pre-populated it with data in DB Browser for SQLite.
And we are going to use createFromAsset("Workout.db") as per the docs. (Haven't tried yet)
However, what I am concerned about is whether there is a conflict between the Work DB of the existing app and the Workout DB to which the WorkoutList table has been added.
Assuming that you want to preserve each app users workouts and workoutsetss that they have input then you would not want to overwrite them by using createFromAsset.
Rather I suspect that what you want to do is introduce a new workoutlist table that is populated with predefined/pre-existing rows in the workoutlist as per a database supplied as an asset. In this case you do not want to use the createFromAsset method (although you could potentially have a second database created from the asset, attach it to the original and then merge the data - this would be more complex than it need be).
You also have to consider how to handle new installs, in which case there will be no existing user input workouts and workoutsetss, in which case you could use createFromAsset method. However, you would not want any other user's workouts and workoutsetss rows.
Based upon this assumption perhaps consider this demo that introduces a new table (workoutlist) whose data is retrieved from an asset maintaining the original user data in the other tables (workout and workoutset) but for a new install of the App creates database from the asset.
the schema is made up so will very likely differ from yours but the principle applies.
Java has been used but it would take little to change it to Kotlin
Workout
#Entity
class Workout {
#PrimaryKey
Long workoutId=null;
String workoutName;
Workout(){};
#Ignore
Workout(String workoutName) {
this.workoutId=null;
this.workoutName=workoutName;
}
}
WorkoutSet (plural not used but easily changed)
#Entity
class WorkoutSet {
#PrimaryKey
Long workoutSetId=null;
String workoutSetName;
long workoutIdMap;
WorkoutSet(){}
#Ignore
WorkoutSet(String workoutSetName, long parentWorkout) {
this.workoutSetId=null;
this.workoutSetName = workoutSetName;
this.workoutIdMap = parentWorkout;
}
}
WorkkoutList (the new table)
#Entity
class WorkoutList {
#PrimaryKey
Long workoutListId=null;
String workoutListName;
}
AllDAO (just for completeness)
#Dao
abstract class AllDAO {
#Insert(onConflict = OnConflictStrategy.IGNORE)
abstract long insert(Workout workout);
#Insert(onConflict = OnConflictStrategy.IGNORE)
abstract long insert(WorkoutSet workoutSet);
#Query("SELECT count(*) FROM workout")
abstract long getNumberOfWorkouts();
}
WorkoutDatabase the #Database annotated class
#Database(entities = {Workout.class,WorkoutSet.class, WorkoutList.class /*<<<<<<<<<< ADDED for V2 */}, exportSchema = false, version = MainActivity.DATABASE_VERSION)
abstract class WorkoutDatabase extends RoomDatabase {
abstract AllDAO getAllDAO();
private static Context passed_context;
private static volatile WorkoutDatabase INSTANCE;
static WorkoutDatabase getInstance(Context context) {
passed_context = context;
if (INSTANCE==null && MainActivity.DATABASE_VERSION == 1) {
INSTANCE = Room.databaseBuilder(context,WorkoutDatabase.class,MainActivity.DATABASE_NAME)
.allowMainThreadQueries()
.build();
}
if (INSTANCE ==null && MainActivity.DATABASE_VERSION > 1) {
INSTANCE = Room.databaseBuilder(context,WorkoutDatabase.class,MainActivity.DATABASE_NAME)
.allowMainThreadQueries()
.createFromAsset(MainActivity.DATABASE_ASSET_NAME) /* so new App installs use asset */
.addMigrations(MIGRATION_1_TO_2) /* to handle migration */
.build();
}
return INSTANCE;
}
static Migration MIGRATION_1_TO_2 = new Migration(1,2) {
#SuppressLint("Range")
#Override
public void migrate(#NonNull SupportSQLiteDatabase database) {
/* Create the new table */
database.execSQL("CREATE TABLE IF NOT EXISTS `WorkoutList` (`workoutListId` INTEGER, `workoutListName` TEXT, PRIMARY KEY(`workoutListId`))");
/* Cater for copying the data from the asset */
String tempDBName = "temp_" + MainActivity.DATABASE_NAME; /* name of the temporary/working database NOT an SQLITE TEMP database */
String newTableName = "workoutlist"; /* The table name */
String qualifiedNewTableName = tempDBName + "." + newTableName; /* The fully qualified new table name for the attached temp/wrk db */
String tempDBPath = passed_context.getDatabasePath(MainActivity.DATABASE_NAME).getParent() + File.separator + tempDBName; /* path to temp/wrk db */
try {
/* Copy the asset to a second DB */
InputStream asset = passed_context.getAssets().open(MainActivity.DATABASE_ASSET_NAME); /* open the asset */
File tempDB_File = new File(tempDBPath); /* File for temp/wrk database */
OutputStream tempdb = new FileOutputStream(tempDB_File); /* now an output stream ready for the copy */
int bufferLength = 1024 * 8; /* length of buffer set to 8k */
byte[] buffer = new byte[bufferLength]; /* the buffer for the copy */
/* copy the temp/wrk database from the asset to it's location */
while(asset.read(buffer) > 0) {
tempdb.write(buffer);
}
/* clean up after copy */
tempdb.flush();
tempdb.close();
asset.close();
/*Use the temporary/working database to populate the actual database */
/* Issues with WAL file change because migration is called within a transaction as per */
/* java.lang.IllegalStateException: Write Ahead Logging (WAL) mode cannot be enabled or disabled while there are transactions in progress. .... */
/* SO COMMENTED OUT */
//database.execSQL("ATTACH DATABASE '" + tempDBPath + "' AS " + tempDBName);
//database.execSQL("INSERT INTO " + newTableName + " SELECT * FROM " + qualifiedNewTableName);
//database.execSQL("DETACH " + tempDBName);
/* Alternative to ATTACH */
SQLiteDatabase assetdb = SQLiteDatabase.openDatabase(tempDB_File.getPath(),null,SQLiteDatabase.OPEN_READONLY);
Cursor csr = assetdb.query(newTableName,null,null,null,null,null,null);
ContentValues cv = new ContentValues();
while (csr.moveToNext()) {
cv.clear();
for (String s: csr.getColumnNames()) {
cv.put(s,csr.getString(csr.getColumnIndex(s)));
}
database.insert(newTableName,SQLiteDatabase.CONFLICT_IGNORE,cv);
}
assetdb.close();
tempDB_File.delete(); /* delete the temporary/working copy of the asset */
} catch (Exception e) {
/* handle issues here e.g. no asset, unable to read/write an so on */
e.printStackTrace();
}
}
};
}
This has been written to allow easy switching/running of the App with either version, simply two changes to run as old or new version of the App.
to run as version 1 DATABASE_VERSION (in MainActivity) is set to 1
AND the WorkoutSet class is commented out in the #Database annotation.
the Mirgration handles the copy of the data from the asset if the database already exists, otherwise for a new file then createFromAssets is used to copy the database from the asset.
MainActivity the activity code that does something with the database to ensure that it is opened/accessed
public class MainActivity extends AppCompatActivity {
public static final String DATABASE_NAME = "workout.db";
public static final int DATABASE_VERSION = 2;
public static final String DATABASE_ASSET_NAME = "testit.db"/*DATABASE_NAME*/ /* could be different */;
WorkoutDatabase wdb;
AllDAO dao;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
wdb = WorkoutDatabase.getInstance(this);
dao = wdb.getAllDAO();
if (dao.getNumberOfWorkouts() < 1) addInitialData();
//wdb.close(); // FORCE close so WAL is full checkpointed (i.e. no -wal or -shm)
}
private void addInitialData() {
String prefix = String.valueOf(System.currentTimeMillis()); // to differentiate devices
dao.insert(new Workout(prefix + "-W001"));
dao.insert(new Workout(prefix + "-W002"));
dao.insert(new WorkoutSet(prefix + "-WS001",1));
dao.insert(new WorkoutSet(prefix + "-WS002",2));
}
}
note the wdb.close() is uncommented when creating the database to be loaded into the SQlite tool.
testit.db the SQLite database file as modified to introduce the new workoutList table. This first copied from a run of the App at Version 1 and then adding the new table (according SQL copied from the createAllTables method of WorkoutDataabase_Impl class in the generated java (aka the exact table as per Room's expectations)).
Note really the rows should be deleted as they are user specific. Instead -WRONG has been added to the end of the data (helps to prove the some points)
as above
The new table
Navicat was the SQLite tool used rather than DB Browser.
Run 1 running at version 1 to populate the database
DATABASE_VERSION = 1
#Database(entities = {Workout.class,WorkoutSet.class/*, WorkoutList.class*/ /*<<<<<<<<<< ADDED for V2 */}, exportSchema = false, version = MainActivity.DATABASE_VERSION)
i.e. the WorkoutList table has been excluded
App Inspection shows:-
and
Impotantly no WorkoutList table
Run 2 version 2 and introduction of new workoutlist table
DATABASE_VERSION = 2
#Database(entities = {Workout.class,WorkoutSet.class, WorkoutList.class /*<<<<<<<<<< ADDED for V2 */}, exportSchema = false, version = MainActivity.DATABASE_VERSION)
i.e. WorkoutList now introduced
App Inspection shows:-
old rows retained (they do not have -WRONG as per the asset)
old rows retained
the new table, populated as per the asset
Run 3 new install (after App uninstalled) at VERSION 2
as an be seen the incorrectly left in the assets rows exist (obviously you would delete the rows in real life but leaving them in shows that they are from the asset rather than generated by the code in the activity)
likewise
Conclusion
So the new install of the App correctly copies the asset database via createFromAsset whilst if migrating only the new data in the workoutlist table is copied from the asset.
Related
I started working in the Android ROOM persistence library and came across Pre-Populating database. In this article, they have mentioned adding .db file to the assets folder. But the issue is how to convert .sqlite file into .db file online.
This is totally a new project where there are only read operations with all the data pre-loaded. I am trying to consume it using the Room library but am failing to do so.
Please help me in getting the best way to either convert .sqlite file to .db file or any other way programmatically doing it so.
What you need to do is create an SQLite database based upon the XAMPP database using XAMPP to export the XAMPP database and an SQLite Tool (Navicat, DBeaver, DB Browser for SQlite, SQlite Studio), the resultant file can then be copied into the assets folder.
However Room is very strict in it's schema expectations and DOES NOT support the flexibility of SQLite's column types. As such it is suggested that first you define the #Entity annotated classes (the tables and indexes) then provide an #Database annotated class where the entities parameter of the #Database annotation specifies the #Entity annotated classes. At this stage compiling the project will generate the SQL for the tables and indexes and thus will be the tables that Room expects.
Perhaps consider this example, the XAMPP database (example):-
The first stage is to create the #Entity annotated classes (Java has been used as you have not specified the language). So in the project in Android Studio:-
Class Contact :-
#Entity
class Contact {
#PrimaryKey
Long contact_id=null; /* XAAMPP INT no need for AUTOINCREMENT i.e. autoGenerate= true in the #PrimaryKey annotation */
String contact_type; /* XAMPP VARCHAR */
String contact_detail; /* XAMPP TEXT */
String contact_notes;
}
so VARCHAR and TEXT equate to a String member/field
any integer type can be int, Integer, long, Long, byte, Byte all of which end up being SQLite type INTEGER when processed by Room.
AUTOINCREMENT for SQLite is inefficient and not needed see https://www.sqlite.org/autoinc.html
Long .... = null; has been used as long being a primitive has a default value of 0. No value (null) for INTEGER PRIMARY KEY (an alias of the rowid) results in value being automatically generated and hence effectively AUTOINCREMENT.
Room will only generate column types INTEGER (integer types), REAL (floating point types), TEXT (String types) and BLOB (byte array/ stream types) any other type in the pre-populated database and an exception will result (expected .... found ....).
Class Student :-
#Entity
class Student {
#PrimaryKey
Long student_id=null;
#NotNull
String student_forename;
#NotNull
String student_surname;
#NotNull
String student_street_address;
#NotNull
String student_city;
#NotNull
String student_county;
#NotNull
String student_country;
#NotNull
String student_dob;
#NotNull
String student_email;
Student(){}
#Ignore
Student(long id,String student_forename, String student_surname,String student_street_address, String student_city, String student_county, String student_country, String student_dob, String student_email) {
this.student_forename = student_forename;
this.student_surname = student_surname;
this.student_street_address = student_street_address;
this.student_city = student_city;
this.student_county = student_county;
this.student_country = student_country;
this.student_dob = student_dob;
this.student_email = student_email;
}
#Ignore
Student(String student_forename, String student_surname, String student_street_address, String student_city, String student_county, String student_country, String student_dob, String student_email) {
this.student_forename = student_forename;
this.student_surname = student_surname;
this.student_street_address = student_street_address;
this.student_city = student_city;
this.student_county = student_county;
this.student_country = student_country;
this.student_dob = student_dob;
this.student_email = student_email;
}
}
Class StudentContactMap (Foreign Key added for referential integrity):-
#Entity(
tableName = "student_contact_map" /* name of the table different to class - not really needed - example of how to */
, primaryKeys = {"student_map","contact_map"} /* composite primary key as can't have multiple #PrimaryKey annotations */
/* Optional Foreign Keys to enforce referential integrity */
, foreignKeys = {
#ForeignKey(
entity = Student.class,
parentColumns = {"student_id"},
childColumns = {"student_map"},
/* Optional but simplifies referential integrity maintenance */
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
),
#ForeignKey(
entity = Contact.class,
parentColumns = {"contact_id"},
childColumns = {"contact_map"},
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
}
)
class StudentContactMap {
long student_map;
/* Added index as Room will warn about full table scan potential */
#ColumnInfo(index = true)
long contact_map;
}
An #Dao annotated interface (could be an abstract class) not required at this stage AllDAOs (can have multiple but for convenience just one):-
#Dao
interface AllDAOs {
#Insert(onConflict = OnConflictStrategy.IGNORE)
Long insert(Contact contact);
#Insert(onConflict = OnConflictStrategy.IGNORE)
Long insert(Student student);
#Insert(onConflict = OnConflictStrategy.IGNORE)
Long insert(StudentContactMap studentContactMap);
#Query("SELECT * FROM student")
List<Student> getAllStudents();
#Query("SELECT * FROM contact")
List<Contact> getAllContacts();
#Query("SELECT * FROM student_contact_map")
List<StudentContactMap> getAllStudentContactMaps();
}
An #Database annotated class including singleton approach to getting the database instance, TheDatabase (obviously could be named otherwise):-
#Database(entities = {Contact.class,Student.class,StudentContactMap.class}, exportSchema = false, version = 1)
abstract class TheDatabase extends RoomDatabase {
/* method to retrieve the databases #Dao annotated interface (can have multiple such interfaces/abstract classes) */
abstract AllDAOs getAllDAOs();
/* For singleton approach */
private static volatile TheDatabase INSTANCE;
static TheDatabase getInstance(Context context) {
if (INSTANCE==null) {
INSTANCE = Room.databaseBuilder(context,TheDatabase.class,"the_database.db")
.allowMainThreadQueries() /* for convenince of demo */
.createFromAsset("the_database.db") /* asset name can be different to database name */
.build();
}
return INSTANCE;
}
}
Compile to generate expected schema (SQL)
Ctrl + F9 will compile the project. From Android View java(generated) will contain a class that is the same name as the #Database annotated class but suffixed with *_Impl.
Within that class there is a method createAllTables with SQL for each component (table index) that Room itself would create. i.e. the EXACT schema that Room expects to find.
Now you can move to create the pre-populated database in the SQLite tool (Navicat used).
For each component (table/index except the room_master_table) copy the SQL from the generated java to an empty connection/database and the window/view that allows you to run SQL (you can run the SQL) e.g. :-
Loading the data
Switch to XAMPP and export the database as SQL e.g. in Notepad or whatever you get something along the lines of :-
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
/*!40101 SET #OLD_CHARACTER_SET_CLIENT=##CHARACTER_SET_CLIENT */;
/*!40101 SET #OLD_CHARACTER_SET_RESULTS=##CHARACTER_SET_RESULTS */;
/*!40101 SET #OLD_COLLATION_CONNECTION=##COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
--
-- Database: `example`
--
-- --------------------------------------------------------
--
-- Table structure for table `contact`
--
CREATE TABLE `contact` (
`contact_id` int(11) NOT NULL,
`contact_type` varchar(16) NOT NULL,
`contact_detail` text NOT NULL,
`contact_notes` text NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
--
-- Dumping data for table `contact`
--
INSERT INTO `contact` (`contact_id`, `contact_type`, `contact_detail`, `contact_notes`) VALUES
(1, 'Mobile', '0000 000 000', ''),
(3, 'Mobile', '1111 111 111', ''),
(4, 'Mobile', '2222 222 222', '');
....
As the tables have been created according to Room's expectations all that is required is to populate them using the insert statements so editing the export SQL:-
--
-- Dumping data for table `contact`
--
INSERT INTO `contact` (`contact_id`, `contact_type`, `contact_detail`, `contact_notes`) VALUES
(1, 'Mobile', '0000 000 000', ''),
(3, 'Mobile', '1111 111 111', ''),
(4, 'Mobile', '2222 222 222', '');
--
-- Dumping data for table `student`
--
INSERT INTO `student` (`student_id`, `student_forename`, `student_surname`, `student_street_address`, `student_city`, `student_county`, `student_country`, `student_dob`, `student_email`) VALUES
(1, 'Fred', 'Blogs', '1 Somewhere Place', 'London', 'Middlesex', 'England', '2002-03-21', 'fblogs#student_mail.org'),
(2, 'Jane', 'Doe', 'Nowhere House', 'Henley on Thames', 'Berkshire', 'England', '2001-08-11', 'jdoe#student_mail.org');
--
-- Dumping data for table `student_contact_map`
--
INSERT INTO `student_contact_map` (`student_map`, `contact_map`) VALUES
(1, 1),
(1, 2),
(2, 3);
Just in case instead of just INSERT INTO the inserts are changed to INSERT OR IGNORE INTO.
Running the SQL results in :-
INSERT OR IGNORE INTO `contact` (`contact_id`, `contact_type`, `contact_detail`, `contact_notes`) VALUES
(1, 'Mobile', '0000 000 000', ''),
(2, 'Mobile', '1111 111 111', ''),
(3, 'Mobile', '2222 222 222', '')
> Affected rows: 3
> Time: 0.024s
--
-- Dumping data for table `student`
--
INSERT OR IGNORE INTO `student` (`student_id`, `student_forename`, `student_surname`, `student_street_address`, `student_city`, `student_county`, `student_country`, `student_dob`, `student_email`) VALUES
(1, 'Fred', 'Blogs', '1 Somewhere Place', 'London', 'Middlesex', 'England', '2002-03-21', 'fblogs#student_mail.org'),
(2, 'Jane', 'Doe', 'Nowhere House', 'Henley on Thames', 'Berkshire', 'England', '2001-08-11', 'jdoe#student_mail.org')
> Affected rows: 2
> Time: 0.024s
--
-- Dumping data for table `student_contact_map`
--
INSERT OR IGNORE INTO `student_contact_map` (`student_map`, `contact_map`) VALUES
(1, 1)
,(1, 2)
,(2, 3)
> Affected rows: 3
> Time: 0.024s
i.e. All data has been successfully inserted as expected.
Save the database, check that only the one file exists that there are no files the same names as the database suffixed with -wal or -shm (-journal file can be ignored).
-wal/-shm file are files used when the database is using Write Ahead Logging. The -wal file contains part of the database. Closing the database should write the -wal to the database and empty and delete the -wal file (with Navicat you have to exit Navicat).
So the end result is a file aka the database e.g. :-
This file can then be copied and pasted into the assets folder e.g.:-
note the file was renamed accordingly
Time to test
To test the copy of the pre-populated database an instance of the #Database annotated class must be obtained and also an attempt must be made to access the database (it is not until an attempt is made to access the database that it will be opened, getting an instance does not open the database). As such some activity code will be needed e.g. :-
public class MainActivity extends AppCompatActivity {
TheDatabase dbInstance;
AllDAOs dao;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
dbInstance = TheDatabase.getInstance(this);
dao = dbInstance.getAllDAOs();
for (Student s: dao.getAllStudents()) {
Log.d("DBINFO","Student is " + s.student_forename + " " + s.student_surname + " etc");
}
}
}
When run then the log includes:-
D/DBINFO: Student is Fred Blogs etc
D/DBINFO: Student is Jane Doe etc
Success
I have an app Im trying to write that has 3 objects that inherit from one object, all entities,
Im using room to store those locally.
In the dao of each entity I have "getAll" function that returnes a livedata<List>.
My question is,
is there a way, to get all of the lists from the database as one list (since they all inherit from the same class)?
Unless Im wrong, if I'll just use "getAll" on the superclass it wont give me the specific fields for every class.
and I have one recyclerView that holds those objects as 1 list so I need a way to combine them.
I tried looking it up but when it comes to inheritance its not really clear how Room handle stuff.(for example in the documentation google gives an example using inheritance with both objects having uniqe id's, but when i tried i got an error that the superclass id will be overwritten by the subclass id.).
If anyone could help, or provide a link to where i can learn more about it I'll greatly appriciate it.
Thanks, and have a great day!
Leaving this here in-case someone else needs it.
There are multiple ways to go about solving this one.
The first one is using a POJO as "MikeT" stated on his answer.
The second one is adding a "type" property to the superclass and get the whole
superclass list, and on the runtime select the proper object and create it.(using
the id since its the same).
the downside is that you access the db multiple times which can reduce
performance. (the solution I was going for before this morning)
The third way(that I ended using) is in this post answer by "Danail Alexiev"
Polymorphic entities in Room
creating a custom MediatorLiveData implementation that zipps the 2 (or more)
livedata objects and returns one.
I believe that you could use a POJO (or perhaps a suitable Entity if one exists) that includes ALL fields and utilise a getAll #Query that includes a UNION. Of course the better way is to perhaps reconsider the design.
The following is an example of a Parent (BaseObject) from which 2 Objects are inherited, name ChildType1 and ChildType2.
In this example a ChildType2 to has 2 additional fields one of which a ChildType1 has as it's only additional field. Hence a ChildType2 is suitable for holding all the fields of a ChildType1.
However, to enable a ChildType1 to be correctly extracted it has to mimic the additional field of the ChildType2. This can be done easily with the SQL in the getAll() method in the Dao Alldao.
The following is the code utilised:-
BaseObject from which the two ChildTypes inherit:-
class BaseObject {
#ColumnInfo(name = BaseColumns._ID)
Long id;
String name;
long createdTimestamp = System.currentTimeMillis() / 1000;
int type;
}
ChildType1 :-
#Entity(primaryKeys = {BaseColumns._ID})
class ChildType1 extends BaseObject {
public static final int TYPE = 1;
String ct1;
}
As will be seen the id column (_id) has been inherited and later that it causes no issues.
ChildType2 :-
#Entity(primaryKeys = {BaseColumns._ID})
class ChildType2 extends BaseObject {
public static final int TYPE = 2;
String ct1;
String ct2;
}
AllDao where All the Dao's have been coded :-
#Dao
interface AllDao {
#Insert
long insert(ChildType1 childType1);
#Insert
long insert(ChildType2 childType2);
#Query("SELECT *, 'n/a' AS ct2 FROM ChildType1 UNION SELECT * FROM childtype2")
List<ChildType2> getAll();
}
The query being using a UNION of initially the childtype1 table filling in the missing ct2 field with the value n/a and the childtype2 table. Note that id's will probably be duplicated so to utilise an id you would have to determine the respective type (e.g. is ct2 = n/a then it's probably a ChildType1 (hence why I'd string suggest an indicator of the type which cannot be ambiguous)).
The #Database TheDatabase :-
#Database(entities = {ChildType1.class,ChildType2.class},version = 1)
abstract class TheDatabase extends RoomDatabase {
abstract AllDao getAllDao();
private volatile static TheDatabase instance;
public static TheDatabase getInstance(Context context) {
if (instance == null) {
instance = Room.databaseBuilder(
context,
TheDatabase.class,
"thedatabase.db"
)
.allowMainThreadQueries()
.build();
instance.getOpenHelper().getWritableDatabase();
}
return instance;
}
}
And finally an Activity, MainActivity putting it all together to demonstrate :-
public class MainActivity extends AppCompatActivity {
TheDatabase db;
AllDao dao;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
db = TheDatabase.getInstance(this);
dao = db.getAllDao();
ChildType1 c1_1 = new ChildType1();
c1_1.name = "CT1 001";
c1_1.ct1 = "This is a CT1 for CT1 001";
c1_1.type = ChildType1.TYPE;
dao.insert(c1_1);
ChildType2 c2_1 = new ChildType2();
c2_1.name = "CT2 002";
c2_1.ct1 = "This is CT1 for CT2 002";
c2_1.ct2 = "This is CT2 for CT2 002";
dao.insert(c2_1);
for(ChildType2 c: dao.getAll()) {
Log.d("" +
"TYPEINFO",
"Name = " + c.name +
"\n\t Created = " + c.createdTimestamp +
"\n\t ID = " + c.id +
"\n\t type = " + c.type +
"\n\t CT1 = " + c.ct1 +
"\n\t CT2 = " + c.ct2
);
}
}
}
Result
When run the log contains :-
D/TYPEINFO: Name = CT1 001
Created = 1626589554
ID = 1
type = 1
CT1 = This is a CT1 for CT1 001
CT2 = n/a
D/TYPEINFO: Name = CT2 002
Created = 1626589554
ID = 1
type = 0
CT1 = This is CT1 for CT2 002
CT2 = This is CT2 for CT2 002
i.e. both types of children have been extracted into a single List of objects that can contain ALL the fields.
How can I find out the size occupied by Room databases by looking at the app settings? The storage section of the app settings is divided into Total, App size, User data and Cache. Does the app database account for the User data section or is it the Cache? I want to find an estimate of how big my database is so I can find the maximum number of rows I can keep in the db without it taking too much space.
I want to find an estimate of how big my database is so I can find the maximum number of rows I can keep in the db without it taking too much space.
The number of rows doesn't exactly equate to database size. That is because the data is stored in pages (by default 4k). A table with 0 rows will take up 4k, with 1000 rows it could still just take up 4k.
Each SQLite entity (table, Index, Trigger etc) will take up at least 1 page.
You may wish to read SQLite Database File Format
Ignoring the page factor you could add a method to the #Database class like :-
public static long getDBSize(Context context) {
return (context.getDatabasePath(instance.getOpenHelper().getDatabaseName())).length()
// Add the shared memory (WAL index) file size
+ (context.getDatabasePath(instance.getOpenHelper().getDatabaseName() + "-shm")).length()
// Add the WAL file size
+ (context.getDatabasePath(instance.getOpenHelper().getDatabaseName() + "-wal")).length()
// Add the journal file size
+ (context.getDatabasePath(instance.getOpenHelper().getDatabaseName() + "-journal")).length();
}
Working Example (based upon an existing App used for answering some qustions)
The #Database class :-
Retrieving the combined filesizes :-
#Database(entities = {State.class,Location.class,School.class,TimerDesign.class,IntervalDesign.class,Table1.class, MessageItem.class, Contact.class},exportSchema = false,version = 1)
abstract class TheDatabase extends RoomDatabase {
abstract AllDao getAllDao();
private static volatile TheDatabase instance;
public static TheDatabase getInstance(Context context) {
if (instance == null) {
instance = Room.databaseBuilder(
context,
TheDatabase.class,
"state.db"
)
.allowMainThreadQueries()
.addCallback(new Callback() {
#Override
public void onCreate(SupportSQLiteDatabase db) {
super.onCreate(db);
}
#Override
public void onOpen(SupportSQLiteDatabase db) {
super.onOpen(db);
}
})
.build();
}
return instance;
}
public static long getDBSize(Context context) {
// For Demonstration Log the individual sizes
Log.d("DBSIZEINFO",
"Space from main DB file = " + String.valueOf(context.getDatabasePath(instance.getOpenHelper().getDatabaseName()).length())
+ "\nSpace from -shm file = " + String.valueOf(context.getDatabasePath(instance.getOpenHelper().getDatabaseName() + "-shm").length())
+ "\nSpace from -wal file = " + String.valueOf(context.getDatabasePath(instance.getOpenHelper().getDatabaseName() + "-wal").length())
+ "\nSpace from journal file = " + String.valueOf(context.getDatabasePath(instance.getOpenHelper().getDatabaseName() + "-journal").length())
);
return (context.getDatabasePath(instance.getOpenHelper().getDatabaseName())).length()
// Add the shared memory (WAL index) file size
+ (context.getDatabasePath(instance.getOpenHelper().getDatabaseName() + "-shm")).length()
// Add the WAL file size
+ (context.getDatabasePath(instance.getOpenHelper().getDatabaseName() + "-wal")).length()
// Add the journal file size
+ (context.getDatabasePath(instance.getOpenHelper().getDatabaseName() + "-journal")).length();
}
}
Note individual file sizes added for demo/explanation
and invoking code :-
public class MainActivity extends AppCompatActivity {
TheDatabase db;
AllDao dao;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//Instantiate Database and get dao
db = TheDatabase.getInstance(this);
dao = db.getAllDao();
Log.d("DBSIZEINFO","Database Size is " + TheDatabase.getDBSize(this));
}
}
Result (i.e. the log includes) :-
2021-06-22 09:20:53.960 25867-25867/a.a.so67952337javaroomstatefacilities D/DBSIZEINFO: Space from main DB file = 4096
Space from -shm file = 32768
Space from -wal file = 70072
Space from journal file = 0
2021-06-22 09:20:53.960 25867-25867/a.a.so67952337javaroomstatefacilities D/DBSIZEINFO: Database Size is 106936
Device Explorer shows :-
Explanation of the Results
As can be seen the result is showing that the database file itself is small in comparison to the -wal and -shm files (it is clear that WAL mode is in effect as both are greater than 0). This is because effectively the database consists of the -wal (i.e changes waiting to be applied to the database) and the database file. The -shm file will not be applied it is a work file used for the -wal file.
the -wal file is applied (may be partly) when CHECKPOINTS are made.
That is in WAL mode changes are written to the -wal file (roll back is removing part of the -wal file).
If journal mode were in effect then the journal is the log that is used to undo changes made to the database file.
Does the app database account for the User data section or is it the Cache?
Although you may wish to read:-
SQLite Write-Ahead Logging
SQLite Temporary Files Used By SQLite
Additional
If you wanted to not have to pass the context when checking the size(s) then you could use something based upon :-
#Database(entities = {State.class,Location.class,School.class,TimerDesign.class,IntervalDesign.class,Table1.class, MessageItem.class, Contact.class},exportSchema = false,version = 1)
abstract class TheDatabase extends RoomDatabase {
abstract AllDao getAllDao();
private static volatile TheDatabase instance;
private static File databaseFile; //<<<<<<<<<< ADDED
public static TheDatabase getInstance(Context context) {
if (instance == null) {
instance = Room.databaseBuilder(
context,
TheDatabase.class,
"state.db"
)
.allowMainThreadQueries()
.addCallback(new Callback() {
#Override
public void onCreate(SupportSQLiteDatabase db) {
super.onCreate(db);
}
#Override
public void onOpen(SupportSQLiteDatabase db) {
super.onOpen(db);
}
})
.build();
}
databaseFile = context.getDatabasePath(instance.getOpenHelper().getDatabaseName()); //<<<<<<<<<< ADDED
return instance;
}
/* ALTERNATIVE without the need for the Context*/
public static long getDBSize() {
if (databaseFile == null) return -1;
return databaseFile.length() +
new File(databaseFile.getPath() + "-shm").length() +
new File(databaseFile.getPath() + "-wal").length() +
new File(databaseFile.getPath() + "-journal").length();
}
}
in my project, there is a login system in which the username, the email and the password are stored in the sqlite database from by android room. They are all hashed via an android chiper algorithm and then converted by a type converter to String to the database. But I cannot compare them with the following statement it is just going to return null...
So how do I compare those byte[] correctly? Or should it work like that and my mistake is somewhere else?
#Query("select passwordHashed from login where userHashed = :userHashGiven")
byte[] getPasswordHashByUserHash(userHashGiven byte[])
My TypeConverter and the Login object looks like this:
#TypeConverter
fun stringToBytes(s: String) : ByteArray {
return s.toByteArray(EncrypterUtil.charset)
}
#TypeConverter
fun bytesToString(bytes: ByteArray) : String {
return String(bytes, EncrypterUtil.charset)
}
#Entity(tableName = "login")
public class Login {
#NonNull
#PrimaryKey
private byte[] userHashed;
#ColumnInfo
private byte[] emailHashed;
#ColumnInfo
private byte[] passwordHashed;
#ColumnInfo
private int biometric;
public Login(#NotNull byte[] userHashed, byte[] emailHashed, byte[] passwordHashed, int biometric) {
this.userHashed = userHashed;
this.emailHashed = emailHashed;
this.passwordHashed = passwordHashed;
this.biometric = biometric;
}
#NonNull
public byte[] getUserHashed() {
return userHashed;
}
public void setUserHashed(#NonNull byte[] userHashed) {
this.userHashed = userHashed;
}
public byte[] getEmailHashed() {
return emailHashed;
}
public void setEmailHashed(byte[] emailHashed) {
this.emailHashed = emailHashed;
}
public byte[] getPasswordHashed() {
return passwordHashed;
}
public void setPasswordHashed(byte[] passwordHashed) {
this.passwordHashed = passwordHashed;
}
public int getBiometric() {
return biometric;
}
public void setBiometric(int biometric) {
this.biometric = biometric;
}
Result in the database is the following:
According to the Login class (Entity) that you have shown.
The #Query is incorrect in that it is using columns passwordHash and userHash whilst the Entity has respective columns named passwordHashed and userHashed.
Should it instead be #Query("select passwordHashed from login where userHashed = :userHashGiven")?
As you have a mix of Java and Kotlin, it may be that you have inadvertently mixed up the table that you are extracting the data from with the table that you are inserting data.
In regard to comparing BLOBs then SQLite uses a memory compare and thus the compare should work. As an example consider the following :-
DROP TABLE IF EXISTS login;
CREATE TABLE IF NOT EXISTS login (userHashed BLOB);
INSERT INTO login VALUES
(x'B1C1D1E1F1'),
(x'F1E1D1C1B1'),
(x'0101010101'),
(x'FFEEDDCCBB')
;
SELECT userHashed, hex(userHashed) FROM login WHERE userHashed = x'B1C1D1E1F1';
This
drops the login table if it exists,
creates the login table (just the one BLOB column),
Inserts 4 rows all with different BLOB values, and finally
Extracts rows (should be only the one that matches the argument)
Running the above returns the expected result as per :-
Note that Navicat was used for the above and that is how it displays BLOBS and hence why the SQLite hex function has been used to display the value as a string representation and thus to confirm that the expected row has been returned.
Example
Based upon the corrected (Query) code that you have supplied does work.
Assuming that the Dao is call AllDao then the following MainActivity was used to test the code.
Note that the following #Query was also added :-
#Query("SELECT * FROM login WHERE userHashed = :userHashGiven")
Login[] getLoginsByUserHash(byte[] userHashGiven);
This to extract a Login object rather than a byte[]
The code used is :-
public class MainActivity extends AppCompatActivity {
Database db;
AllDao allDao;
byte[] t1 = new byte[]{0,1,2,3,4,5,6,7,8,9},
t2 = new byte[]{10,11,12,13,14,15,16,17,18,19},
t3 = new byte[]{20,21,22,23,24,25,26,27,28,29},
t4 = new byte[]{30,31,32,33,34,35,36,37,38,39}
;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
db = Room.databaseBuilder(this,Database.class,"mydb")
.allowMainThreadQueries()
.build();
allDao = db.allDao();
Login l1 = new Login(t1,t2,t3,10);
Login l2 = new Login(t2,t3,t4,20);
Login l3 = new Login(t3,t4,t1,30);
Login l4 = new Login(t4,t1,t2,40);
allDao.insertLogin(l1);
allDao.insertLogin(l2);
allDao.insertLogin(l3);
allDao.insertLogin(l4);
Login[] extractedLogins = allDao.getLoginsByUserHash(t2);
Log.d("LOGINTEST","Extracted " + extractedLogins.length + " rows");
byte[] passwordHash = allDao.getPasswordHashByUserHash(t2);
Log.d("LOGINTEST","Extracted " + passwordHash.length + " rows");
Log.d("LOGINTEST",passwordHash.toString());
}
}
As you can see, this adds 4 rows to the Login table using some pre-defined byte[]'s as per t1 - t4 (the userHashed being t1-t4 respectively for the 4 rows).
After the data has been inserted an Array of Login's is extracted using the new query which should retrieve just the 1 Login as per the WHERE clause.
Then the original query is used to extract the byte[].
When run in debug mode with a break point one the line Log.d("LOGINTEST",passwordHash.toString());
The the following results can be seen.
The Log includes :-
D/LOGINTEST: Extracted 1 rows
D/LOGINTEST: Extracted 10 rows
i.e. 1 Login object has been extracted by the first(new) query and a byte[] that is 10 bytes in length has been returned by the 2nd(original) query.
The Debug screen shows (as expected):-
That is extractedLogins (an array of Login objects) has 1 object and the passwordHashed value is t4 (3rd value of the 2nd row)
and also that the byte[] extracted is t4 (again as expected).
Or should it work like that and my mistake is somewhere else?
I believe that your issue is elsewhere (as the working example shows you that the query works).
I'm trying to test Room DB after including a new Column date. But I'm not getting how to insert data as I cannot use context in Testing.java file.
my code looks like this,
#RunWith(AndroidJUnit4.class)
public class MigrationTest {
private static final String TEST_DB = "note_database";
#Rule
public MigrationTestHelper helper;
public MigrationTest() {
helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
NoteDatabase.class.getCanonicalName(),
new FrameworkSQLiteOpenHelperFactory());
}
#Test
public void migrateAll() throws IOException {
// Create earliest version of the database.
SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 2);
db.execSQL("INSERT INTO note_table (title,description,priority,date)" +
" VALUES ('title_test','description_test','priority_test','10/12/2019'); ");
db.close();
// Open latest version of the database. Room will validate the schema
// once all migrations execute.
NoteDatabase appDb = Room.databaseBuilder(
InstrumentationRegistry.getInstrumentation().getTargetContext(),
NoteDatabase.class,
TEST_DB)
.addMigrations(ALL_MIGRATIONS).build();
appDb.getOpenHelper().getWritableDatabase();
appDb.close();
}
// Array of all migrations
private static final Migration[] ALL_MIGRATIONS = new Migration[]{
MIGRATION_1_2};
}
This code was working fine but , look at this reference code. i need to do something like this.
and I'm not getting how to read getMigratedRoomDatabase data. can anyone help me on this.
// MigrationTestHelper automatically verifies the schema
//changes, but not the data validity
// Validate that the data was migrated properly.
User dbUser = getMigratedRoomDatabase().userDao().getUser();
assertEquals(dbUser.getId(), USER.getId());
assertEquals(dbUser.getUserName(), USER.getUserName());
// The date was missing in version 2, so it should be null in
//version 3
assertEquals(dbUser.getDate(), null);
That is nothing but appDb in your code. it will look like
appDb.userDao().getUser();