Comparing byte[] in android room Daos - android

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).

Related

How to pre-populate an existing DB with data?

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.

Supplement a Records Columns in Room

I am fairly new to Android Room and SQLite in general, so sorry if this is a simple question.
I am getting data from a API that I'd like to insert into a database so it's accessible when the device is offline.
Depending on the endpoint of the API, some fields of my Data objects may be null (Think a summary with just the basic fields versus a fully detailed object with all fields)
To keep the database clean, I'd like to update the entries, but only the columns that are not null (eg. that I have new values for) and keep the rest of the columns untouched.
Here are some example classes to clarify:
Person
#Entity(tableName = "person", indices = {
#Index(value = "id", unique = true)
})
public class Person {
#PrimaryKey
public int id;
public String name;
public String description;
}
Example:
// create db
RoomDB db = RoomDB.create(ctx);
// create some sample objects
final Person p2 = new Person(2, "Peter", null);
// insert them into the db
db.personDao().insert(p2);
// create a updated peter that likes spiders
// but has no name (as a example)
final Person newPeter = new Person(2, null, "Peter likes spiders");
// and update him
db.personDao().updateNonNull(newPeter);
// now we read him back
final Person peter = db.personDao().getById(2);
In this example, the desired values of 'peter' would be:
id = 2
name = "Peter"
description = "Peter likes spiders"
However, using Room's #Update or #Insert i can only get this:
id = 2
name = null
description = "Peter likes spiders"
The only way i found to achive this would be to manuall get the object and supplement the values like so:
#Transaction
public void updateNonNull(Person newPerson) {
final Person oldPerson = getById(newPerson.id);
if (oldPerson == null) {
insert(newPerson);
return;
}
if (newPerson.name == null)
newPerson.name = oldPerson.name;
if (newPerson.description == null)
newPerson.description = oldPerson.description;
update(newPerson);
}
However, that would result in quite a bit of code with bigger objects...
So my question, is there a better way to do this?
Edit:
After some Testing with the SQL by #Priyansh Kedia, i found that those functions indeed work as intended and do so at a higher performance than java.
However, as a SQL statement would have required me to write huge queries, i decided to use a Reflection based solution, as can be seen below.
I only did so because the function isn't called regularly, so the lower performance won't matter too much.
/**
* merge two objects fields using reflection.
* replaces null value fields in newObj with the value of that field in oldObj
* <p>
* assuming the following values:
* oldObj: {name: null, desc: "bar"}
* newObj: {name: "foo", desc: null}
* <p>
* results in the "sum" of both objects: {name: "foo", desc: "bar"}
*
* #param type the type of the two objects to merge
* #param oldObj the old object
* #param newObj the new object. after the function, this is the merged object
* #param <T> the type
* #implNote This function uses reflection, and thus is quite slow.
* The fastest way of doing this would be to use SQLs' ifnull or coalesce (about 35% faster), but that would involve manually writing a expression for EVERY field.
* That is a lot of extra code which i'm not willing to write...
* Besides, as long as this function isn't called too often, it doesn't really matter anyway
*/
public static <T> void merge(#NonNull Class<T> type, #NonNull T oldObj, #NonNull T newObj) {
// loop through each field that is accessible in the target type
for (Field f : type.getFields()) {
// get field modifiers
final int mod = f.getModifiers();
// check this field is not status and not final
if (!Modifier.isStatic(mod)
&& !Modifier.isFinal(mod)) {
// try to merge
// get values of both the old and new object
// if the new object has a null value, set the value of the new object to that of the old object
// otherwise, keep the new value
try {
final Object oldVal = f.get(oldObj);
final Object newVal = f.get(newObj);
if (newVal == null)
f.set(newObj, oldVal);
} catch (IllegalAccessException e) {
Log.e("Tenshi", "IllegalAccess in merge: " + e.toString());
e.printStackTrace();
}
}
}
}
There is no in-built method in room to do this
What you can do is, put check in the query for your update method.
#Query("UPDATE person SET name = (CASE WHEN :name IS NOT NULL THEN :name ELSE name END), description = (CASE WHEN :description IS NOT NULL THEN :description ELSE description END) WHERE id = :id")
Person update(id: Int, name: String, description: String)
We have written the update query for SQL which checks if the inserted values are null or not, and if they are null, then the previous values are retained.

Is it possible to enable/ disable transformation on selected Room Database Dao column?

Currently, we have the following database table
#Entity(
tableName = "note"
)
public class Note {
#ColumnInfo(name = "body")
private String body;
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
}
The length of the body string, can be from 0 to a very large number.
In certain circumstance, we need to
Load the all notes into memory.
A LiveData which is able to inform observers, if there's any changes made in the SQLite note table.
We just need the first 256 characters of body. We do not need entire body. Loading entire body string for all notes might cause OutOfMemoryException.
We have the following Room Database Dao
#Dao
public abstract class NoteDao {
#Query("SELECT * FROM note")
public abstract LiveData<List<Note>> getAllNotes();
}
getAllNotes able to fulfill requirements (1) and (2), but not (3).
The following getAllNotesWithShortBody is a failed solution.
#Dao
public abstract class NoteDao {
#Query("SELECT * FROM note")
public abstract LiveData<List<Note>> getAllNotes();
#Query("SELECT * FROM note")
public abstract List<Note> getAllNotesSync();
public LiveData<List<Note>> getAllNotesWithShortBody() {
MutableLiveData<List<Note>> notesLiveData = new MutableLiveData<>();
//
// Problem 1: Still can cause OutOfMemoryException by loading
// List of notes with complete body string.
//
List<Note> notes = getAllNotesSync();
for (Note note : notes) {
String body = note.getBody();
// Extract first 256 characters from body string.
body = body.substring(0, Math.min(body.length(), 256));
note.setBody(body);
}
notesLiveData.postValue(notes);
//
// Problem 2: The returned LiveData unable to inform observers,
// if there's any changes made in the SQLite `note` table.
//
return notesLiveData;
}
}
I was wondering, is there any way to tell Room database Dao: Before returning List of Notes as LiveData, please perform transformation on every Note's body column, by trimming the string to maximum 256 characters?
Examining the source code generated by Room Dao
If we look at the source code generated by Room Dao
#Override
public LiveData<List<Note>> getAllNotes() {
final String _sql = "SELECT * FROM note";
final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
...
...
final String _tmpBody;
_tmpBody = _cursor.getString(_cursorIndexOfBody);
_tmpPlainNote.setBody(_tmpBody);
It will be great, if there is a way to supply transformation function during runtime, so that we can have
final String _tmpBody;
_tmpBody = transform_function(_cursor.getString(_cursorIndexOfBody));
_tmpPlainNote.setBody(_tmpBody);
p/s Please do not counter recommend Paging library at this moment, as some of our features require entire List of Notes (with trimmed body String) in memory.
You can use SUBSTR, one of SQLite's built-in functions.
You need a primary key in your #Entity. Assuming that you call it id, you can write a SQL like below.
#Query("SELECT id, SUBSTR(body, 0, 257) AS body FROM note")
public abstract LiveData<List<Note>> getAllNotes();
This will return the body trimmed to 256 chars.
With that being said, you should consider segmenting your rows. If you have too many rows, they will eventually use up your memory at some point. Using Paging is one way to do it. You can also use LIMIT and OFFSET to manually go through segments of rows.

android left join query with room

I am trying to change my sqlite database with room library. I am little confuse with left join query.
I have implemented it with sqlite, but don't know how can I achieve same withh room?
Here is my table creation:
first table: Notification
db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_NAME ($COLUMN_ID INTEGER PRIMARY KEY, $ICON TEXT, $TITLE INTEGER," +
" $DATE INTEGER, $TYPE INTEGER,$URL TEXT, $MESSAGE INTEGER, FOREIGN KEY($TITLE) REFERENCES ${TableNotificationsTrans.getTableName(this)}(id)," +
"FOREIGN KEY($MESSAGE) REFERENCES ${TableNotificationsTrans.getTableName(this)}(id))")
second table: Notification_Trans
db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_NAME ($COLUMN_ID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, $COLUMN_EN TEXT, $COLUMN_GU TEXT, $COLUMN_HI TEXT)")
What I am doing is I am storing notification in notification table but its name and description will be stored with specific language, in notification_trans.
Query to achieve
DatabaseHelper.database!!.rawQuery("SELECT A.$COLUMN_ID, A.$ICON, N.${language.toLowerCase()} $TITLE, A.$DATE, A.$TYPE, A.$URL, M.${language.toLowerCase()} $MESSAGE FROM $TABLE_NAME A LEFT JOIN NotificationsTrans N ON A.$TITLE = N.id LEFT JOIN NotificationsTrans M ON A.$MESSAGE = M.id ORDER BY $DATE DESC LIMIT $pageNum*10, 10", null)
Question
How can I achieve same with room?
Edit
My application is multi-language application, where I am getting notification title with specific language, like Hindi or Gujarati. I am storing notification details in notification table, while title in notification_trans.
NotificationTrans have column with id, english, hindi, gujarati.
When user asked for gujarati, I am retriving notification title from notificationTrans's column gujarati.
I am able do so, in sqlite.
But now I want it with Room
First You have to make the model classes for both, You may have already declared them , You just need to make a few changes if they already exists.
#Entity
public class Notification {
#PrimaryKey
int id;
String icon;
#ForeignKey(entity = Notification_Trans.class, parentColumns = "col_id", childColumns = "id")
String title;
int date;
int type;
String url;
int msg;
}
#Entity
public class Notification_Trans {
#PrimaryKey(autoGenerate = true)
int col_id;
String column_en;
String column_gu;
String column_hi;
This makes for your POJO, I couldn't understand your Foreign key constraints, so Pardon me for that, you can make changes as you see fit.
You can Declare your DAO as per this`
#Dao
public interface DAO {
#Query("SELECT note.id, note.title, note.description, category.name as categoryName " +
"FROM note " +
"LEFT JOIN category ON note.category_id = category.id")
List getCategoryNotes();
}
`
I haven't made changes in the query, which I found at Link here. As your query was a complex one, But, it'll give you an Idea about how to do that.,
After this, You just need to access your Dao interface from your Database class object, which will handle the creation & all other things room, like this one below`
#Database(entities = {Notification.class, NotificationTrans.class}, version = 3)
public abstract class AppDatabase extends RoomDatabase {
private static AppDatabase instance;
public static AppDatabase getAppDatabase(Context context) {
if (instance == null) {
instance =
Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "database_name")
// allow queries on the main thread.
// Don't do this on a real app! See PersistenceBasicSample for an example.
//.allowMainThreadQueries()
.fallbackToDestructiveMigration()
.build();
}
return instance;
}
public static void destroyInstance() {
instance = null;
}
public abstract Dao notificationDao();
It helps creating a separate class for Database, & keeping track of object from it.
& you can access your data with AppDatabase.getAppDatabase(context).notificationDao().yourQueryMethodName();
You may require to refer to this to understand the relations between room, & implement your requirement,
EDIT 1:
Here's how your DAO should look like ,`
#Insert
void insert(Notifications object);
//This will insert a single Item
#Insert
void insertAll(Notifications... objects);
While this can enter a list of Data,
You can call this methods with your Database object, likeAppDatabase.getAppDatabase(context).notificationDao().yourQueryMethodName() here instead of yourQueryMethod(), if you call insert() & pass the object you need to store in the database, It'll do it,
For E.g.db.parcelDao().insert(parcel);
this is how I insert Data in my ParcelDao, db is Database object, & parcel is the object of data need to be stored. One more thing, you can't call this method on main thread, so you may need to use Handler or AsyncTask for the purpose, Sorry I forgot to mention that.
Have a look at Room Training at Android Developers for implementation of basic functionality of room
#1 - Need to create a model class that matches with the result of the query
data class ClientAndCity(
#ColumnInfo(name="id") val id: Long,
#ColumnInfo(name="client_name") val clientName: String?,
#ColumnInfo(name="city_name") val cityName: String?
)
#2 - Inside your DAO create your query
#Query("SELECT clients.id, clients.name AS client_name, cities.name AS city_name FROM clients LEFT JOIN cities ON cities.id = clients.city_id WHERE clientes.id = :clientId")
fun getClientAndHisCity(clientId: Long): ClientAndCity?
#3 - Use you function
CoroutineScope(Dispatchers.IO).launch{
val result: ClientAndCity = clientDAO.getClientAndHisCity(clientId)
//do something with it
}

How to filter a nested relation in Room?

Let's take this example: I have a form, which has several sections, each having questions. Sideways, I have answers that are mapped to questions and they have another column that I want to filter on when querying:
So I have the following entities:
#Entity(tableName = "sections")
public class Section {
#PrimaryKey
public long id;
public String title;
}
#Entity(tableName = "questions")
public class Question {
#PrimaryKey
public long id;
public String title;
public long sectionId;
}
#Entity(tableName = "answers")
public class Answer {
#PrimaryKey
public long id;
public long questionId;
public int otherColumn;
}
In the section DAO I want to retrieve all of them.
Here's the POJO that I want filled by this query:
class SectionWithQuestions {
#Embedded
public Section section;
#Relation(parentColumn = "id", entityColumn = "sectionId", entity = Question.class)
public List<QuestionWithAnswer> questions;
public static class QuestionWithAnswer {
#Embedded
public Question question;
#Relation(parentColumn = "id", entityColumn = "questionId", entity = Answer.class)
List<Answer> answers;
}
}
In another application, the query would be:
SELECT s.*, q.*, a.*
FROM sections s
LEFT JOIN questions q ON q.sectionId = s.id
LEFT JOIN answers a ON a.questionId = q.id
WHERE s.id = :sectionId and a.otherColumn = :otherColumn
However in Room I have found out that if you want an object and their relations (like a user and its pets in the example), you only select the object, and the relations are queried in a second query. That would be:
#Query("SELECT * FROM sections WHERE id = :sectionId")
Then in the generated code there would be (pseudo code):
sql = "SELECT * FROM sections WHERE id = :sectionId" // what's inside #Query
cursor = query(sql)
int indexColumn1 = cursor.getColumnIndex(col1)
int indexColumn2
... etc
while (cursor.moveToNext) {
masterObject = new object()
masterObject.property1 = cursor.get(indexColumn1)
... etc
__fetchRelationshipXXXAsYYY(masterObject.relations) // fetch the child objects
}
and this __fetch XXX as YYY method is as follows:
sql = "SELECT field1, field2, ... FROM a WHERE foreignId IN (...)"
similar algo as previously: fetch column indices, and loop through the cursor
So basically it creates 2 queries: one for the master object and one for the relations. The 2nd query is automatically created and we have no control over it.
To get back to my problem where I want relations but also filter on the child column, I'm stuck:
in the 1st query I can't reference the otherColumn column because it doesn't exist
in the #Relation I can't either because the only properties of this annotation are the join column and entity definition
Is this possible in Room or do I have to make the subqueries myself?
Bonus question: why don't they join tables in a single query but create 2 queries instead? Is this for performance reasons?
Edit to clarify what I expected:
That's what I expected to write:
#Query("SELECT s.*, q.*, a.* " +
"FROM sections s " +
"LEFT JOIN questions q ON q.sectionId = s.id " +
"LEFT JOIN answers a ON a.questionId = q.id " +
"WHERE s.id = :sectionId and a.otherColumn = :additionalIntegerFilter")
SectionWithQuestionsAndAnswers fetchFullSectionData(long sectionId);
static class SectionWithQuestionsAndAnswers {
#Embedded Section section;
#Relation(parentColumn = "id", entityColumn = "sectionId", entity = Question.class)
List<QuestionWithAnswers> questions;
}
static class QuestionWithAnswers {
#Embedded Question question;
#Relation(parentColumn = "id", entityColumn = "questionId", entity = Answer.class)
Answer answer; // I already know that #Relation expects List<> or Set<> which is
// not useful if I know I have zero or one relation (ensured
// through unique keys)
}
That's pseudo code that I imagined to be implemented by Room as the generated code:
function fetchFullSectionData(long sectionId, long additionalIntegerFilter) {
query = prepare(sql); // from #Query
query.bindLong("sectionId", sectionId);
query.bindLong("additionalIntegerFilter", additionalIntegerFilter);
cursor = query.execute();
Section section = null;
long prevQuestionId = 0;
Question question = null;
while (cursor.hasNext()) {
if (section == null) {
section = new Section();
section.questions = new ArrayList<>();
section.field1 = cursor.get(col1); // etc for all fields
}
if (prevQuestionId != cursor.get(questionIdColId)) {
if (question != null) {
section.questions.add(question);
}
question = new Question();
question.fiedl1 = cursor.get(col1); // etc for all fields
prevQuestionId = question.id;
}
if (cursor.get(answerIdColId) != null) { // has answer
Answer answer = new Answer();
answer.field1 = cursor.get(col1); // etc for all fields
question.answer = answer;
}
}
if (section !=null && question != null) {
section.questions.add(question);
}
return section;
}
That's one query, and all my objects fetched.
I find Room Relations hard to work with, not very flexible and much of the work is done under the hood in a way that is hard to really be sure how.
In my projects, most of the time I just create presentation objects - objects dedicated for some UI presentation that can be filled with a custom select.
That way I have much more control over what I want to fetch from DB (i.e. what I really need), and I fill that into that custom presentation object.
I'm just pasting the information provided on the feature request I posted (see my comment on my question):
Hi there - we have recently released a new feature where relational query methods can be defined with Multimap return types. With this new feature, you should be able to achieve the results discussed in this thread. For more info on this new feature, you can check out the following resources:
Define relationships between objects: https://developer.android.com/training/data-storage/room/relationships
Relational Query Methods in ADS 2021: https://youtu.be/i5coKoVy1g4?t=344
The new MapInfo annotation: https://developer.android.com/reference/androidx/room/MapInfo
I know link-only answers aren't great, but I didn't have the opportunity to test this. If someone has a better answer, I'll accept it.
I found a better solution for this. Instead of aliasing all columns you can use #RawQuery annotation.
First of all, add a prefix for embedded table annotation using table name or its alias like #Embedded(prefix = "P.") or #Embedded(prefix = "Post."):
public class UserPost {
#Embedded
private User user;
#Embedded(prefix = "P.")
private Post post;
}
Then in your Dao, create a function to run a raw query, and create another function to run a raw query:
#Dao
public interface UserDao {
String USER_POST_QUERY = "SELECT U.*, P.* FROM User as U " +
"INNER JOIN Post as P ON U.id = P.userId " +
"WHERE P.status = 1";
#RawQuery
LiveData<List<UserPost>> rawQuery(SimpleSQLiteQuery query);
default LiveData<List<UserPost>> getAlertViolationsAsync() {
return rawQuery(new SimpleSQLiteQuery(USER_POST_QUERY));
}
}

Categories

Resources