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>
Related
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.
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.
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.
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.
In Android room relation, is it possible to use search query using the property of the related table. Below is my table structure. In this i am relating transaction with payment and lines(transaction items). I have an search field in my UI where the user could search using payment amount which is inside payment table. How to form a query to access the properties of payment table.
class TransactionWithPaymentAndLines(
#Embedded
var transactions: Transactions? = null,
#Relation(
parentColumn = "id",
entityColumn = "transactionId",
entity = Payment::class
)
var payments: List<Payment> = listOf(),
#Relation(
parentColumn = "id",
entityColumn = "transactionId",
entity = TransactionLines::class
)
var transactionLines: List<TransactionLines> = listOf()
)
Ideal way is to query multiple related tables is to create a View. A view combines data from two or more tables using join.
In Android, using Room Persistance library, you can create such a view, and then you can query the fields of view. This is how you can do it:
Suppose, you have tables:
User: id, name, departmentId
Department: id, name
Create a View:
#DatabaseView("SELECT user.id, user.name, user.departmentId," +
"department.name AS departmentName FROM user " +
"INNER JOIN department ON user.departmentId = department.id")
data class UserDetail(
val id: Long,
val name: String?,
val departmentId: Long,
val departmentName: String?
)
Add View to Database:
#Database(entities = arrayOf(User::class),
views = arrayOf(UserDetail::class), version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDetailDao(): UserDetailDao
}
Create a DAO:
#Dao
interface UserDetailDao {
#Query("SELECT * FROM UserDetail")
fun loadAllUserDetails(): Array<UserDetail>
}
Now, you can query a View using this DAO.
Absolutely possible, you can use #Query in your DAO class please read Room Database Documentation
Examples of #Query
#Query("SELECT * FROM user")
List<User> getAll();
#Query("SELECT * FROM user WHERE uid IN (:userIds)")
List<User> loadAllByIds(int[] userIds);
#Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
"last_name LIKE :last LIMIT 1")
User findByName(String first, String last);
use #DB or #Query.
That should perfectly work...
#Query("SELECT * FROM TABLE_NAME")
List<Identifier> getAll();