Android Room Many to many relation with associateBy and Junction not compiling - android

I tried same code as of given in https://developer.android.com/training/data-storage/room/relationships#many-to-many exactly with room 2.2.0.
#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
)
data class PlaylistWithSongs(
#Embedded val playlist: Playlist,
#Relation(
parentColumn = "playlistId",
entityColumn = "songId",
associateBy = Junction(PlaylistSongCrossRef::class, parentColumn = "playlistId", entityColumn = "songId")
)
val songs: List<Song>
)
I have more complex data but when i tried same code from link it shows
error: Not sure how to convert a Cursor to this method's return type (java.util.List<com.skybase.compose_tut.PlaylistWithSongs>).
I am wondering why this sample code not working?

You issue will be with one of the functions (methods in java) in the #Dao annotated interface (or abstract class). The code provided (Song, Playlist, PlaylistSongCrossref and the POJO PLaylistsWithSongs) are all OK.
In the #Dao annotated interface you want something like:-
#Query("SELECT * FROM playlist")
fun getAllPlayListsWithSongs(): List<PlaylistWithSongs>
The return type being List<PlaylistWithSongs>
Demo
With a project with your code and the following extra code for the database:-
#Database(entities = [Song::class,Playlist::class,PlaylistSongCrossRef::class], exportSchema = false, version = 1)
abstract class TheDatabase: RoomDatabase() {
abstract fun getAllDAOs(): AllDAOs
companion object {
private var instance: TheDatabase?=null
fun getInstance(context: Context): TheDatabase {
if (instance==null) {
instance = Room.databaseBuilder(context,TheDatabase::class.java,"the_database")
.allowMainThreadQueries() /* for convenience/brevity of demo */
.build()
}
return instance as TheDatabase
}
}
}
#Dao
interface AllDAOs {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(song: Song): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(playlist: Playlist): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(playlistSongCrossRef: PlaylistSongCrossRef): Long
#Query("SELECT * FROM playlist")
fun getAllPlayListsWithSongs(): List<PlaylistWithSongs>
}
and then the following activity code:-
class MainActivity : AppCompatActivity() {
lateinit var dbInstance: TheDatabase
lateinit var dao: AllDAOs
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
dbInstance = TheDatabase.getInstance(this)
dao = dbInstance.getAllDAOs()
var song1Id = dao.insert(Song(songId=1, songName = "Song1", artist = "Artist1"))
var song2Id = dao.insert(Song(songId=2, songName = "Song2", artist = "Artist1"))
var song3Id = dao.insert(Song(songId=3, songName = "Song3", artist = "Artist2"))
var pl1id = dao.insert(Playlist(playlistId = 100, playlistName = "Playlist 1"))
var pl2id = dao.insert(Playlist(playlistId = 101, playlistName = "Playlist 2"))
var pl3id = dao.insert(Playlist(playlistId = 102, playlistName = "Playlist 3"))
dao.insert(PlaylistSongCrossRef(pl1id,song1Id))
dao.insert(PlaylistSongCrossRef(pl1id,song2Id))
dao.insert(PlaylistSongCrossRef(pl1id,song3Id))
dao.insert(PlaylistSongCrossRef(pl2id,song2Id))
val sb = StringBuilder()
for (pws in dao.getAllPlayListsWithSongs()) {
sb.clear()
for (s in pws.songs) {
sb.append("\n\tSong ID is ${s.songId} Name is ${s.songName} Artist is ${s.artist}")
}
Log.d("DBINFO","PLaylist ID is ${pws.playlist.playlistId} Name is ${pws.playlist.playlistName} it has ${pws.songs.size} songs: They are:-${sb}")
}
}
}
Then it compiles (using Room 2.2.0 libraries) fine and when run results in the expected:-
D/DBINFO: PLaylist ID is 100 Name is Playlist 1 it has 3 songs: They are:-
Song ID is 1 Name is Song1 Artist is Artist1
Song ID is 2 Name is Song2 Artist is Artist1
Song ID is 3 Name is Song3 Artist is Artist2
D/DBINFO: PLaylist ID is 101 Name is Playlist 2 it has 1 songs: They are:-
Song ID is 2 Name is Song2 Artist is Artist1
D/DBINFO: PLaylist ID is 102 Name is Playlist 3 it has 0 songs: They are:-

Related

How can I get the desired object with sql from a many-to-many relationship table?

i'm android beginner developer. i am createing stationArrivalInformation App.
#Entity
data class StationEntity(
#PrimaryKey val stationName: String,
val isFavorite: Boolean = false
)
#Entity
data class SubwayEntity(
#PrimaryKey val subwayId: Int
)
#Entity(primaryKeys = ["stationName", "subwayId"])
data class StationSubwayCrossRefEntity(
val stationName: String,
val subwayId: Int
)
data class StationWithSubwaysEntity(
#Embedded val station: StationEntity,
#Relation(
parentColumn = "stationName",
entityColumn = "subwayId",
entity = SubwayEntity::class,
associateBy = Junction(
StationSubwayCrossRefEntity::class,
parentColumn = "stationName",
entityColumn = "subwayId"
)
)
val subways: List<SubwayEntity>
)
i have built a data class with a many-to-many relationship.
1. station Table
enter image description here
2. subway Table
enter image description here
3. cross Ref Table
enter image description here
if you look at the DAO File:
#Dao
interface StationDao {
#Transaction
#Query("SELECT * FROM StationEntity")
fun getStationWithSubways(): Flow<List<StationWithSubwaysEntity>>
#Insert(onConflict = REPLACE)
suspend fun insertStations(station: List<StationEntity>)
#Insert(onConflict = REPLACE)
suspend fun insertSubways(subway: List<SubwayEntity>)
#Insert(onConflict = REPLACE)
suspend fun insertCrossRef(refEntity: List<StationSubwayCrossRefEntity>)
#Transaction
#Insert(onConflict = REPLACE)
suspend fun insertStationSubways(stationSubways: List<Pair<StationEntity, SubwayEntity>>) {
insertStations(stationSubways.map { it.first })
insertSubways(stationSubways.map { it.second })
insertCrossRef(stationSubways.map { (station, subway) ->
StationSubwayCrossRefEntity(
station.stationName, subway.subwayId
)
})
}
#Update
suspend fun updateStation(station: StationEntity)
}
class StationRepositoryImpl #Inject constructor(
private val stationDao: StationDao
): StationRepository {
override val stations: Flow<List<Station>> =
stationDao.getStationWithSubways()
.distinctUntilChanged()
.map { stations -> stations.toStation().sortedByDescending { it.isFavorite } }
.flowOn(dispatcher)
}
here, the result of stationDao.getStationWithSubways() is null. I referred to the Android official documentation and applied it, but I am not getting the desired result. Why?
i expected getting multiple subways at one station
enter image description here
<Type of the parameter must be a class annotated with #Entity or a collection/array of it.>
-> how solve?
Room does not anything (according to the classes shown) about the Station class and thus it does not know how to handle:-
#Update
suspend fun updateStation(station: Station)
As there is no such #Entity annotated class.
The fix I believe is to use
#Update
suspend fun updateStation(station: StationEntity)
if that is what you are trying to update
However, you will then possibly have issues due to the warning that will be in the Build log:-
warning: The affinity of child column (subwayId : INTEGER) does not match the type affinity of the junction child column (subwayId : TEXT). - subways in a.a.so75052787kotlinroomretrievefrommanytomany.StationWithSubwaysEntity
Another warning that you may wish to consider is
warning: The column subwayId in the junction entity a.a.so75052787kotlinroomretrievefrommanytomany.StationSubwayCrossRefEntity is being used to resolve a relationship but it is not covered by any index. This might cause a full table scan when resolving the relationship, it is highly advised to create an index that covers this column. - subwayId in a.a.so75052787kotlinroomretrievefrommanytomany.StationSubwayCrossRefEntity
Both will not appear if you used:-
#Entity(primaryKeys = ["stationName", "subwayId"])
data class StationSubwayCrossRefEntity(
val stationName: String,
//val subwayId: String
#ColumnInfo(index = true)
val subwayId: Int
)
commented out line left in just to show before/after
You would then have to use :-
#Transaction
#Insert(onConflict = REPLACE)
fun insertStationSubways(stationSubways: List<Pair<StationEntity, SubwayEntity>>) {
insertStations(stationSubways.map { it.first })
insertSubways(stationSubways.map { it.second })
insertCrossRef(stationSubways.map { (station, subway) ->
StationSubwayCrossRefEntity(
station.stationName, subway.subwayId
)
})
}
subway.subwayId instead of subway.subwayId.toString()
i expected getting multiple subways at one station in normal operation
You would. With the changes suggested above (and some others to run on the main thread for brevity/convenience), an appropriate #Database annotated class as per:-
#Database(entities = [SubwayEntity::class,StationEntity::class, StationSubwayCrossRefEntity::class], version = 1, exportSchema = false)
abstract class TheDatabase: RoomDatabase() {
abstract fun getStationDao(): StationDao
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()
.build()
}
return instance as TheDatabase
}
}
}
And the following in an Activity, to demonstrate :-
class MainActivity : AppCompatActivity() {
lateinit var dbInstance: TheDatabase
lateinit var dao: StationDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
dbInstance = TheDatabase.getInstance(this)
dao = dbInstance.getStationDao()
dao.insertStationSubways(listOf(Pair(StationEntity("Station1",false),SubwayEntity(1))))
dao.insertStationSubways(listOf(Pair(StationEntity("Station1",false),SubwayEntity(2))))
dao.insertStationSubways(listOf(Pair(StationEntity("Station1",false),SubwayEntity(3))))
dao.insertStationSubways(listOf(Pair(StationEntity("Station2",false),SubwayEntity(4))))
dao.insertStationSubways(listOf(Pair(StationEntity("Station2",false),SubwayEntity(5))))
dao.insertStationSubways(listOf(Pair(StationEntity("Station2",false),SubwayEntity(6))))
val sb = StringBuilder()
for (sws in dao.getStationWithSubways()) {
sb.clear()
for (subway in sws.subways) {
sb.append("\n\tSubway is ${subway.subwayId}")
}
Log.d("DBINFO","Station is ${sws.station.stationName} it has ${sws.subways.size} subways. They are:-${sb}")
}
}
}
Then the log (when run for the first time) includes (as expected):-
D/DBINFO: Station is Station1 it has 3 subways. They are:-
Subway is 1
Subway is 2
Subway is 3
D/DBINFO: Station is Station2 it has 3 subways. They are:-
Subway is 4
Subway is 5
Subway is 6

Retrieve user's posts from Room

I am trying this below, that I been writing. However, I am a bit new to this whole thing with Room. It does remind me of the Microsoft.Linq to some extent, however, the MS version is easier and more straightforward. Whereas this one is confusing a bit.
#Dao
interface AllDao {
// Account Data Access Object:
#Transaction
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertAccount(account: Account)
#Delete
suspend fun deleteAccount(account: Account)
#Update
suspend fun updateAccount(account: Account)
#Transaction
#Query("SELECT * FROM `accounts` WHERE email = :email")
suspend fun getAccountByEmail(email: String): Flow<Account?>
// Post Data Access Object:
#Transaction
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertPost(post: Post)
#Delete
suspend fun deletePost(post: Post)
#Update
suspend fun updatePost(post: Post)
#Transaction
#Query("SELECT * FROM `posts` WHERE post_id = :post_id")
suspend fun getPostById(post_id: Int, user_id: Int): Flow<Post?>
#Transaction
#Query("SELECT * FROM `posts` ORDER BY posts.title")
fun getPostsByUserId(uid: Int): Flow<List<Posts>>
#Transaction
#Query("SELECT * FROM `posts` ORDER BY posts.title WHERE posts.post_id = :post_id AND accounts._id = :user_id")
fun getUserPostSingle(post_id: Int, user_id: Int) : Flow<Post?>
/*
Account with Post Data Access Object:
*/
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(join: AccountWithPost)
}
Data structure: This is how I have setup the entities, however, this isn't as mentioned as straight forward as anticipated e.g., like Microsoft Linq.
#Entity(tableName = "accounts")
data class Account(
#PrimaryKey(autoGenerate = true)
#NonNull
val id: Int,
#ColumnInfo(name = "first_name")
val firstName: String,
#ColumnInfo(name = "last_name")
val lastName: String?,
val email: String
)
#Entity(
tableName = "posts",
foreignKeys = [ForeignKey(
entity = Account::class,
parentColumns = ["id"],
childColumns = ["postUserId"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)]
)
data class post(
#PrimaryKey(autoGenerate = true)
#NonNull
val post_id: Int,
val title: String,
val content: String,
)
data class AccountWithPost(
#Embedded
var account: Account,
#Relation(
parentColumn = "id",
entity = Post::class,
entityColumn = "postUserId"
)
var posts: List<Post>,
)
You have a few issues, the most important as per the comments, is that you need to have something to relate a Post with it's PARENT account.
Another issue is that you appear to consider that AccountWithPost is a table (and you try to insert into this). It is not a table, rather it is a container that due to the #Relation annotation will retrieve an Account (the #Embedded) along with all the related Posts according to the ParentColumn and the Child Column (which is effectively the join).
Here's a working example (note without Flows/Suspends i.e. run on the mainThread for brevity/convenience).
The example (designed to just run once):-
adds 3 accounts and then
adds 5 posts to the first (Fred Bloggs)
adds 2 posts to the second account (Mary Smith)
adds 1 post to the third account (Jane Doe)
finally extracts everything as a List of AccountWithPosts
All of your classes PLUS an #Database annotated class :-
#Entity(tableName = "accounts")
data class Account(
#PrimaryKey(autoGenerate = true)
#NonNull
val id: Int,
#ColumnInfo(name = "first_name")
val firstName: String,
#ColumnInfo(name = "last_name")
val lastName: String?,
val email: String
)
#Entity(
tableName = "posts",
foreignKeys = [ForeignKey(
entity = Account::class,
parentColumns = ["id"],
childColumns = ["postUserId"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)]
)
data class Post /* Changed to Post from post */(
#PrimaryKey(autoGenerate = true)
#NonNull
val post_id: Int,
#ColumnInfo(index = true) /* Index on FK column (else warning issued) */
val postUserId: Int, /*<<<<<<<<<< Added the Userid that is the parent to the post IMPORTANT */
val title: String,
val content: String,
)
data class AccountWithPost(
#Embedded
var account: Account,
#Relation(
parentColumn = "id",
entity = Post::class,
entityColumn = "postUserId"
)
var posts: List<Post>,
)
#Dao
interface AllDao {
// Account Data Access Object:
//#Transaction will be in a single transaction anyway
#Insert(onConflict = OnConflictStrategy.IGNORE)
/*suspend*/ fun insertAccount(account: Account): Long /* Returns the rowid aka id of the inserted Account */
#Delete
/*suspend*/ fun deleteAccount(account: Account): Int /* Returns how many rows have been deleted */
#Update
/*suspend*/ fun updateAccount(account: Account): Int /* Returns how many rows have been updated */
//#Transaction will be in a single transaction anyway
#Query("SELECT * FROM `accounts` WHERE email = :email")
/*suspend*/ fun getAccountByEmail(email: String): /*Flow<Account?>*/ List<Account> /*changed for demo on main thread */
// Post Data Access Object:
//#Transaction
#Insert(onConflict = OnConflictStrategy.REPLACE)
/*suspend*/ fun insertPost(post: Post): Long /* id of inserted row */
#Delete
/*suspend*/ fun deletePost(post: Post): Int
#Update
/*suspend*/ fun updatePost(post: Post): Int
#Transaction
#Query("SELECT * FROM `posts` WHERE post_id = :post_id")
/*suspend*/ fun getPostById(post_id: Int/*, user_id: Int UNUSED */): /*Flow<Post?>*/ List<Post>
#Transaction
#Query("SELECT * FROM `posts` /* ADDED */ WHERE postUserId=:uid /* END OF ADDED*/ ORDER BY posts.title")
fun getPostsByUserId(uid: Int): /*Flow<List<Post>>*/ List<Post>
#Transaction
#Query("SELECT * FROM `posts` WHERE posts.post_id = :post_id AND postUserId = :user_id /* CHANGED to use postUserId columns */ ORDER BY posts.title")
fun getUserPostSingle(post_id: Int, user_id: Int) : /*Flow<Post?>*/ List<Post>
/*
Account with Post Data Access Object:
Account With Post is NOT a table, a Post contains the reference
Commented out
*/
//#Insert(onConflict = OnConflictStrategy.IGNORE)
//fun insert(join: AccountWithPost)
#Transaction
#Query("SELECT * FROM accounts")
fun getAllAccountsWithTheirPosts(): List<AccountWithPost>
}
#Database(entities = [Account::class,Post::class], exportSchema = false, version = 1)
abstract class TheDatabase: RoomDatabase() {
abstract fun getAllDao(): AllDao
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()
.build()
}
return instance as TheDatabase
}
}
}
Please refer to the comments contained in the code
In addition to the above the code in the Activity (MainActivity) is :-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AllDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getAllDao()
val fbId = dao.insertAccount(Account(0,"Fred","Blogs","fred_bloggs#email.com"))
val msId = dao.insertAccount(Account(0,"Mary","Smith","m_smith#email.com"))
val jdId = dao.insertAccount(Account(0,"Jane","Doe","jane_doe#email.com"))
dao.insertPost(Post(0,fbId.toInt(),"FBP001","blah for fb p001"))
dao.insertPost(Post(0,fbId.toInt(),"FBP002","blah for fb p002"))
dao.insertPost(Post(0,fbId.toInt(),"FBP003","blah for fb p003"))
dao.insertPost(Post(0,fbId.toInt(),"FBP004","blah for fb p004"))
dao.insertPost(Post(0,fbId.toInt(),"FBP005","blah for fb p005"))
dao.insertPost(Post(0,msId.toInt(),"MSP001","blah for ms p001"))
dao.insertPost(Post(0,msId.toInt(),"MSP002","blah for ms p002"))
dao.insertPost(Post(0,jdId.toInt(),"JDP001","blah for jd p001"))
val sb = StringBuilder()
for(awp in dao.getAllAccountsWithTheirPosts()) {
sb.clear()
for (p in awp.posts) {
sb.append("\n\tPost Title is ${p.title} Content is ${p.content} ID is ${p.post_id} References User ${p.postUserId}")
}
Log.d("DBINFOI","Account FirstName is ${awp.account.firstName} " +
"Lastname is ${awp.account.lastName} " +
"Email is ${awp.account.email} ID is ${awp.account.id} " +
"The account has ${awp.posts.size} posts, if any they are:-$sb"
)
}
}
}
Result
The output sent to the log is:-
D/DBINFOI: Account FirstName is Fred Lastname is Blogs Email is fred_bloggs#email.com ID is 1 The account has 5 posts, if any they are:-
Post Title is FBP001 Content is blah for fb p001 ID is 1 References User 1
Post Title is FBP002 Content is blah for fb p002 ID is 2 References User 1
Post Title is FBP003 Content is blah for fb p003 ID is 3 References User 1
Post Title is FBP004 Content is blah for fb p004 ID is 4 References User 1
Post Title is FBP005 Content is blah for fb p005 ID is 5 References User 1
D/DBINFOI: Account FirstName is Mary Lastname is Smith Email is m_smith#email.com ID is 2 The account has 2 posts, if any they are:-
Post Title is MSP001 Content is blah for ms p001 ID is 6 References User 2
Post Title is MSP002 Content is blah for ms p002 ID is 7 References User 2
D/DBINFOI: Account FirstName is Jane Lastname is Doe Email is jane_doe#email.com ID is 3 The account has 1 posts, if any they are:-
Post Title is JDP001 Content is blah for jd p001 ID is 8 References User 3

Android Room embedded relationship query results

I am currently trying to get results from a query of nested relationships in Room. Here Are my classes/database entities involved:
#Entity
data class PrayerRequestEntity(
var title: String,
var details: String,
var category: String,
var isAnswered: Boolean,
var prayerCount: Int,
var dateCreated: Date,
var dateUpdated: Date
) {
#PrimaryKey(autoGenerate = true)
var prayerRequestId: Int = 0
}
#Entity
data class PrayerPlanEntity(
var title: String,
var sessionCount: Int,
var dateCreated: Date,
var dateUpdated: Date
) {
#PrimaryKey(autoGenerate = true)
var planId: Int = 0
}
#Entity(primaryKeys = ["planId", "prayerRequestId"])
data class PrayerPlanPrayerRequestJoinEntity(
val planId: Int,
val prayerRequestId: Int
)
I am trying to get a list of PrayerPlanEtities along with the PrayerRequestEntities associated with each and have no problem using the following return:
data class PrayerPlanPrayerRequestsJoin(
#Embedded
val prayerPlan: PrayerPlanEntity,
#Relation(
parentColumn = "planId",
entityColumn = "prayerRequestId",
associateBy = Junction(PrayerPlanPrayerRequestJoinEntity::class)
)
val prayers: List<PrayerPlanEntity>
)
I need to also query the relationships of PrayerRequestEntity in that same query so Imodify the aforementioned class like so:
data class PrayerPlanPrayerRequestsJoin(
#Embedded
val prayerPlan: PrayerPlanEntity,
#Relation(
parentColumn = "planId",
entityColumn = "prayerRequestId",
associateBy = Junction(PrayerPlanPrayerRequestJoinEntity::class)
)
val prayers: List<PrayerRequestJoin>
)
PrayerRequestJoin is a previous relationship I created which works perfectly fine on it own and looks like so:
data class PrayerRequestJoin(
#Embedded
val prayerRequest: PrayerRequestEntity,
#Relation(
parentColumn = "prayerRequestId",
entityColumn = "prayerRequestCategoryId",
associateBy = Junction(PrayerRequestCategoryJoinEntity::class)
)
val category: PrayerRequestCategoryEntity,
#Relation(
parentColumn = "prayerRequestId",
entityColumn = "photoId",
associateBy = Junction(PrayerRequestPhotoJoinEntity::class)
)
val photos: List<PhotoEntity>,
#Relation(
parentColumn = "prayerRequestId",
entityColumn = "noteId",
associateBy = Junction(PrayerRequestNoteJoinEntity::class)
)
val notes: List<NoteEntity>
)
But now I am getting two build errors an Android Studio:
error: constructor PrayerPlanPrayerRequestsJoin in class PrayerPlanPrayerRequestsJoin cannot be applied to given types;
_item = new PrayerPlanPrayerRequestsJoin();
^
required: PrayerPlanEntity,List
found: no arguments
reason: actual and formal argument lists differ in length
and also:
error: prayerPlan has private access in PrayerPlanPrayerRequestsJoin
_item.prayerPlan = _tmpPrayerPlan;
Can anyone provide some insight on what may be causing this issue?
I believe that your issue is with the #Query's rather than with the relationships.
Upon closer inspection the messages shows java code e.g. termination with ;'s.
Therefore the issue is within the generated java and it would appear (from creating a working example) that the issue is in the code underlying the class or classes annotated with #Dao
i.e. _item does not appear in the java for the class annotated with #Database. Whilst _item = new .... appears for each #Query annotated function. e.g.
for a Query using SELECT * FROM PrayerPlanPrayerRequestJoinEntity then the code includes _item = new PrayerPlanPrayerRequestJoinEntity(_tmpPlanId,_tmpPrayerRequestId);
for a Query using "SELECT * FROM prayerRequestEntity" then the code includes _item = new PrayerRequestJoin(_tmpPrayerRequest,_tmpCategory_1,_tmpPhotosCollection_1,_tmpNotesCollection_1);
As can be seen these are used to build the final result. The first example being the closest from the working example but there is no issue.
As such I believe that the issue is not with the relationships (working example runs OK) but the issue is with the #Query annotated functions.
I would suggest commenting them all out and adding each until you find the culprit.
The working example
Note that this example introduces/uses some shortcuts so the code differs a little.
the code is run on the main thread so uses .allowMainThreadQueries
Long's are used instead of Ints for id's (they should really always be longs as they can be signed 64 bit)
Autogenerate has not been used for classes that have been guessed such as NoteEntity (autogenerate (which equates to AUTOINCREMENT) is actually not recommended by SQLite themselves The AUTOINCREMENT keyword imposes extra CPU, memory, disk space, and disk I/O overhead and should be avoided if not strictly needed. It is usually not needed. https://sqlite.org/autoinc.html)
TypeConverters have been avoided
The code:-
NoteEntity made up just to test, so very simple
#Entity
data class NoteEntity(
#PrimaryKey
var noteId: Long? = null,
var noteText: String = ""
)
PhotoEntity made up ....
#Entity
data class PhotoEntity(
#PrimaryKey
var photoId: Long? = null,
var photoImage: String = "unknown"
)
PrayerPlanEntity
#Entity
data class PrayerPlanEntity(
var title: String,
var sessionCount: Int,
var dateCreated: String, /* changed for brevity */
var dateUpdated: String /* changed for brevity */
) {
#PrimaryKey(autoGenerate = true)
var planId: Long = 0
}
PrayerPlanPrayerRequestJoinEntity
#Entity(primaryKeys = ["planId", "prayerRequestId"])
data class PrayerPlanPrayerRequestJoinEntity(
val planId: Long,
val prayerRequestId: Long
)
PrayerPlanPrayerRequestsJoin assume the 2nd is as it is now
data class PrayerPlanPrayerRequestsJoin(
/*
#Embedded
val prayerPlan: PrayerPlanEntity,
#Relation(
parentColumn = "planId",
entityColumn = "prayerRequestId",
associateBy = Junction(PrayerPlanPrayerRequestJoinEntity::class)
)
val prayers: List<PrayerPlanEntity>
*/
#Embedded
val prayerPlan: PrayerPlanEntity,
#Relation(
parentColumn = "planId",
entityColumn = "prayerRequestId",
associateBy = Junction(PrayerPlanPrayerRequestJoinEntity::class)
)
val prayers: List<PrayerRequestJoin>
)
PrayerRequestCategoryEntity made up ....
#Entity
data class PrayerRequestCategoryEntity(
#PrimaryKey
var prayerRequestCategoryId: Long? = null,
var categoryName: String
)
PrayerRequestCategoryJoinEntity made up ....
#Entity(primaryKeys = ["prayerRequestId", "prayerRequestCategoryId"])
data class PrayerRequestCategoryJoinEntity(
val prayerRequestId: Long,
val prayerRequestCategoryId: Long
)
PrayerRequestEntity
#Entity
data class PrayerRequestEntity(
var title: String,
var details: String,
var category: String,
var isAnswered: Boolean,
var prayerCount: Int,
var dateCreated: String, /* changed for brevity */
var dateUpdated: String /* changed for brevity */
) {
#PrimaryKey(autoGenerate = true)
var prayerRequestId: Long = 0
}
PrayerRequestJoin
data class PrayerRequestJoin(
#Embedded
val prayerRequest: PrayerRequestEntity,
#Relation(
parentColumn = "prayerRequestId",
entityColumn = "prayerRequestCategoryId",
associateBy = Junction(PrayerRequestCategoryJoinEntity::class)
)
val category: PrayerRequestCategoryEntity,
#Relation(
parentColumn = "prayerRequestId",
entityColumn = "photoId",
associateBy = Junction(PrayerRequestPhotoJoinEntity::class)
)
val photos: List<PhotoEntity>,
#Relation(
parentColumn = "prayerRequestId",
entityColumn = "noteId",
associateBy = Junction(PrayerRequestNoteJoinEntity::class)
)
val notes: List<NoteEntity>
)
PrayerRequestNoteJoinEntity made up
#Entity(primaryKeys = ["prayerRequestId", "noteId"])
data class PrayerRequestNoteJoinEntity(
val prayerRequestId: Long,
val noteId: Long
)
PrayerRequestPhotoJoinEntity made up ....
#Entity(primaryKeys = ["prayerRequestId", "photoId"])
data class PrayerRequestPhotoJoinEntity(
val prayerRequestId: Long,
val photoId: Long
)
AllDAO made up
#Dao
abstract class AllDAO {
#Insert(onConflict = OnConflictStrategy.IGNORE)
abstract fun insert(photoEntity: PhotoEntity): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
abstract fun insert(noteEntity: NoteEntity): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
abstract fun insert(prayerPlanEntity: PrayerPlanEntity): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
abstract fun insert(prayerRequestCategoryEntity: PrayerRequestCategoryEntity): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
abstract fun insert(prayerRequestEntity: PrayerRequestEntity): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
abstract fun insert(prayerPlanPrayerRequestJoinEntity: PrayerPlanPrayerRequestJoinEntity): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
abstract fun insert(prayerRequestCategoryJoinEntity: PrayerRequestCategoryJoinEntity): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
abstract fun insert(prayerRequestNoteJoinEntity: PrayerRequestNoteJoinEntity): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
abstract fun insert(prayerRequestPhotoJoinEntity: PrayerRequestPhotoJoinEntity): Long
#Query("SELECT * FROM prayerRequestEntity")
abstract fun getAllPrayerRequests(): List<PrayerRequestEntity>
#Query("SELECT * FROM prayerPlanEntity")
abstract fun getAllPrayerPlans(): List<PrayerPlanEntity>
#Query("SELECT * FROM PrayerPlanPrayerRequestJoinEntity")
abstract fun getAllPrayerPlanRequestJoins(): List<PrayerPlanPrayerRequestJoinEntity>
#Query("SELECT * FROM prayerRequestEntity")
abstract fun getAllPrayerRequestsWithCategoryNotesAndPhotos(): List<PrayerRequestJoin>
}
TheDatabase made up
#Database(entities = [
PhotoEntity::class,
NoteEntity::class,
PrayerRequestEntity::class,
PrayerPlanEntity::class,
PrayerRequestCategoryEntity::class,
PrayerRequestCategoryJoinEntity::class,
PrayerRequestNoteJoinEntity::class,
PrayerRequestPhotoJoinEntity::class,
PrayerPlanPrayerRequestJoinEntity::class]
, version = 1,
exportSchema = false
)
abstract class TheDatabase: RoomDatabase() {
abstract fun getAllDAO(): AllDAO
companion object {
private var instance: TheDatabase? = null
fun getInstance(context: Context): TheDatabase {
if (instance == null) {
instance = Room.databaseBuilder(context,TheDatabase::class.java,"prayer.db")
.allowMainThreadQueries()
.build()
}
return instance as TheDatabase
}
}
}
Last but not least an activity to do a simple test of the related data via the getAllPrayerRequestsWithCategoryNotesAndPhotos query:-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AllDAO
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getAllDAO()
val n1id = dao.insert(NoteEntity(noteText = "Note1"))
val n2id = dao.insert(NoteEntity(noteText = "Note2"))
val n3id = dao.insert(NoteEntity(noteText = "Note3"))
val photo1id = dao.insert(PhotoEntity(photoImage = "photo1"))
val photo2id = dao.insert(PhotoEntity(photoImage = "photo2"))
val photo3id = dao.insert(PhotoEntity(photoImage = "photo3"))
val pp1id = dao.insert(PrayerPlanEntity(title = "PP01",10,"today","never"))
val pcat01id = dao.insert(PrayerRequestCategoryEntity(categoryName = "CAT01"))
val pcat02id = dao.insert(PrayerRequestCategoryEntity(categoryName = "CAT02"))
val pcat03id = dao.insert(PrayerRequestCategoryEntity(categoryName = "CAT03"))
val pr01id =dao.insert(PrayerRequestEntity("PR01","details01","cat01",false,10,"today","never"))
dao.insert(prayerPlanPrayerRequestJoinEntity = PrayerPlanPrayerRequestJoinEntity(prayerRequestId = pr01id, planId = pp1id))
dao.insert(PrayerRequestPhotoJoinEntity(pr01id,photo1id))
dao.insert(PrayerRequestPhotoJoinEntity(pr01id,photo2id))
dao.insert(PrayerRequestPhotoJoinEntity(pr01id,photo3id))
dao.insert(PrayerRequestCategoryJoinEntity(pr01id,n1id))
dao.insert(PrayerRequestNoteJoinEntity(pr01id,n2id))
dao.insert(PrayerRequestNoteJoinEntity(pr01id,n3id))
dao.insert(PrayerRequestCategoryJoinEntity(pr01id,pcat01id))
/* Extract the Data and output to the log */
for (prj: PrayerRequestJoin in dao.getAllPrayerRequestsWithCategoryNotesAndPhotos()) {
Log.d("DBINFO","Prayer Request = ${prj.prayerRequest.details} \n\tRequestCategory = ${prj.category.categoryName} ID is ${prj.category.prayerRequestCategoryId}")
for (photos: PhotoEntity in prj.photos) {
Log.d("DBINFO","\tPhoto = ${photos.photoImage} ID = ${photos.photoId}")
}
for (notes: NoteEntity in prj.notes) {
Log.d("DBINFO","\tNote = ${notes.noteText} ID = ${notes.noteId}")
}
}
}
}
Finally the result from the Log:-
2022-03-24 12:52:35.758 D/DBINFO: Prayer Request = details01
RequestCategory = CAT01 ID is 1
2022-03-24 12:52:35.758 D/DBINFO: Photo = photo1 ID = 1
2022-03-24 12:52:35.758 D/DBINFO: Photo = photo2 ID = 2
2022-03-24 12:52:35.758 D/DBINFO: Photo = photo3 ID = 3
2022-03-24 12:52:35.758 D/DBINFO: Note = Note2 ID = 2
2022-03-24 12:52:35.759 D/DBINFO: Note = Note3 ID = 3

ROOM database entity modeling

There must be a better way of doing this. I want to create a database table with all my clothing and have subcategories of clothing, like outerwear, dresses, shoes, etc. They all will have the same attributes (Id, name, image, about, price). Couldn't I create one table? I believe this is a One-to-Many relationship.
#Serializable
#Entity(tableName = CLOTHING_DATABASE_TABLE)
data class Clothing(
#PrimaryKey(autoGenerate = false)
val id: Int,
val name: String,
val image: String,
val about: String,
val price: String
)
#Serializable
#Entity(tableName = POPULAR_DATABASE_TABLE)
data class Popular(
#PrimaryKey(autoGenerate = false)
val popularId: Int,
val name: String,
val image: String,
val about: String,
val price: String
)
#Serializable
#Entity(tableName = OUTERWEAR_DATABASE_TABLE)
data class Outerwear(
#PrimaryKey(autoGenerate = false)
val outerwearId: Int,
val name: String,
val image: String,
val about: String,
val price: String
)
#Serializable
#Entity(tableName = TOPS_DATABASE_TABLE)
data class Tops(
#PrimaryKey(autoGenerate = false)
val topsId: Int,
val name: String,
val image: String,
val about: String,
val price: String
)
#Serializable
#Entity(tableName = SWIMWEAR_DATABASE_TABLE)
data class Swimwear(
#PrimaryKey(autoGenerate = false)
val swimwearId: Int,
val name: String,
val image: String,
val about: String,
val price: String
)
#Serializable
#Entity(tableName = SHOES_DATABASE_TABLE)
data class Shoes(
#PrimaryKey(autoGenerate = false)
val shoesId: Int,
val name: String,
val image: String,
val about: String,
val price: String
)
#Serializable
#Entity(tableName = BUNDLES_DATABASE_TABLE)
data class Bundles(
#PrimaryKey(autoGenerate = false)
val bundlesId: Int,
val name: String,
val image: String,
val about: String,
val price: String
)
#Serializable
#Entity(tableName = DRESSES_DATABASE_TABLE)
data class Dresses(
#PrimaryKey(autoGenerate = false)
val dressesId: Int,
val name: String,
val image: String,
val about: String,
val price: String
)
#Serializable
#Entity(tableName = PAJAMAS_DATABASE_TABLE)
data class Pajamas(
#PrimaryKey(autoGenerate = false)
val pajamasId: Int,
val name: String,
val image: String,
val about: String,
val price: String
)
#Serializable
#Entity(tableName = ACCESSORIES_DATABASE_TABLE)
data class Accessories(
#PrimaryKey(autoGenerate = false)
val accessoriesId: Int,
val name: String,
val image: String,
val about: String,
val price: String
)
You would typically have either 2 or 3 tables (3 for a many-many i.e. an item of clothing could have multiple sub-categories).
For one-many have a clothing table which has a column for the sub-category that references(relates) to the single sub-category and a sub-category table that is referenced according to a unique column (the primary key).
For the many-many you have the clothing table (without the column to reference the single sub-category), the sub-category table and then a third table that has two columns, one for the reference to the clothing and the other for the reference to the sub-category with the primary key being a composite of both.
So you could have:-
#Entity(tableName = CLOTHING_DATABASE_TABLE)
data class Clothing(
#PrimaryKey(autoGenerate = false)
val id: Long, /* should really be Long as */
val subCategoryReference: Long, /*<<<<< ADDED for the 1 subcategory */
val name: String,
val image: String,
val about: String,
val price: String
)
and :-
#Entity(tableName = SUBCATEGORY_DATABASE_TABLE)
data class SubCategory(
#PrimaryKey
val id: Long?,
val subCategoryName: String
)
to enforce referential integrity you could add a foreign key constraint to the subCategoryReference column of the clothing table.
If you wanted a many-many, allowing a clothing to have multiple sub-categories then you could have the third table as :-
#Entity(
tableName = CLOTHING_SUBCATEGORY_MAP_DATABASE_TABLE,
primaryKeys = ["clothingMap","subcategoryMap"],
)
data class ClothingSubCategoryMap(
val clothingMap: Long,
#ColumnInfo(index = true)
val subcategoryMap: Long
)
Of course you could have a single clothing table and just have a column for the sub-category. However this would be considered as not being normalised as you would be duplicating the sub-category throughout.
Example 1-many (i.e. using the 2 tables Clothing and SubCategory)
As you would very likely want to retrieve clothing along with it's sub-category then you would have a POJO that uses the #Embedded and #Relation annotations e.g.
data class ClothingWithSingleSubCategory (
#Embedded
val clothing: Clothing,
#Relation(
entity = SubCategory::class,
parentColumn = "subCategoryReference",
entityColumn = "id"
)
val subCategory: SubCategory
)
You could then have the following as an #Dao annotated class :-
#Dao
interface AllDao {
#Insert(onConflict = IGNORE)
fun insert(clothing: Clothing): Long
#Insert(onConflict = IGNORE)
fun insert(subCategory: SubCategory): Long
#Transaction
#Query("SELECT * FROM clothing")
fun getAllClothingWithSubCategory(): List<ClothingWithSingleSubCategory>
}
With a suitable #Database annotated class you could then have something like the following in an activity:-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AllDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getAllDao()
val sc_popular = dao.insert(SubCategory(null,"Popular"))
val sc_outerwear = dao.insert(SubCategory(null,"OuterWear"))
val sc_tops = dao.insert(SubCategory(null,"Tops"))
val sc_swimwear = dao.insert(SubCategory(100,"Swimwear"))
val sc_shoes = dao.insert(SubCategory(null,"Shoes"))
val sc_dresses = dao.insert(SubCategory(null,"Dresses"))
val sc_pyjamas = dao.insert(SubCategory(null,"Pyjamas"))
dao.insert(Clothing(100200300400,sc_popular,"Jeans","jeans_image","blah","100.55"))
dao.insert(Clothing(100200300500,sc_outerwear,"Anorak","anorak_image","blah","214.55"))
for (cwsc: ClothingWithSingleSubCategory in dao.getAllClothingWithSubCategory()) {
Log.d("DBINFO","Name = ${cwsc.clothing.name} Price is ${cwsc.clothing.price} Sub-Category is ${cwsc.subCategory.subCategoryName}")
}
}
}
When run the log would then include:-
D/DBINFO: Name = Jeans Price is 100.55 Sub-Category is Popular
D/DBINFO: Name = Anorak Price is 214.55 Sub-Category is OuterWear
Example many-many
Like the 1-many you will want a POJO BUT one that has a List of sub-categories obtained via the mapping table. This uses the #Embeded annotation and the #Relation annotation but extended to include the associateBy to inform Room about the intermediate table. So you could have:-
data class ClothingWithListOfSubCategories(
#Embedded
val clothing: Clothing,
#Relation(
entity = SubCategory::class,
parentColumn = "id",
entityColumn = "id",
associateBy = Junction(
value = ClothingSubCategoryMap::class,
parentColumn = "clothingMap",
entityColumn = "subcategoryMap"
)
)
val subCategories: List<SubCategory>
)
The you could have the following in an #Dao annotated class:-
/* ADDED for many-many */
#Insert(onConflict = IGNORE)
fun insert(clothingSubCategoryMap: ClothingSubCategoryMap): Long
#Transaction
#Query("SELECT * FROM clothing")
fun getAllClothingWithSubCategories(): List<ClothingWithListOfSubCategories>
and if the activity were extended to include :-
/* Added for many-many */
/* Note utilises clothing and sub-categories above */
dao.insert(ClothingSubCategoryMap(jeans,sc_popular))
dao.insert(ClothingSubCategoryMap(jeans,sc_swimwear))
dao.insert(ClothingSubCategoryMap(jeans,sc_shoes))
dao.insert(ClothingSubCategoryMap(anorak,sc_popular))
dao.insert(ClothingSubCategoryMap(anorak,sc_outerwear))
for(cwlsc: ClothingWithListOfSubCategories in dao.getAllClothingWithSubCategories()) {
Log.d("DBINFO","Name = ${cwlsc.clothing.name} Price is ${cwlsc.clothing.price} it is in ${cwlsc.subCategories.size} sub-categories. They are:-")
for(sc: SubCategory in cwlsc.subCategories) {
Log.d("DBINFO","\t${sc.subCategoryName}")
}
}
The the log would also include :-
D/DBINFO: Name = Jeans Price is 100.55 it is in 3 sub-categories. They are:-
D/DBINFO: Popular
D/DBINFO: Swimwear
D/DBINFO: Shoes
D/DBINFO: Name = Anorak Price is 214.55 it is in 2 sub-categories. They are:-
D/DBINFO: Popular
D/DBINFO: OuterWear

Android Room - Many-to-many relationship not returning relation entities

I've followed the documentation provided on android developer guides and a Medium article.
I'm trying to return a playlist of songs, but want a list of entities and not just the IDs. Following the above links I have this.
My Entities:
#Entity
data class MediaEntity(
#PrimaryKey val identifier: String,
val imageUrl: String,
val title: String,
val description: String,
val media: String,
val duration: Double,
val progress: Int,
val listenedToCompletion: Boolean
)
#Entity
data class PlaylistEntity(
#PrimaryKey val playlistId: String,
val playlistName: String,
val playlistDescription: String = "",
val currentPosition: Int = 0
)
#Entity(primaryKeys = ["playlistId", "identifier"])
data class PlaylistMediaLinkEntity(
val playlistId: String,
val identifier: String
)
My DAO for the link table is as follows:
#Dao
interface PlaylistMediaLinkDao {
#Transaction
#Query("SELECT * FROM PLAYLISTENTITY")
fun getPlaylistWithMediaItems(): List<MediaInPlaylist>
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(playlistMediaLinkEntity: PlaylistMediaLinkEntity)
}
And then my media in playlist object:
class MediaInPlaylist() {
#Embedded
lateinit var playlist: PlaylistEntity
#Relation(
parentColumn = "playlistId",
entity = MediaEntity::class,
entityColumn = "identifier",
associateBy = Junction(
value = PlaylistMediaLinkEntity::class,
parentColumn = "playlistId",
entityColumn = "identifier"
)
)
lateinit var mediaEntities: List<MediaEntity>
}
I can confirm my PlaylistMediaLinkEntity is populated with playlist-media id pairs, but when calling getAllPlaylistsWithMediaItems, the MediaInPlaylist object is returned with the Playlist data, but the list of mediaEntries is empty.
Have I left something out or where am I going wrong? What I've done matches most examples online.

Categories

Resources