I'm trying to update my Android knowledge with the new Android Architecture Components. I'm looking at how to persist data with Room, but it seems like it is practically unusable if you actually have relational data and I'm hoping you can help me understand something I've missed or confirm that Room really doesn't work for any kind of serious relational data.
Here's my problem in as small a nutshell as I can offer.
I've got an app for animal breeders and I have an Animal entity. Let's say I have a Weight entity which is a record of how much an animal weighed on a given date. An Animal has a to-many relationship to Weight to create a history of how much they weighed over time.
According to the Room training documentation, I would need to create a 3rd class called AnimalWithWeights like:
data class AnimalWithWeights(
#Embedded val Animal: Animal,
#Relation(
parentColumn = "AnimalId",
entityColumn = "AnimalId"
)
val Weights: List<Weight>
)
and add a function to my DAO like fun getAnimalsWithWeights(): List<AnimalWithWeights>.
#Transaction
#Query("SELECT * FROM Animal")
fun getAnimalsWithWeights(): List<AnimalWithWeights>
But I don't have just one relationship.
My Animal entity has relationships to other Animals like father, mother, surrogate mother, breeding parter, and relationships to other entity types like Breed, Breeder, Buyer, DNA, and to-many relationships for Weights, Notes, MedicalTreatments, MatingRecords, Ultrasounds, etc.
Unless I'm missing something, it sounds like I need to create a class that is AnimalWithWeights and then wrap that in a 4th class called AnimalWithWeightsAndNotes
and wrap that in a 5th class called AnimalWithWeightsAndNotesAndMedicalTreatments
and wrap that in a 6th class called AnimalWithWeightsAndNotesAndMedicalTreatmentsAndMatingRecords
and wrap that in a 7th class called AnimalWithWeightsAndNotesAndMedicalTreatmentsAndMatingRecordsAndUltrasounds...
and I haven't even gotten to ...AndFatherAndMotherAndSurrogateMotherAndBreedingParterAndBreedAndBreederAndBuyerAndDNA....
I'm currently using ORMLite to handle my sqlite persistence and it handles relational data well, but it seems like Room is only really designed to handle flat data.
Am I missing something with Room?
I'd like to be able to use some of the new features like LiveData<> so that I can get automatic callbacks when my data is updated and so that's one of the reasons I was looking into Room, but it just seems too rudimentary to be able to handle a realistic relational data model.
It's not clear from your post whether you're aware of possibility to link several #Relations in mentioned auxilary 3rd class. With Animal having one-to-many and one-to-one relations to other entities technically you need add just one final class AnimalWithAllStuff:
data class AnimalWithAllStuff(
#Embedded val Animal: Animal,
#Relation(
parentColumn = "AnimalId",
entityColumn = "AnimalId"
)
val weights: List<Weight>,
#Relation(
parentColumn = "AnimalId",
entityColumn = "AnimalId"
)
val notes: List<Notes>,
....
#Relation(
parentColumn = "fatherId",
entityColumn = "AnimalId"
)
val father: Animal,
#Relation(
parentColumn = "motherId",
entityColumn = "AnimalId"
)
val mother: Animal,
....
)
Technically on each #Relation Room internally invokes separate query and if you use LiveData Room sets tracker to notify observer on any change in Weight, Notes and so on.
I doubt though that with that structure you can get, for example, father's and mother's weight, notes and so on.
Related
I have a structure as follows:
A Conversation that has one or more Messages.
Conversation and Message are each in their own tables.
I created a ConversationDetailed relation POJO merging the Conversation with a list of its messages.
public class ConversationDetailed {
#Embedded
public Conversation conversation;
#Relation(
parentColumn = "id",
entityColumn = "conversation_id",
entity = Message.class
)
public List<Message> messageList;
}
So far this works as expected and found no issues.
Now I am trying to create a ConversationSummary relation where there is only the most recent message belonging to that conversation and I cannot find an example of how to do this anywhere. This is what I am trying to achieve:
public class ConversationSummary {
#Embedded
public Conversation conversation;
#Relation(
parentColumn = "id",
entityColumn = "conversation_id",
entity = Message.class
)
public Message recentMessage; // <- the most recent message of this conversation
}
I know I can use the ConversationDetailed and just pick the most recent message from the list, but that means querying from the database a lot of messages without any need, when all I want is just the most recent one.
How can I achieve this using a Relation POJO like the example above?
EDIT: After trying the initial answer suggested by another user I discovered that relations are separate queries, that is the reason why queries with Relation POJOs use Transaction, so the question remains unanswered. Is there a way to specify the order of the relation item?
EDIT2: After a detailed explanation by the same user that provided the first answer I was finally able to make this work. Here's the rationale that worked for my case.
A combination of MAX with JOIN allows me to select the select the conversation with the most recent message out of all, including out of all conversations unrelated to that message.
Adding GROUP BY for the conversation id split the above logic to get the MAX for each unique conversation id.
After that it was a matter of applying the ORDER BY to the message creation date.
#Query(
"SELECT " +
"*, MAX(Message.date)" +
"FROM Conversation " +
"JOIN Message " +
"ON Conversation.id = Message.conversation_id " +
"GROUP BY Conversation.id " +
"ORDER BY Message.date DESC "
)
abstract List<ConversationSummary> getSummaryList();
Try next workaround (since I don't know straight method to do what you want only with Relations):
Write a query to get all recent messages within conversation. Let's say you have some field posted that you can use to define the message was recent (in other words you should find detailed conversation with maximal posted).
Use Room Relations to attach Conversation entity to query's result.
So, additional class (query should return List of that Class values):
public class RecentMessage {
#Embedded
public Message recentMessage;
#Relation(
parentColumn = "conversation_id",
entityColumn = "id"
)
public Conversation conversation;
}
And query:
SELECT * FROM conversation_detailed as table1 JOIN (select conversation_id, max(posted) as posted from conversation_detailed) as table2 on table1.conversation_id=table2.conversation_id and table1.posted = table2.posted
It seems that #Relation has never been designed to take into account sorting from custom #Query. This has been confirmed in the issue related to this problem located here: https://issuetracker.google.com/issues/128470264
The second best alternative is the to use multiple queries and build the returned data structure manually, as per the suggestion in the linked issue.
For my case it will be a JOIN between both tables, grouped by the conversation id and ordered by newest message first.
Wanted to share an alternative solution, one I applied to my own same issue. You could instead save a reference to the latest Message in Conversation, making sure to write that correctly, and then use that in your Relation. For example, you could add "recent_message_id" to Conversation, and then use that in relation to "message_id", targeting that specific message. This assumes there's a "message_id" in Message, of course.
public class ConversationSummary {
#Embedded
public Conversation conversation;
#Relation(
parentColumn = "recent_message_id",
entityColumn = "message_id",
entity = Message.class
)
public Message recentMessage;
}
#Entity(tableName="user_table")
data class User(
#PriamryKey(autoGenerate = false) val id:Int,
#Embeded(prefix="address_") val address:Address
)
#Entity(tableName = "address_table")
data class Address(
#PrimaryKey(autoGenerate = false) val id:Int
)
Is there a way to just ignore the Id column from Address table because from my knowledge I will be getting
columns id, address_id in the user object once created
I have similar columns here and there and some are no longer in use once I create views for these tables i.e foreign keys etc
Is there a way to just ignore the Id column from Address.
I don't believe so. Assuming that Address has other fields (otherwise simply don't Embed) then to Embed an Entity with an Entity is de-normalising the database.
If you have decided that you don't need to store the addresses in a seperate table as there is effectively a 1-1 relationship then you should do away with the address table, which could/would be converting the Address class to not be an Entity and remove the id field.
If you were to keep the Address class as an Entity then you cannot #Ignore the #PrimaryKey as Room requires that an Entity has an #PrimaryKey. Really, in this situation of having another table for the address, you should reference/map the address to not de-normalise and thus just have a field in the user_table for that reference/mapping rather than Embed. You would/could then have a non-entity class (POJO) for extracting data that would use Embed both Entities or Embed one and Relate the other.
I'm using a Room table that takes an entity that has a "Favorited" boolean field, which is by default False, but can be made True through user input. The database needs to be wiped periodically and repopulated by a network call. Doing so naturally reverts all "Favorited" fields to False.
Using SharedPreferences, I can keep a second, persistent list of Favorited entities, but what's the cleanest way to repopulate Room and hold on to the favorites?
Using RxKotlin here for clarity, if not accuracy,
should it be something like
saveEntities()
.flatMapIterable { it }
.map{
if (sharedPrefs.contains(it.id))
it.apply{favorite}
else it }
.toList()
.subscribe()
Or should I serialize a list of favorites, save that to SharedPreferences, and then
val favoritesList = PrefSerializer().getFavorites
saveEntities()
.map{ it.forEach{
if (favoritesList.contains(it))
it.apply{favorite}
else it}
}
.subscribe()
Or does it even make sense to store Favorite information in Room?
Referencing SharedPreferences every time an entity is called feels like bad practice.
I would recommend using 2 tables in Room because that way you can use a database view to combine them. Room can monitor the tables and notify the UI via LiveData. You can technically monitor SharedPreferences, but I believe it is in fact more tedious.
For example, this is a table where data is periodically overwritten by sync.
#Entity
data class Cheese(
#PrimaryKey
val id: Long,
val name: String
)
And you have a separate table to hold the "favorite" status.
#Entity
data class CheeseFavorite(
#PrimaryKey
val id: Long,
val favorite: Boolean
)
Then you can create a database view like this to combine 2 tables.
#DatabaseView(
"""
SELECT c.id, c.name, f.favorite
FROM Cheese AS c
LEFT OUTER JOIN CheeseFavorite AS f
ON c.id = f.id
"""
)
data class CheeseDetail(
val id: Long,
val name: String,
val favorite: Boolean?
)
The view can be queried just like a table. You can of course use LiveData or Rx data types as the return type.
#Query("SELECT * FROM CheeseDetail")
fun all(): List<CheeseDetail>
In an app I'm working on, I need to very simply fetch data from a Room database. There are relations involved. Everything was working perfectly with simple queries returning LiveData, however, the size of the data that needs to be retrieved grew much larger than expected and it also contains blobs (images), which can make the query very slow. I've resolved to using the Paging library, implemented as follows, but for some reason the #relation annotation simply doesn't work anymore.
The entity fetched is a DTO, looks basically like this:
data class EntityOtherAnotherDTO(
var id: Long? = null,
var name: String? = null,
...,
#Relation(parentColumn = "id", entityColumn = "entity_id", entity = OtherEntity::class)
var others: List<OtherEntity>,
#Relation(parentColumn = "id", entityColumn = "entity_id", entity = AnotherEntity::class)
var anothers: List<AnotherEntity>
)
The query:
#Query("SELECT * FROM other
JOIN entity ON entity.id = other.entity_id
JOIN another ON entity.id = another.entity_id
WHERE entity.deleted = 0
ORDER BY
CASE WHEN other.some_column IS NULL THEN 1 ELSE 0 END,
other.some_column ASC,
entity.some_other_column DESC")
fun getAllEntityOtherAnotherDTOs(): DataSource.Factory<Int, EntityOtherAnotherDTO>
When the query was like this: fun getAllEntityOtherAnotherDTOs(): LiveData<List<EntityOtherAnotherDTO>> everything worked just fine. The results were ordered as required and all data was fetched, including the lists annotated with #relation. But after changing the return type to DataSource.Factory and of course implementing a paging adapter, the relations return empty.
The ordering still works perfectly, the query appears to be working exactly as before, the paging also works, but the data is simply missing. All the columns in the entity table are there (name, some_other_column etc.), the relations are the only but major problem.
(I can provide further details about the paging implementation, if that's of any relevance.)
Turns out this is an issue in Room which can happen even when you don't use the Paging library, given a large-ish (hundreds of results+) query.
There is no solution, but there is a workaround for 1:1 relations: using #embedded instead of #relation. That can however complicate things through the need of setting a prefix and enumerating all columns in the query and giving them aliases. That's pain but such is life. Room sucks.
Or if the joined entity doesn't have too many columns and there aren't any duplicate names, it works just as fine to copy those columns/properties in the DTO that is returned by the query.
Let's say I have an Order and a User Entity that resembles something like this:
User.java:
#Entity
public class User {
#PrimaryKey
public long id;
#NonNull
public String username;
}
Order.java:
#Entity(foreignKeys = #ForeignKey(entity = User.class,
parentColumns = "id",
childColumns = "user_id",
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE),
indices = #Index("user_id"))
public class Order {
#PrimaryKey
public long id;
public String description;
#ColumnInfo(name = "user_id")
public long userId;
#Ignore
private User user;
}
So this way, I have an Order with a User inside it, on the database I am only saving this relation with the user id as a Foreign Key and not the object itself (it's #Ignored), so everything is nice and relational and can be expanded to be used with other types in several different ways and queries and etc.
Now, the question is, how do I get an Order with the User object automatically populated by Room inside it?
If I use #Embedded, then Order and User will live on the same table, which is not good for relational separation of the types. I could return them both together with a JOIN but still this only works for data types that have different names (maybe they both have a column named "description"?), besides, inserting an Order with a User inside it wont be simple with a JOIN. #Relation only works for one-to-many and it needs a List, which is not my case.
I thought that maybe a #TypeConverter would be the best option here, converting between long and User, but this also is tricky. The #TypeConverter would need a reference to the DAO so it could query the User and the #TypeConverter is a static method, invoked by Room, so passing the DAO can be tricky and lead to many code smells, besides this extra query for each User would trigger multiple searches that won't be in the same #Transaction.
I am new to Room, but I bet there's a proper way to fix this, to use Room with relational types as it was intended to be used, I just can't find out how to make this work simply and nicely nor I can find it in any documentations.
Now, the question is, how do I get an Order with the User object automatically populated by Room inside it?
I think there is not an out of box solution for this.
IMHO, the entity model (i.e. the Order mapping a FK with User.id) should not be propagated to upper layer like domain/presentation layer, providing a DataMapper to transform the entity model to a domain model (i.e. a User contains an Order) for the upper layer may be a better option.