This question already has answers here:
Polymorphic entities in Room
(2 answers)
Closed 1 year ago.
TL;DR: How can I store and retrieve objects of type ClassA and ClassB which both inherit from ClassP in the same "list", using the Room Persistence Library?
In other words, How should I store a List<? extends BaseAnimal> in my Room DB?
I have Animals, for the purpose of this question.
public abstract class BaseAnimal {
#PrimaryKey(autoGenerate = true)
private long id;
public BaseAnimal(long id){this.id = id;}
public abstract void func(int param);
public long getId() {
return id;
}
}
An animal has an id and implements some function func. There will likely be many kinds of animals. To start with, I have an Elephant which additionally has a trunkLength property, and a Giraffe which additionally has a neckLength property.
public class Giraffe extends BaseAnimal {
public long neckLength;
public Giraffe(long id) {
super(id);
}
#Override
public void func(int param) {
}
}
I have several instances of Elephant and Giraffe in my application. How can I, using the Android Room Persistence Library, store and retrieve them sorted by BaseAnimal.id?
Expectations
At first, I hoped that it would be as simple as this:
Annotate the BaseAnimal class with #Entity(tableName = "base_animal_table")
Annotate my extension of the RoomDatabase with
#Database(entities = {BaseAnimal.class }, version = 1)
Add a function for inserting to the Data Access Object
#Insert
void insertAnimal(BaseAnimal animal);
Add a method to the Repository
public void insertAnimal(BaseAnimal animal){
new insertAnimalAsyncTask(recipeDAO).execute(animal);
}
private static class insertAnimalAsyncTask extends AsyncTask<BaseAnimal, Void, Void> {
private RecipeDAO mAsyncTaskDao;
insertAnimalAsyncTask(RecipeDAO dao) {
mAsyncTaskDao = dao;
}
#Override
protected Void doInBackground(final BaseAnimal... params) {
mAsyncTaskDao.insertAnimal(params[0]);
return null;
}
}
I expected that this would lead to room generating a table named base_animal_table which would look somewhat like this (with some sample data):
| id | ElephantTrunkLength | GiraffeNeckLength |
| 0 | NULL | 12 |
| 12 | 1337 | NULL |
I also expected that I could retrieve the data from this table something like this:
// this is in the DAO
#Query("SELECT * from `base_animal_table` ORDER BY id ASC")
LiveData<List<BaseAnimal>> getAllAnimals();
and then get a list that contains both, entities of type Elephant with their trunkLength property set, and entities of type Giraffe with their neckLength set.
This seems not to be straightforward. How can I implement this?
One approach I see is the following, but I am not sure whether it is the best way. And I am also starting to doubt that Room makes this any easier than using plain SQLite. I tried to produce a working example for this approach with room, but there are still some unresolved issues.
One Approach
Create a table base_animals which only contains the id and other base animal attributes, along with an indicator of their child type:
// base_animals
| id | animalType |
| 0 | "GIRAFFE" |
| 12 | "ELEPHANT" |
Use this table to keep a list of all animals and their ids. Create a table elephants and a table giraffes with the id and their specific properties (trunkLength and neckLength, respectively).
Then we can store an animal in the DB by creating an entry in both the base table and the child table.
To retrieve a specific animal by id, we can now find the animalType in the base_animals table and with that decide in which child table - elephants or giraffes we need to search.
The problem I have with this approach is that it requires that I have to write quite some code and I have to update it whenever I create a new animal entity - e.g. a Dolphin.
So, my question is: How should I store a List<? extends BaseAnimal> in my Room DB?
I would prefer it if I could leave BaseAnimal an abstract class.
I'm not sure that using inheritance is a good idea here.
I would suggest using composition instead.
Keep a class named Animal (not BaseAnimal).
The animal should have a type just like you suggested.
Another table (AnimalProperties) would hold the properties of each animal.
It will have a foreign key to the Animals table and a property name and property value.
Now...
If this is all that you need, meaning a class with type and properties then you're all set.
But if you also need the class to behave differently like implementing a fly() method for a bird and run() a dog then you should consider using Composition to add behaviors to the Animal.
If you are not familiar with Composition have a look here for a simple explanation.
Related
Please bear with me, i'm new to architecture components and android in general.
My question is similar to this question but unfortunately the accepted answer doesn't seem to work.
I have an example one to many relation like in this answer. My example database has two tables USERS and PETS as shown in the following images:
Let's say I want to get a list of users containing a list of their pets grouped by user id only with pets younger than 5.
The result should look like this (pseudo code):
{uId: 2, [Pet3, Pet4]; uId: 4, [Pet6, Pet7];}
Another requirement is that the Dao needs to return the list as a LiveData object because I'm using MVVM architecture and want it to be Lifecycle aware and observable.
With these requirements, the UserDao would look like this:
#Dao
interface UserDao {
#Insert
void insert(User user);
#Transaction
#Query("SELECT USERS.uId, PETS.pId , PETS.userId, PETS.age " +
"FROM USERS INNER JOIN PETS ON PETS.userId = USERS.uId " +
"WHERE PETS.age < 5 " +
"GROUP BY USERS.uId")
LiveData<List<UserWithPets>> getUserPets();
}
User Entity:
#Entity
public class User {
#PrimaryKey
public int id; // User id
}
Pet Entity:
#Entity
public class Pet {
#PrimaryKey
public int id; // Pet id
public int userId; // User id
public int age;
}
The problem is now: how should i design the UserWithPets that room understands it and maps the cursor the way i want?
Here is what i tried so far:
Approach 1:
The most convenient way in my opinion would be using a Relation, like in the POJO below.
UserWithPets POJO:
public class UserWithPets {
#Embedded
public User user;
#Relation(parentColumn = "id", entityColumn = "userId", entity = Pet.class)
public List<Pet> pets;
}
Unfortunately, the functionality to assign a condition to a relation is not yet implemented by google. So we always get a full list of pets for every user that owns a pet younger than 5. Hopefully this will be possible soon, since the feature request is already assigned here and here.
Statement from google from this feature request: "we are planning to implement some query rewriting logic to fix these, not for 2.1 but hopefully in 2.2 where we'll focus more on relations."
Approach 2:
Another option would be Embedding both, User and Pet like:
public class UserWithPets {
#Embedded
public User user;
#Embedded
public Pet pet;
}
This doesn't work either, because now we only get 1 pet per user.
Approach 3:
this answer suggests to just create a merged class that extends from user like:
public class UserWithPets extends User {
#Embedded(prefix = "PETS_")
List<Pet> pets = new ArrayList<>();
}
I tried in many ways, with contructor and without, but i can't get it to work. it always gives errors like "Entities and Pojos must have a usable public constructor. You can have an empty constructor or a constructor whose parameters match the fields (by name and type). - java.util.List"
or
The query returns some columns ... which are not used by UserWithPets. So any advice is welcome here.
Approach 4:
Just make two queries and stitch the results together. How would i do that using LiveData? Where should the joining operation be done? I can't do it in the Activity, that's not the point of an MVVM pattern. And not in the repository or viewmodel, since LiveData is immutable. Or is there another way?
What would be a working solution to get a result with the above requirements?
Fairly new to room and having a hard time finding info on this. Currently we define our database like this:
#Database(entities = {TwcLocation.class,
CurrentObservation.class,
Day.class,
Hour.class,
Station.class,
StationCurrentObservation.class}, version = 1, exportSchema = false)
public abstract class TwcLocationDatabase extends RoomDatabase {
Now, we want to remove a bunch of unused entities so it looks like this:
#Database(entities = {TwcLocation.class, Tag.class}, version = 1)
#TypeConverters({TwcLocationTypeConverter.class})
public abstract class NbcRoomDatabase extends RoomDatabase {
Question: How do I do this migration?
You need to increment the version (so it will become version = 2).
Next, when building the Room instance by using the Room.databaseBuilder method, add the addMigration() line.
As migration, pass it the following to remove the tables:
// Migration from version 1 to 2
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
#Override
public void migrate(SupportSQLiteDatabase database) {
// Remove the table
database.execSQL("DROP TABLE day"); // This line for each table that you want to remove
}
};
Later on you might need multiple different migrations, you can do so by using the addMigrations() method:
Room.databaseBuilder(..., ..., ...)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build()
Other option is to use fallbackToDestructiveMigration and increment database version. In such case you will not have to provide migrations. However it will clear data in all old tables.
I'm running into an issue with Realm, one to many relationship and updating existing entries to add more data to the relationship.
Model 1
public class Model1 extends RealmObject
{
#PrimaryKey
private String identifier;
#SerializedName("type")
private String type;
#SerializedName("additionalInfo")
private String additionalInfo;
#SerializedName("options")
private RealmList<Model2> moreModels;
}
Model 2
public class Model2 extends RealmObject
{
#SerializedName("hint")
private String hint;
#SerializedName("label")
private String label;
#SerializedName("favorite")
private boolean favorite;
#PrimaryKey
private String identifier;
}
So from an API I'm getting a list of Model1 objects, and each of those objects contains their own list of Model2 objects. Simple enough. This works well, I can add it to Realm and see the relationship.
Now my problem emerges when I make a second API call for a different user. The way I was hoping to have this work was to have the identifier property on Model2 be made up of userId + label. Therefore, each user will have their own set of Model2 objects. However, I was hoping to have only one set of Model1 objects, where its reference to Model2 objects gets updated as more are added to the Model2 table.
Instead what I got working is I keep my one set of Model1 (good), but the relationship to Model2 always gets overwritten with the latest set. So I have a table of Model2 objects that has 80 entries, but only the last 40 are connected.
So it looks like my update or insert is updating the Model1 entries in the table (GOOD) but instead of concatenating the existing relationships with the new ones, its just updating the relationships to only use the new ones. The Model2 entries are being added to their table correctly in that there are not duplicates based on the primary key. But the connection is broken.
Update Code
List<Model1> test = response.body();
try(Realm realmInstance = Realm.getDefaultInstance()) {
realmInstance.executeTransaction((realm) ->
{
realm.insertOrUpdate(test);
final RealmResults<Model> category = realm.where(Model1).findAll();
}
);
}
Yes, that is how the insertOrUpdate and copyToRealmOrUpdate methods works currently. Ideally, there would be a mode you could provide like REPLACE_LISTS or MERGE_LISTS, but right now that hasn't been implemented.
Your only choice is manually updating these relationships.
So basically i am using room and trying to add migration from database version 1 to 2 but my alter command is not working
My current implementation is below :
void init() {
db = Room.databaseBuilder(Global.getInstance(),
AppDatabase.class, "feed").addMigrations(MIGRATION_1_2).build();
}
Migration property :
static final Migration MIGRATION_1_2 = new Migration(1,2) {
#Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE 'post' ADD COLUMN 'age' INTEGER NOT NULL DEFAULT 0");
Log.d("VROM","Migration");
}
};
Database implementation :
#Database(entities = {Feed.class, DownloadModel.class}, version = 1) public abstract class AppDatabase extends RoomDatabase {
public abstract DaoAccess getFeedDao();}
So after incrementing the version from 1 to 2, the execSQL() is executed but new column is not added in my db.
I have pulled my db from app directory and checked multiple times but column is not there. Apart from that if I kill my app and launch it again the migrate method is called again , don't know if this is the intended functionality but it breaks the functionality for me.I thought migrate will be only called once same as onUpgrade()
Make sure your column is in model class. In your case, you are adding column age like this: ADD COLUMN 'age' INTEGER, so you must have int age in your model class.
Also, it is a good idea to write migration test to known exactly what is failing. You can find about migration test in android documentation here: https://developer.android.com/topic/libraries/architecture/room.html#db-migration-testing
Try Changing version=2 in AppDatabase inside #Database.
In this example, the docs talked about getting the parent objects while specifying queries for the child objects.
Is there a way for getting the child objects while specifying a query for the parent object?
In the given example, can I search for dogs who are of brown color with the user named John?
EDIT: Since Realm 3.5.0, you can actually use the "backlinks" mentioned in the comment section. Rejoice!
In fact, since Realm 3.0.0, bidirectional links are a performance bottleneck, so using backlinks is the preferred way.
The way it works is:
public class User extends RealmObject {
private RealmList<Dog> dogs;
}
public class Dog extends RealmObject {
#LinkingObjects("dogs")
private final RealmResults<User> owners = null;
}
Now you can do:
realm.where(Dog.class).equalTo("color", "Brown").equalTo("owners.name", "John").findAll();
OLD ANSWER:
You can only search for dogs with a given user if you have an object link to the User.
public class Dog extends RealmObject {
//...
private User user;
}
Then you could do
realm.where(Dog.class).equalTo("color", "Brown").equalTo("user.name", "John").findAll();