SQLDelight and data classes with one to many relations in Kotlin - android

I am trying to create a small app using Kotlin Multiplatform where i can save book titles and their authors but i'm struggling with the data classes and how to map them together so that i get the author with all of their books and the publish date.
CREATE TABLE book(
id INTEGER NOT NULL,
name TEXT NOT NULL,
publishDate INTEGER NOT NULL,
authorId INTEGER NOT NULL
)
CREATE TABLE author(
id INTEGER NOT NULL,
name TEXT NOT NULL
)
Here are my data classes:
#Serializable
data class bookEntity(
id: Int,
name: String,
authorId: Int
)
#Serializable
data class authorEntity(
id: Int,
authorName: String
books: List<bookEntity>
)
and my Query:
selectAuthors:
SELECT * FROM author
JOIN book ON book.authorId = author.id
WHERE book.authorId = author.id
i tried the following mapping but it didn't work:
private fun mapAuthor(
id: Int,
authorName: String,
bookId: String,
name: String,
publishDate: Long
): Author(
return Author (
id = id,
authorName = authorName,
book = List<BookEntity>(
id = bookId,
name = name,
publishDate = publishDate
)
)
)
How can i work with lists like this?
Every help is appreciated!

The ON clause of the JOIN is the condition which links both tables. You don't need to repeat the condition in the WHEN clause. Use WHEN to further narrow down the query, for instance if you're searching for a specific author name.
SELECT * FROM author
JOIN book ON book.authorId = author.id
WHERE author.name LIKE 'John%';
If you want to query all authors, just remove the WHEN completely.
Also, you don't need to create data classes and do the mapping yourself. SQLDelight already creates data classes for your queries.

Related

Unable to get correct values in Room by using #Relation

I have three tables in my Room database, my query should return a list of products that contains details about them.
All products are inserted based on a documentId so when the query is done I need to get in the list only the items for that documentId.
The DAO looks like this:
#Transaction
#Query("SELECT * FROM document_products dp LEFT JOIN products ON products.id = dp.productId LEFT JOIN products_barcodes ON products_barcodes.barcode = dp.productId WHERE dp.documentId = :documentId AND (products_barcodes.barcode = dp.productId OR products.id = dp.productId) ORDER BY timestamp ASC")
fun getProductsWithDetails(documentId: Long): Flow<List<DocumentProductWithDetails>>
And if I test the query in a table like this:
Where documentId is 5 the query returns the correct values:
But those values are incorrect in the application probably cause of #Relation in DocumentProductWithDetails but I'm unable to find the issue, in facts inside the application the data is shown as this:
So as the item with productId is saved three times it is showing the last item instead of the one related to documentId
The data class which contains #Relation annotation looks like this:
#JsonClass(generateAdapter = true)
data class DocumentProductWithDetails(
#Relation(
parentColumn = "id",
entityColumn = "productId"
)
var product: DocumentProduct,
#Embedded
var details: ProductWithBarcodes?
)
Where DocumentProduct and ProductWithBarcodes:
data class DocumentProduct(
#PrimaryKey(autoGenerate = true)
var id: Long,
var productId: String,
var quantity: Float,
var orderQuantity: Float,
var purchase: Float,
var documentId: Long,
var labelType: String?,
var timestamp: Long?
)
data class ProductWithBarcodes(
#Embedded
var product: Product,
#Relation(
parentColumn = "id",
entityColumn = "productId"
)
var barcodes: List<Barcode>
)
So as the item with productId is saved three times it is showing the last item instead of the one related to documentId
IF any columns of the query have the same column name, the values assigned may(will be) inconsistent in that the value of the last column, of the repeated name, will be the value assigned to all the other columns that have the same name (etc).
I would suggest that the fix is to use unique column names. e.g. instead of productId in the Barcode that you use a column name more indicative of the use of the value, it could be considered as a map to the product so perhaps barcode_productIdMap.
it is not the query that is at fault but how Room handles retrieving and assigning values.
Consider your second image (with some marking):-
The above is explained in more detail, with examples, in this answer
Which is the correct id, productId, quantity (Rhetorical).
How is Room supposed to know what goes where? (Rhetorical)
Consider that the following query extracts exactly the same data (but with the data columns in a different sequence):-
#Query("SELECT products.*,products_barcodes.*,document_products.* FROM document_products dp LEFT JOIN products ON products.id = ....
How is Room meant to cope with the different order (should the correct productId be the first or the second (Rhetorical)).
with unique column names the column order is irrelevant as Room then knows exactly what column is associated with what field, there is no ambiguity.
with unique columns tablename.columname can be reduced to just the column name in the SQL, so the SQL can be simplified.

Limited observability with observe/observeAsState with SQL alias fields

When using Room with LiveData, ViewModel, and JetPack Compose I have the following entity:
#Entity(tableName = "Item")
data class FitItem(val name: String,
val goal: Int,
val type: Int) {
#ColumnInfo(name = "id")
#PrimaryKey(autoGenerate = true)
var id: Int = 0
// This field will be created in the table, although it is actually calculated with SUM()
// in runtime, it is defined in the data object so it can be fetched
var amount: Int = 0
}
The variable amount is part of the entity, and is created as a column even though it's not needed in the table scheme. It's created with the value of zero, and does not change.
In the DAO a join query is defined where you get all the items and their amount (which is retrieved from a different table)
SELECT
item.id,
item.name,
item.goal,
item.type,
SUM(table2.amount) as amount
...
FROM table1 (AS) t1
JOIN table2 (AS) t2
ON item.id = table2.item_id
The Item Entity requires the amount field, for Room to return that field.
When data in inserted to table2, which effects the result of the query above, the observable is not emitting, not sure if it's the JOIN that is not monitored or the alias field (amount) which the actual field in the Item table is always zero does not change.
This results in missed updates in real time, is there a way to address the issue?

Deconflict column names in "join" entity

I have 2 entities:
#Entity(tableName = "author")
data class Author(
#PrimaryKey
#ColumnInfo(name = "id")
val id: String,
#ColumnInfo(name = "name")
val name: String
)
data class Book(
#ColumnInfo(name = "id")
val id: String,
#ColumnInfo(name = "title")
val title: String,
#ColumnInfo(name = "author_id")
var authorId: String
)
And I would like to join them in a query:
#Query("SELECT * FROM book JOIN author ON author.id = book.author_id AND author.id = :authorId WHERE book.id = :bookId")
fun item(authorId: String, bookId: String): LiveData<BookWithAuthor>
Into this entity:
#Entity
data class BookWithAuthor(
#Relation(parentColumn = "author_id", entityColumn = "id")
val author: Author,
#Embedded
val book: Book
)
However when I do that I get back a BookWithAuthor object in which the author.id and book.id are the same id, in this case they are both the author's id. How do I deconflict the "id" property in the entities in the "join" object?
You can use the #Embedded's prefix to disambiguate the names.
e.g. use :-
#Embedded(prefix="book_")
val book: Book
along with :-
#Query("SELECT author.*, book.id AS book_id, book.title AS book_title, book.author_id AS book_author_id FROM book JOIN author ON author.id = book.author_id AND author.id = :authorId WHERE book.id = :bookId")
Note the above is in-principle code, it has not been tested or run.
You would then change BookWithAuthor to use the prefixed column so :-
#Entity /* not an Entity i.e. Entity = table this just needs to be a POJO */
data class BookWithAuthor(
#Embedded(prefix = "book_")
val book: Book,
/* with (makes more sense to have parent followed by child) */
#Relation(/*entity = Author::class,*/ parentColumn = "book_author_id", entityColumn = "id")
val author: Author
)
However, your comment it assumes that all book ids are unique. In my case I could potentially have duplicate book ids for different authors.
appears to not fit in with the table/entities (schema) you have coded. i.e.
Author entity is fine.
Book though does not have #Entity annotation, nor does it have the obligatory #PrimaryKey annotation if it is an Entity defined in the entities=[....] lis. The assumption made is that the id is/would be the primary key and annotated accordingly and thus unique.
BookWithAuthor You will see that BookWithAuthor has been commented with Not an Entity (table). You cannot have the #Relationship annotation in an Entity that is defined as an Entity to the database (i.e. one of the classes in the entities=[....] list of the #Database annotation).
So unless the primary key of the Book entity/table is not the id or that the authorid is a list of authors then a Book can have only one author. As such it would appear that you only need #Query("SELECT * FROM book WHERE id=:bookId"): LiveData<BookWithAuthor>
if not then coding #Relation will basically ignore your JOIN and select ALL authors but then only pick the first which would be an arbitrary author to complete the author. That is #Relation works by obtaining the parent(s) and then build it's own underlying query to access ALL children of the parent. So whatever Query you supply it ONLY uses this to ascertain the parents.
I suspect that what you want is that a book can have a number of authors and that an author can be an author of a number of books. In this scenario you would typically use a mapping table (can be called other names such as link, reference, associative .... ). If this is the case and you can't ascertain how to create the mapping table via room, then you could ask another question in that regard.
I think the problem here is defining the relation.
My understanding is this is a one to many relationship: One Author (parent) has zero or more Books (entities).
What your #Relation defines is a 1:1 relationship.
If what you want eventually is a BookWithAuthor, why not embedd the Author in Book directly? You would then have the following tables:
#Entity(tableName = "author")
data class Author(
#PrimaryKey
#ColumnInfo(name = "author_id")
val id: String,
#ColumnInfo(name = "name")
val name: String
)
#Entity(tableName = "BookWithAuthor")
data class Book(
#PrimaryKey
#ColumnInfo(name = "id")
val id: String,
#ColumnInfo(name = "book_id")
val id: String,
#ColumnInfo(name = "title")
val title: String,
#Embedded
val author: Author
)
And your query can look like this:
#Query("SELECT * FROM BookWithAuthor WHERE book_id = :bookId AND author_id = :authorId")
fun item(authorId: String, bookId: String): LiveData<BookWithAuthor>
After embedding, Book takes the same columns of Author with their exact names. So we need to at least rename either of the id columns to resolve ambuiguity.
Since book ids can be duplicate, we need to introduce a new column as the PrimaryKey for Book.

RoomDB - JOIN query for which the result is a list of data types with a field whose value is a multiplication of 2 tables fields

Let's say I have the following entities for my RoomDB:
enum class Type {
FOO,
BAR
}
#Entity(
tableName = "amount",
primaryKeys = ["id", "type"]
)
data class Amount(
val id: Long,
val type: Type,
val amount: Int
)
#Entity(
tableName = "value",
primaryKeys = ["id", "valueType"]
)
data class Value(
val id: Long,
val valueType: Type,
val value: Int
)
What I want to do is somehow, with a SQL query (or RoomDB annotations ideally...) query the amount table, and join each amount with the corresponding value row(s) from the value table (using the type from amount to cross reference the valueType field on the value table) by multiplying the amount field with the value field to end up with an object like this:
data class ValueOfAmount(
val type: Type,
val valueOfAmount: Int // amount * value
)
I can think of a way to do this, but it requires doing some of the JOIN "logic" in my repo code layer, when I'd prefer to do this at the query layer instead (if at all possible).
Create a joined data class like so:
data class AmountWithValue(
#Embedded
val amount: Amount,
#Relation(
parentColumn = "type",
entityColumn = "valueType"
)
val value: Value
)
Expose a function from my dao to retrieve the joined data:
#Query("SELECT * from amount")
suspend fun getAmountsWithValues() : List<AmountWithValue>
Consume this function, and map the results to ValueOfAmount instances like so:
val valueOfAmounts = dao.getAmountsWithValues().map { amountWithValue ->
ValueOfAmount(
amountWithValue.amount.type,
amountWithValue.amount.amount * amountWithValue.value.value
)
}
// Do stuff with value of amounts
What I'd like to know is if there is some way to encode that mapping code into the QUERY itself (either via SQL or, even better, if RoomDB has some annotations that support this kind of complex query as annotations on my data types - similar to how it let's me define relationships for simple JOIN operations).
I believe that the following may be what you want:-
First a Class to be joined :-
data class AmountWithCalculatedValue(
#Embedded
val amount: Amount,
val calculatedValue: Int
)
And then a Dao :-
#Query("SELECT *,(amount * value) AS calculatedValue FROM amount JOIN value ON amount.type = valueType")
fun getAmountWithCalculatedValue(): List<AmountWithCalculatedValue>
if you wanted the Value as well it's a little more complicated due to duplicate/ambiguous columns but you could use:-
data class AmountWithCalculatedValue(
#Embedded
val amount: Amount,
val calculatedValue: Int,
#Embedded(prefix = "_value_")
val value: Value
)
With :-
#Query("SELECT amount.id, amount.amount, amount.type,value.id AS _value_id, value.id AS _value_value, value.valueType AS _value_valueType,(amount * value) AS calculatedValue FROM amount JOIN value ON amount.type = valueType")
fun getAmountWithCalculatedValue(): List<AmountWithCalculatedValue>
or :-
#Query("SELECT amount.*,value.id AS _value_id, value.id AS _value_value, value.valueType AS _value_valueType,(amount * value) AS calculatedValue FROM amount JOIN value ON amount.type = valueType")
fun getAmountWithCalculatedValue(): List<AmountWithCalculatedValue>
that is using the #Embedded's prefix parameter is saying that the values will be prefixed with the prefixed with the prefix so you have to us AS clauses to disambiguate the respective columns. As no prefix is used on the amount columns then amount.* can be used (2nd dao).
I'd say that for 1-1 relationships using #Relationship (as opposed to #Embedded) is perhaps a little less efficient than using a JOIN. As the way Room works it gets the Parent's from the given query and then retrieves the #Relation from the Parent via a separate query and hence why it recommends #Transaction.

How to store a list of integers in Room database so that it can be easily queried later?

I'm storing podcast data in a Room database, where each podcast has a List<Int> called genreIds. I'd like to be able to store this in such a way that I can easily query it later by doing something like SELECT * FROM podcasts WHERE genreIds CONTAINS :genre, or whatever the command would be.
So what is the best way to store that list of Ints so that it can be easily queried later, and how would I do that using Room? I've used TypeConverters before, but that converts it to a string, which is difficult to query, so I'd like to be able to link it to another table or something that can be easily queried, I'm just not sure how to do that.
Thanks in advance.
The data stored on a the db with Room, depends on the data class you use. If you specify a data class with an Int member, that will be an Int on the db.
Example:
data class TrackPlayedDB (
#PrimaryKey(autoGenerate = true)
val _id: Int = 0,
val timesPlayed: Int,
val artist: String,
val trackTitle: String,
val playDateTime: LocalDateTime
)
here timesPlayed will be an Int on the DB (as _id). You'll specify your data classes like the following, this will build the corresponding tables.
#Database(entities = [TrackPlayedDB::class], version = 1, exportSchema = false)
#TypeConverters(Converters::class)
abstract class MyRoomDatabase : BaseRoomDatabase() {
Edit: Following author's comment, I stand corrected i didn't get the question right.
Author actually asks how to store a List<Int> as field on a table. There are 2 solutions to do that: one, as Author suggests, is to store the List as String and use Like keyword to write queries with a clause like the following:
SELECT * FROM mytable
WHERE column1 LIKE '%word1%'
OR column1 LIKE '%word2%'
OR column1 LIKE '%word3%'
as a simple search on SO would have shown: SQL SELECT WHERE field contains words
The Author says he used TypeConverters so i'll skip how to convert a List<Int> into a string
The other solution to this problem is to realise that nothing was understood about the theory of Transactional Databases. In fact, when you have a many-to-many relationship, as in the case of podcast and genre, theory dictates that you build a table that links the ids of podcasts and the ids of genres, as it is explained here: https://dzone.com/articles/how-to-handle-a-many-to-many-relationship-in-datab
and other countless books, videos and blogs.
This benefits the db with added clarity, performance and scalability.
Bottom line, Author's db design is wrong.
I found [this article on Medium][1] that I found very helpful. What I'm trying to do is a many to many relationship, which in this case would be done something like the following:
Podcast class:
#Entity(tableName = "podcasts")
data class Podcast(
#PrimaryKey
#ColumnInfo(name = "podcast_id")
val id: String,
// other fields
}
Genre class:
#Entity(tableName = "genres")
data class Genre (
#PrimaryKey
#ColumnInfo(name = "genre_id")
val id: Int,
val name: String,
val parent_id: Int
)
PodcastDetails class:
data class PodcastDetails (
#Embedded
val podcast: Podcast,
#Relation(
parentColumn = "podcast_id",
entityColumn = "genre_id",
associateBy = Junction(PodcastGenreCrossRef::class)
)
val genres: List<Genre>
)
PodcastGenreCrossRef:
#Entity(primaryKeys = ["podcast_id", "genre_id"])
data class PodcastGenreCrossRef (
val podcast_id: Int,
val genre_id: Int
)
And access it in the DAO like this:
#Transaction
#Query(SELECT * FROM podcasts)
fun getPodcastsWithGenre() : List<PodcastDetails>
[1]: https://medium.com/androiddevelopers/database-relations-with-room-544ab95e4542

Categories

Resources