How to specify foreign relations with Embedded Entity - android

Given answer changed the embedded type to a foreign key type, I'm not looking for this.
I've a student table and an embedded type address.
Student Schema
+----+------+------------+
| id | name | address_id |
+----+------+------------+
| 1 | John | 1 |
| 2 | Jane | 2 |
+----+------+------------+
Address Schema
+----+------------------+
| id | addressLine |
+----+------------------+
| 1 | 123 Acme Street |
| 2 | 456 Beach Street |
+----+------------------+
Student Entity
#Entity(tableName = "student")
data class Student(
#PrimaryKey var id: Long,
var name: String?,
#Embedded var address: Address // can't change, given answer changed it
)
Address Entity
#Entity(tableName = "address")
data class Address(
#PrimaryKey var id: Long,
val addressLine: String
)
DAO Interface
#Dao
interface StudentDao {
#Query("SELECT * FROM student WHERE id = :id INNER JOIN address ON student.address_id = address.id")
fun findById(id: Long): Student
}
Issue: Current unable to populate the Embedded address of the Student.

Common way for your use-case is to use additional POJO class (not an entity) for getting joined result from both tables.
So, entities:
#Entity(tableName = "student")
data class Student(
#PrimaryKey var id: Long,
var name: String?,
var address_id: Long // <- you can make it foreign key in addition
)
#Entity(tableName = "address")
data class Address(
#PrimaryKey var id: Long,
val addressLine: String
)
and auxiliary POJO class:
data class StudentWithAdress( // <-- you can change the class name to more descriptive one
#Embedded var student: Student
#Embedded var address: Address
)
Your dao then:
#Dao
interface StudentDao {
#Query("SELECT * FROM student WHERE id = :id INNER JOIN address ON student.address_id = address.id")
fun findById(id: Long): StudentWithAdress
}
UPDATED
Second way is to use one-to-one Room's #Relation instead using SQLite Joins
This method uses the same entities, but query and auxiliary class differ a little bit:
data class StudentWithAdress(
#Embedded var student: Student
#Relation(
parentColumn = "addressId",
entityColumn = "id"
)
var address: Address
)
and query would be simpler:
#Transaction
#Query("SELECT * FROM student WHERE id = :id")
fun findById(id: Long): StudentWithAdress
}
Conclusion
Student class should include ONLY addressId (not whole Address object), and you use separate class StudentWithAdress that has both Student and Address. So to insert value to Student you should insert there addressId from corresponding Address table. That is common and correct way.
Both described ways are similar, you could choose any. Personally I prefer the Relations-way.
Technically it's possible to use just two entities without auxiliary class (and hold all the Address content inside Stident entity), but I think it breaks principle of relational table's normalisation and I don't want to include this in my answer since it's anti-pattern.
UPDATED 2 (anti-pattern)
I don't recommend it, but if you insist you can use next schema:
#Entity(tableName = "address")
data class Address(
#PrimaryKey var idAddress: Long, // <-- changed
val addressLine: String
)
#Entity(tableName = "student")
data class Student(
#PrimaryKey var id: Long,
var name: String?,
#Embedded var address: Address
)
How it works? #Embedded means that in fact Sqlite table Student includes all the fields of Address. In your case in actual Sqlite table there will be 4 columns - id, name, idAddress, addressLine (that's why it's necessary to change Address primary key's name, since there can't be fields with the same name).
Room hides this a little bit and you work with the Student object via address field. Why this is bad? Let's look at scenario:
You save Address with id = 1, addressLine = "Some address #1".
You set this address in some Student object and persist it. Under the hood Sqlite in Student table will hold values id = 1, addressLine = Some address #1.
Then on some reason you change addressLine in Address to "Some address #2".
But in Student table there is still old value of addressLine. That's a bug. Or you should persist this change at the Student table, and that's bad as well.

Related

Room Library - relationship between tables having auto generated keys

The examples I came across were forming relations without primary key, my database schema is as follow.
I have primary key in both tables, and I'm attempting to create a DAO method that will join both user and department tables to display a record.
Question, how to create a DAO method to access the record joined by the department information. I prefer to do without creating more data classes if possible. for instance using Map<User, Department>. If there's none then I will accept other solutions.
User Table
+----+----------+----------+---------------+
| id | username | name | department_id |
+----+----------+----------+---------------+
| 1 | johndoe | John Doe | 3 |
| 2 | janedoe | Jane Doe | 4 |
+----+----------+----------+---------------+
User data class
#Entity(tableName = "user")
data class User(
#PrimaryKey (autoGenerate = true) var id: Long,
var username: String,
var name: String,
var department_id: Long)
Department Table
+----+----------------+
| id | name |
+----+----------------+
| 1 | Sales |
| 2 | Account |
| 3 | Human Resource |
| 4 | Marketing |
+----+----------------+
Department data class
#Entity(tableName = "department")
data class Department(
#PrimaryKey(autoGenerate = true) var id: Long,
var name: String)
My attempt at DAO
#Dao
interface UserDAO {
#Query("SELECT user.*, department.name AS 'department_name' FROM user " +
"INNER JOIN department ON user.department_id = department.id " +
"WHERE user.id = :id")
fun findById(id: Long): Map<User, Department>
}
Edit
Will it work if Department is composed inside User? If that's the case then how the query inside DAO would look like?
#Entity(tableName = "user")
data class User(
#PrimaryKey (autoGenerate = true) var id: Long,
var username: String,
var name: String,
#Embedded var department: Department)
data class Department(
#PrimaryKey(autoGenerate = true)
var id: Long,
var name: String? = null): Parcelable
DAO attempt
#Query("SELECT * FROM user " +
"INNER JOIN department d ON department.id = d.id " +
"WHERE id = :id")
fun findById(id: Long): Map<User, Department>
Your code works as it is BUT due to ambiguous column names (id and name) values are not as expected.
Here's the result (noting that User id's are 500 and 501 instead of 1 and 2 to better highlight the issue) when debugging, with a breakpoint at a suitable place and when using:-
findById(500)
then the debugger shows :-
That is that although John's department id is 3, it is instead 500 in the Department itself, likewise the name of the department is John Doe not the expected *Human Resource.
This is because Room doesn't know which id is for what and likewise name.
The fix is to use unique column names. e.g. with:-
#Entity(tableName = "department")
data class Department(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "dept_id")
var id: Long,
#ColumnInfo(name = "dept_name")
var name: String
)
i.e. the columns in the table, but not in the Department have been changed. You could just change the name of the fields to be unique names.
Obviously if the column names are changed then the SQL has to be changed accordingly, so :-
#Query("SELECT * FROM user " +
"INNER JOIN department ON department_Id = dept_id " +
"WHERE id = :id")
fun findById(id: Long): Map<User, Department>
as the column names now have no ambiguities then there is no need to use table names to differentiate (you still can. Without the table banes the SQL can be more concise.
Now Debug shows:-
Additional re Edit
Will it work if Department is composed inside User?
If you use #Embedded within an #Entity annotated class that is included in the entities parameter of the #Database annotated class then the resultant table has columns as per the embedded class.
If the embedded class was previously a table then in respect of the relationship that doesn't exist and that previous table may well be defunct.
There is an issue with the embedded Department in that you will have 2 columns named id
From a database perspective, if what was a 1 to many relationship then the same data (e.g. Human Resource) will be repeated and is contrary to normalisation.
In regard to the query
#Query("SELECT * FROM user " +
"INNER JOIN department d ON department.id = d.id " +
"WHERE id = :id")
fun findById(id: Long): Map<User, Department>
Then this will not work as there will be no department table (as according to your code the Department class is not annotated with #Entity).
Room supports this since version 2.4 as described in the official documentation. Similar issue to yours appear to be solved also in this answer https://stackoverflow.com/a/72990697/5473567
In your concrete example you are forgetting, that the relation is 1:N, therefore your return type in the DAO function should look like this Map<User, List<Department>>.
Also the department.name AS 'department_name' in your SELECT is wrong. The field name in the data class Department is called name, therefore also in Database table department this field will be called name. If you would like to change its name to the department_name as you use in your SELECT, you have to use annotation #ColumnInfo as described in here. The data class will then look like this:
#Entity(tableName = "department")
data class Department(
#PrimaryKey(autoGenerate = true) var id: Long,
#ColumnInfo(name = "department_name") var name: String
)
Last tip, if you are pulling full data from the database, there is enough to use SELECT * FROM..., Room will resolve it for you.
Finally the whole DAO interface may look like this:
#Dao
interface UserDAO {
#Query("SELECT * FROM user " +
"INNER JOIN department ON user.department_id = department.id " +
"WHERE user.id = :id")
fun findById(id: Long): Map<User, Department>
}
Solved. Based on the answers, here's my solution without using additional data classes while also keeping the column names intact, for instance using id column in each table without any prefix (preferable). I had to use user.id in the WHERE clause to avoid ambiguity
User
#Entity(tableName = "user")
data class User(
#PrimaryKey (autoGenerate = true)
var id: Long,
var username: String,
var name: String,
var department_id: Long)
Department
#Entity(tableName = "department")
data class Department(
#PrimaryKey(autoGenerate = true)
var id: Long,
var name: String)
DAO Query
#Query("SELECT * FROM user " +
"INNER JOIN department ON user.department_id = department.id " + // fixed from user.id = department.id
"WHERE user.id = :id")
fun find(id: Long): Map<User, Department>

Do I have a way to search objects on Room Database

I am now building an Android App with Local Database
The table structure is like following (coming from API)
#Entity
data class Person(
name: Name,
... ... ...
... ... ...
)
data class Name(
legalName: String.
common: String
)
This is sql code I have tried to person with legal name
#Query("SELECT * FROM person WHERE name.legalName = :legalName")
suspend fun getPersonByName (legalName: String): Person?
This gave me compile error as we can't search by name.legalName on Room database
In addition, we have static name list of person (only legal name) in Homepage (No ID or other reasonable fields to perform search)
DO we have proper way to search Users with legalName field?
The #Entity annotation is used by Room to determine the underlying SQLite table schema. A class so annotated is an object but the individual fields/members of the object are stored as columns in the table which are not objects.
Such columns can never be anything other than specific types being either:-
integer type values (e.g. Int, Long .... Boolean) (column type of INTEGER)
string type values (e.g. String) (column type of TEXT)
decimal/floating point type values (e.g, Float, Double) (column type REAL)
bytestream type values (e.g. ByteArray) (column type BLOB)
null (column definition must not have NOT NULL constraint)
Thus, objects are NOT stored or storable directly SQLite has no concept/understanding of objects just columns grouped into tables.
In your case the name field is a Name object and Room will require 2 Type Converters:-
One that converts the object into one of the above that can represent the object (typically a json representation of the object)
The other to convert the stored data back into the Object.
This allowing an object to be represented in a single column.
As such to query a field/member of the object you need to consider how it is represented and searched accordingly.
There will not be a name.legalName column just a name column and the representation depends upon the TypConverter as then would the search (WHERE clause).
Now consider the following based upon your code:-
#Entity
data class Person(
#PrimaryKey
var id: Long?=null,
var name: Name,
#Embedded /* Alternative */
var otherName: Name
)
data class Name(
var legalName: String,
var common: String
)
PrimaryKey added as required by Room
#Embedded as an alternative that copies the fields/members (legalName and common as fields)
Thus the name column will require TypeConverters as per a class with each of the 2 annotated twith #TypeConverter (note singular), the class where the Type Converters are defined has to be defined (see the TheDatabase class below). So :-
class TheTypeConverters {
/* Using Library as per dependency implementation 'com.google.code.gson:gson:2.10.1' */
#TypeConverter
fun convertFromNameToJSONString(name: Name): String = Gson().toJson(name)
#TypeConverter
fun convertFromJSONStringToName(jsonString: String): Name = Gson().fromJson(jsonString,Name::class.java)
}
note that there are other Gson libraries that may offer better functionality.
The entities (just the one in this case) have to be defined in the #Database annotation for the abstract class that extends RoomDatabase(). so:-
#TypeConverters(value = [TheTypeConverters::class])
#Database(entities = [Person::class], exportSchema = false, version = 1)
abstract class TheDatabase: RoomDatabase() {
abstract fun getTheDAOs(): TheDAOs
companion object {
private var instance: TheDatabase? = null
fun getInstance(context: Context): TheDatabase {
if (instance == null) {
instance = Room.databaseBuilder(context,TheDatabase::class.java,"the_database.db")
.allowMainThreadQueries() /* For brevity convenience of the demo */
.build()
}
return instance as TheDatabase
}
}
}
The #TypeConverters annotation (plural) in addition to defining a class or classes where the TypeConverters are, also defines the scope (#Database being the most encompassing scope).
At this stage the project can be compiled (CTRL + F9) and the annotation processing will generate some code. Importantly TheDatabase_Impl in the java(generated) The name being the same as the #Database annotated class suffixed with _Impl. This includes a method createAllTables which is the SQL used when creatin the SQLite tables. The SQL for the person table is:-
CREATE TABLE IF NOT EXISTS `Person` (
`id` INTEGER,
`name` TEXT NOT NULL,
`legalName` TEXT NOT NULL,
`common` TEXT NOT NULL, PRIMARY KEY(`id`)
)
As can be seen the id column as the primary key, the name column for the converted representation of the name object and then the legal and common columns due to the name object being #Embedded via the otherName field.
Just to finish matters with the following #Dao annotated interface (allowing some data to be added):-
#Dao
interface TheDAOs {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(person: Person): Long
#Query("SELECT * FROM person")
fun getAllPersonRows(): List<Person>
}
And with MainActivity as:-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: TheDAOs
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getTheDAOs()
dao.insert(Person(null, name = Name("Frederick Bloggs","Fred Bloggs"), otherName = Name("Frederick ","Fred Bloggs")))
dao.insert(Person(null, name = Name("Jane Doe","Jane Doe"), otherName = Name("Jane Doe","Jane Doe")))
}
}
and the project run and then App Inspection used to view the actual database then:-
The name column contains the string {"common":"Fred Bloggs","legalName":"Frederick Bloggs"}
So the WHERE clause to locate all legal names that start with Fred could be
WHERE instr(name,',\"legalName\":\"Fred')
or
WHERE name LIKE '%,\"legalName\":\"Fred%'
it should be noted that both due to the search being within a column requires a full scan.
Of course that assumes that there is no name that has the common name ,"legalName":"Fred or as part of the common name or some other part of entire string. i.e. it can be hard to anticipate what results may be in the future.
For the alternative #Embedded Name object, the legalName and common columns are more easily searched, the equivalent search for legal names starting with Fred could be
WHERE legalname LIKE 'Fred%'
There is no potential whatsoever for Fred appearing elsewhere meeting the criteria. The search just on the single column/value nothing else. Indexing the column would very likely improve the efficiency.
Amending the #Dao annotated interface TheDAOs to be:-
#Dao
interface TheDAOs {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(person: Person): Long
#Query("SELECT * FROM person WHERE instr(name,',\"legalName\":\"Fred')")
fun getPersonsAccordingToLegalNameInNameObject(): List<Person>
#Query("SELECT * FROM person WHERE legalName LIKE 'Fred%'")
fun getPersonsAccordingToLegalName(): List<Person>
}
And MainActivity to be:-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: TheDAOs
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getTheDAOs()
dao.insert(Person(null, name = Name("Frederick Bloggs","Fred Bloggs"), otherName = Name("Frederick ","Fred Bloggs")))
dao.insert(Person(null, name = Name("Jane Doe","Jane Doe"), otherName = Name("Jane Doe","Jane Doe")))
logPersonList(dao.getPersonsAccordingToLegalNameInNameObject(),"RUN1")
logPersonList(dao.getPersonsAccordingToLegalName(),"RUN2")
}
private fun logPersonList(personList: List<Person>, suffix: String) {
for (p in personList) {
Log.d("DBINFO_${suffix}","Person ID is ${p.id} Name.legalName is ${p.name.legalName} Name.common is ${p.name.common} LegalName is ${p.otherName.legalName} Common is ${p.otherName.common}")
}
}
}
Then running (first time after install) the log contains:-
2023-01-14 11:26:03.738 D/DBINFO_RUN1: Person ID is 1 Name.legalName is Frederick Bloggs Name.common is Fred Bloggs LegalName is Frederick Common is Fred Bloggs
2023-01-14 11:26:03.740 D/DBINFO_RUN2: Person ID is 1 Name.legalName is Frederick Bloggs Name.common is Fred Bloggs LegalName is Frederick Common is Fred Bloggs
i.e. in this limited demo the expected results either way.
Note that Name.legalName and Name.common is not how the data is accessed, it is just text used to easily distinguish then similar values.

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.

What is the proper way to get all child entities in Android Room?

Say I have a one to many relationship between City and Person.
#Entity(tableName = "city")
data class City(
#PrimaryKey
val cityId: Int,
val name: String
)
#Entity(tableName = "person")
data class Person(
#PrimaryKey(autoGenerate = true)
val personId: Long = 0,
val name: String,
val cityId: Int
)
The goal is to get all person in the database and their corresponding city. According to the Jetpack documentation, the obvious way is to create a CityWithPersons class.
data class CityWithPersons(
#Embedded
val city: City,
#Relation(parentColumn = "cityId", entityColumn = "cityId")
val persons: List<Person>
)
Get all CityWithPersons, then combine persons from there.
But in my scenario, there could be less than 10 person and more than 1000 cities in the database. It seems ridiculous and very inefficient to do it this way.
Other potential approaches could be:
get a full list of person then query the city with cityId one by one
embed the City in Person entitiy instead of cityId
Do it as many to many relationship. PersonWithCity will just have a cities array with one entity
I wonder which would be the best way to do it? Or a better way I didn't think of?
I wonder which would be the best way to do it? Or a better way I didn't think of?
I don't believe that the many-many relationship would provide any performance advantage as you would still need to search through one of the tables. Nor do I believe that get a full list of person then query the city with cityId one by one would be of benefit (however do you need to? (rhetorical) See the PersonAndCity that effectively does this in one go)
the obvious way is to create a CityWithPersons class
Seeing that you are looking at the issue from the Person perspective, then why not PersonWithCity class?
embed the City in Person entitiy instead of cityId :-
data class PersonWithCity(
#Embedded
val person: Person,
#Relation(parentColumn = "cityId",entityColumn = "cityId")
val city: City
)
And a Dao such as :-
#Query("SELECT * FROM person")
fun getPersonWithCity(): List<PersonWithCity>
Do you need to build everything?
Another consideration I don't believe you've considered :-
data class PersonAndCity(
val personId: Long,
val name: String,
val cityId: Int,
val cityName: String,
)
And a Dao such as
#Query("SELECT *, city.name AS cityName FROM person JOIN city ON person.cityId = city.cityId")
fun getPersonAndCity(): List<PersonAndCity>
No #Relation
Running the above 2 and the original with 100000 Person and 10000 cities (I assume more Person rows) and Person randomly linked to a City extracting all with each of the 3 methods then the results are :-
690ms (extracts 10000 Cities with ? Persons) using CityWithPersons
1560ms (extracts all 100000 Persons with the City) using PersonWithCity
1475ms (extracts all 100000 Persons with the City information rather than a City object)
Changing to 10 Persons with 1000 Cities then
49ms (CityWithPersons (10000 extracted))
2ms (PersonWithCity (10) extracted)
5ms (PersonAndCity (10 extracted))
As such, the best way is dependant upon the what you are doing. As can be seen the ratio between Cities and Persons is a factor that should be considered.
In short you should undertake testing :-
For the results above I used :-
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val db = Room.databaseBuilder(applicationContext,MyDatabase::class.java,"Mydb")
.allowMainThreadQueries()
.build()
val dao = db.getAllDao()
val people = 10
val cities = 1000
for(i in 1..cities) {
dao.insertCity(City(null,"City00" + i))
}
for(i in 1..people) {
dao.insertPerson(Person(null,"Person" + i,(1..cities).random()))
}
val TAG = "CITYPERSONINFO"
Log.d(TAG,"Starting Test 1 - Using CityWithPerson")
val usingCityWithPerson = dao.getCityWithPerson()
Log.d(TAG,"Done Test 1. Rows Extracted = " + usingCityWithPerson.size)
Log.d(TAG,"Starting Test 2 - UsingPersonAndCity")
val usingPersonWithCity = dao.getPersonWithCity()
Log.d(TAG,"Done Test 2. Rows Extracted = " + usingPersonWithCity.size)
Log.d(TAG,"Starting Test 3 - UsingPersonAndCity (no #Relation just JOIN)")
val usingPersonAndCity = dao.getPersonAndCity()
Log.d(TAG,"Done Test 3. Rows Extracted = " + usingPersonAndCity.size)
}
}
Note that I uninstall the App between runs.

How to model many-to-many relationship in Android Room with CrossRef Junction

How would I model a many-to-many relationship as described in https://developer.android.com/training/data-storage/room/relationships#many-to-many, but with an additional property on the junction? I basically want to achieve the following:
#Entity
data class Playlist(
#PrimaryKey val playlistId: Long,
val playlistName: String
)
#Entity
data class Song(
#PrimaryKey val songId: Long,
val songName: String,
val artist: String
)
#Entity(primaryKeys = ["playlistId", "songId"])
data class PlaylistSongCrossRef(
val playlistId: Long,
val songId: Long,
val rating: Int // <-- the additional property
)
data class PlaylistWithRating(
val playlist: Playlist,
val rating: Int // <-- the additional property
)
data class SongWithPlaylists(
#Embedded val song: Song,
#Relation(
parentColumn = "songId",
entityColumn = "playlistId",
associateBy = #Junction(PlaylistSongCrossRef::class)
)
val playlists: List<PlaylistWithRating>
)
so I could access it in my Dao:
#Dao
interface SongWithPlaylistsDao {
#Query("SELECT * FROM Song")
fun list(): LiveData<List<SongWithPlaylists>>
}
I know how, from an ERM perspective, you would model this relationship like this:
/-- A ---\ /- ACrossB -\ /-- B ---\
| | | | | |
| - id |----->| - aId | |------| - id |
| - name | | - bId |-----| | -name |
| | | - prop | | |
\--------/ \-----------/ \--------/
I also know how to query this relationship using JOIN, but couldn't figure out from the docs how to do this in Room while keeping data integrity.
have you tried this :
#Entity
data class PlaylistSongCrossRef(
val playlistId: Long,
val songId: Long,
#PrimaryKey(autoGenerate = true)
val rating: Int // <-- the additional property
)
Note : not sure for the way to declare the primary in kotlin, I m java developper.
But you've got the idea..
The junction will still works, while you can have many association with the same song differentiated by a unique rating.
I ran into the same problem and finally achieved it in other way. There are cases when you can not assign an attribute to any side of the binary relationship so the attribute belongs to the relationship itself.
Step 1 -> define entity A and entity B
#Entity
data class Playlist(
#PrimaryKey val playlistId: Long,
val playlistName: String
)
#Entity
data class Song(
#PrimaryKey val songId: Long,
val songName: String,
val artist: String
)
Step2 -> define the associative entity(CrossRef entity), and add your additional attribute to it
#Entity(primaryKeys = ["playlistId", "songId"])
data class PlaylistSongCrossRef(
val playlistId: Long,
val songId: Long,
val rating: Int // <-- the additional property
)
till now it was all repeating your code :D
Step3 -> define the desired data model (Pojo) suitable to your query needs
Lets consider you want to use SongWithPlaylists model class. In SongWithPlaylists you should omit #Relation annotation from playlists and instead annotate it with #Embedded.
Note-> change playlists type from List<PlaylistWithRating> to Playlist. No need to use PlaylistWithRating, just Playlist not even a list of it. Room no longer manages join for us. we will make it :D
And also add your CrossRef's additional attributes to SongWithPlaylists cause we want that additional attribute to be mapped to this object after join query finished.
SongWithPlayList will be like bellow
data class SongWithPlaylists(
#Embedded val song: Song,
#Embedded val playlist: Playlist,
val rating: Int
)
Step4 -> Add plain Join query to your DAO
now that you get rid of #Relation annotation and PlaylistWithRatings, you should inner join your entity A on Entity B and again join on CrossRef entity.
DAO is like bellow
#Dao
interface SongWithPlaylistsDao {
#Query("""
select Song.songId,Song.songName,Song.artist,Playlist.playlistId,Playlist.playlistName,PlaylistSongCrossRef.rating
from Song inner join PlaylistSongCrossRef on Song.songId = PlaylistSongCrossRef.songId
inner join Playlist on Playlist.playlistId = PlaylistSongCrossRef.playlistId
""")
fun list(): LiveData<List<SongWithPlaylists>>
}
This was the dirty way but worked for me. Sorry for my poor English. I Hope helped you and anybody may would run into the same problem.
The solution is to treat the PlaylistSongCrossRef as an entity of its own.
Instead of defining one many-to-many relationship, you define two relationships:
A one-to-many relationship between Playlist and PlaylistSongCrossRef, and
a many-to-one relationship between PlaylistSongCrossRef and Song.
I would recommend to rename PlaylistSongCrossRef to something like PlaylistSongParameters to avoid clashing with the naming convention of many-to-many relationships.
You can even query all three entities in one go without writing custom queries. The Room documentation describes this in the "nested relationships" section.
I think you don't need to create a new data class PlaylistWithRating. Because you can set the additional property rating to entity directly. For example,
If your rating is for playlist, you can set this property as follow.
#Entity
data class Playlist(
#PrimaryKey val playlistId: Long,
val userCreatorId: Long,
val playlistName: String,
val rating: Int // <-- the additional property
)
If your rating is for song, you can set this property as follow.
#Entity
data class Song(
#PrimaryKey val songId: Long,
val songName: String,
val artist: String,
val rating: Int // <-- the additional property
)
So, the many-to-many relation of your schema as you mentioned document will be correct. You don't need to consider additional data class for rating property.
So, it might be as follow.
#Entity
data class Playlist(
#PrimaryKey val playlistId: Long,
val userCreatorId: Long,
val playlistName: String,
val rating: Int // <-- the additional property
)
#Entity
data class Song(
#PrimaryKey val songId: Long,
val songName: String,
val artist: String
)
#Entity(primaryKeys = ["playlistId", "songId"])
data class PlaylistSongCrossRef(
val playlistId: Long,
val songId: Long
)
data class PlaylistWithSongs(
#Embedded val playlist: Playlist,
#Relation(
parentColumn = "playlistId",
entityColumn = "songId",
associateBy = #Junction(PlaylistSongCrossRef::class)
)
val songs: List<Song>
)
data class SongWithPlaylists(
#Embedded val song: Song,
#Relation(
parentColumn = "songId",
entityColumn = "playlistId",
associateBy = #Junction(PlaylistSongCrossRef::class)
)
val playlists: List<Playlist>
)
I hope this will be helpful.

Categories

Resources