LiveData and room using inheritance - android

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.

Related

How do I perform a Room DAO multi table join #Query using select fields?

My Problem:
I'm struggling to eliminate the compiling error on the following Room #Query statement in a Room DAO. As you can see, the SQLite query statement is joining various fields from different tables. The missing fields identified by the error are a part of the Notes class constructor identified in the List type for the method. I think I need to change the List type identified. If I'm right, I need some guidance/suggestion on how I should resolve it. Do I need to create a new Class and DAO with just those specific fields queried? Or maybe just a class since there is not table specific to these fields only. The error is:
error: The columns returned by the query does not have the fields [commentID,questionID,quoteID,termID,topicID,deleted] in com.mistywillow.researchdb.database.entities.Notes even though they are annotated as non-null or primitive. Columns returned by the query: [NoteID,SourceID,SourceType,Title,Summary]
List getNotesOnTopic(String topic);
#Query("SELECT n.NoteID, s.SourceID, s.SourceType, s.Title, c.Summary FROM Comments as c " +
"LEFT JOIN Notes as n ON n.CommentID = c.CommentID " +
"LEFT JOIN Sources as s ON n.SourceID = s.SourceID " +
"LEFT JOIN Topics as t ON n.TopicID = t.TopicID WHERE t.Topic = :topic AND n.Deleted = 0")
List<Notes> getNotesOnTopic(String topic);
What I'm trying to do:
I'm attempting to convert and existing Java desktop app with an embedded an SQLite database. The above query does work fine in that app. I only want to pass field data from these tables.
What I've tried:
I've done some googling and visited some forums for the last few days (e.g. Android Forum, Developer.Android.com) but most of the Room #Query examples are single table full field queries (e.g. "Select * From table"). Nothing I found yet (there is probably something) quite addresses how and what to do if you are joining and querying only specific fields across tables.
I think I may have fixed my issue. I just created a new class called SourceTable and designated the queried fields in the constructor. The only catch was I, according to a follow up error, was that the parameters had to match the field names.
public class SourcesTable {
private int NoteID;
private int SourceID;
private String SourceType;
private String Title;
private String Summary;
public SourcesTable(int NoteID, int SourceID, String SourceType, String Title, String Summary){
this.NoteID = NoteID;
this.SourceID = SourceID;
this.SourceType = SourceType;
this.Title = Title;
this.Summary = Summary;
}
}
and then I update my list method:
List<SourcesTable> getNotesOnTopic(String topic);

How to get specific column using Android Room

I'm trying to get id column from my database, ad it to ArrayList and to each id add "\t0",
My database is created using Room, i have a lot of column which one of them is
#PrimaryKey(autoGenerate = true)
private int id;
I am operating using ItemDAO and i have there function
#Query("SELECT * FROM item")
List<Item> getItems();
Which writes to ArrayList<Items> all of contents
I was thinking of running it trough the loop getting id and adding to ArrayList<String> but this doesn't seems to be eficient.
Your DAO:
#Query("SELECT Id FROM item")
List<Integer> getAllIds();
Your model:
#ColumnInfo(name = "Id")
#PrimaryKey(autoGenerate = true)
private int id;
In you query SELECT * FROM item * means select All, put there your column name and you will get list of objects from that column
Example: Select all items in id column SELECT id FROM item
I tried to modify and test #Valgaal 's solution. It turns out that Room can also return other type of values, more than just id (or integer).
For example, you can write an item class like this:
#Entity(tableName = Item.TABLE_NAME)
public class Item {
public static final String TABLE_NAME = "ItemsTable";
public static final String COL_DESC = "Description";
#PrimaryKey(autoGenerate = true)
private int id;
#ColumnInfo(name = COL_DESC)
private String description;
// getter & setter...
}
And then, you can write Dao like this:
#Dao
public interface ItemDao {
#Query("SELECT * FROM " + Item.TABLE_NAME)
List<Item> getItems();
#Query("SELECT " + Item.COL_DESC + " FROM " + Item.TABLE_NAME)
List<String> getItemDescriptions();
}
And it's functional as it should be.
I guess all of the other data types that Room can save (including custom types?) can be queried (and returned lists of specific column data) by the same logic above. Hope this would help someone in the future!
For returning multiple columns, create a pojo class that can be set as a return type for your DAO function
Note the select query should contain the Pojo class variable name (can be done via AS keyword)
Detailed answer here
https://stackoverflow.com/a/50802209/1029110
I landed on this question for my issue...but didnt find answer. So this may help others.

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 make many-to-many relation query in GreenDAO with source property other than primary key?

Let's assume we have following entities:
Item:
class Item {
...
#Index(unique=true)
private String guid;
...
#ToMany
#JoinEntity(entity = JoinItemsWithTags.class, sourceProperty = "itemGuid", targetProperty = "tagName")
private List<Tag> tagsWithThisItem;
...
}
Tag:
class Tag {
#Id
private Long localId;
#Index(unique = true)
private String name;
...
}
and we need to join them. Here is my join entity class:
#Entity(nameInDb = "item_tag_relations")
class JoinItemsWithTags {
#Id
private Long id;
private String itemGuid;
private String tagName;
...
}
I want to use tag name as a join property instead of Long id, because it's easier to support consistency when syncing with server.
But currently tags getter in Item class always return an empty list. I've looked into log and found generated query which using internally in that getter:
SELECT * <<-- there were a long sequence of fields
FROM "tags" T JOIN item_tag_relations J1
ON T."_id"=J1."TAG_NAME" <<-- here is the problem, must be `T."NAME"=J1."TAG_NAME"`
WHERE J1."ITEM_GUID"=?
So the problem is that join is base on tag's _id field. Generated List<Tag> _queryItem_TagsWithThisItem(String itemGuid) method implicitly uses that id to make a join:
// this `join` nethod is overloaded and pass tag's id as source property
queryBuilder.join(JoinItemsWithTags.class, JoinItemsWithTagsDao.Properties.TagName)
.where(JoinItemsWithTagsDao.Properties.ItemGuid.eq(itemGuid));
Correct approach is this case might be following, I suppose:
// source property is passed explicitly
queryBuilder.join(/* Desired first parameter -->> */ TagDao.Properties.Name,
JoinItemsWithTags.class, JoinItemsWithTagsDao.Properties.TagName)
.where(JoinItemsWithTagsDao.Properties.ItemGuid.eq(itemGuid));
But this code is in generated dao, and I don't know how to do anything with it. Is there any way to workaround this?

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