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
Related
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
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
I am trying to understand how to use Room with relational tables. I have created a Job model, that has a list of locations and therefore needs a 1-to-many relation between the Job and Location object. For that, I have created a JobWrapper data class to hold both the Job and the locations. But, when building I get the following error:
The class must be either #Entity or #DatabaseView. -
java.util.Collectionerror: Entities and POJOs must have a usable
public constructor. You can have an empty constructor or a constructor
whose parameters match the fields (by name and type). -
java.util.Collection \models\JobWrapper.java:12: error: Cannot find
the child entity column parentId in java.util.Collection.
Options:
private java.util.Collection<models.Location> locations;
public final class JobWrapper {
^ Tried the following constructors but they failed to match:
JobWrapper(models.Job,java.util.Collection<models.Location>)
-> [param:job -> matched field:job, param:locations -> matched field:unmatched] models\JobWrapper.java:9:
error: Cannot find setter for field.
I notice that it at least cannot find the locations table. But, I do not know how to handle the problem. The problem did not appear while reading from the database - it first appeared when I was trying to put data into the database with my JobDAO. I have already spent a day trying to solve it and are therefore searching for a solution or some advise on how to solve it.
Note: I have been following the following guides:
https://developer.android.com/training/data-storage/room/relationships#one-to-many
https://dev.to/normanaspx/android-room-how-works-one-to-many-relationship-example-5ad0
Here follows some relevant code snippets from my projects:
JobWrapper.kt
data class JobWrapper(
#Embedded val job: Job,
#Relation(
parentColumn = "jobid",
entityColumn = "parentId"
) var locations : Collection<Location>
)
Job
#Entity
data class Job (
#PrimaryKey
#NonNull
var jobid : String,
#NonNull
#ColumnInfo(name = "job_status")
var status : JobStatus,
#NonNull
#SerializedName("createdByAuth0Id")
var creator : String,
#SerializedName("note")
var note : String?,
#NonNull
var organisationId : String,
#NonNull
var type : JobType,
#SerializedName("atCustomerId")
#NonNull
#ColumnInfo(name = "working_at_customer_id")
var workingAtCustomerId : String,
#SerializedName("toCustomerId")
#NonNull
#ColumnInfo(name = "working_to_customer_id")
var workingToCustomerId : String,
)
JobStatus.kt
enum class JobStatus {
CREATED,
READY,
IN_PROGRESS,
FINISHED
}
Location.kt
#Entity
data class Location (
#PrimaryKey(autoGenerate = true)
var entityId: Long,
#NonNull
var parentId: String,
#NonNull
var locationId: String,
#NonNull
var type: String
) {
constructor() : this(0, "", "", "")
}
JobDao.kt
#Dao
interface JobDAO {
#Transaction
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(job: JobWrapper)
#Transaction
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(jobs: List<JobWrapper>)
#Transaction
#Update
fun update(job: JobWrapper)
#Transaction
#Delete
fun delete(job: JobWrapper)
#Transaction
#Query("DELETE FROM Job")
fun deleteAll()
#Transaction
#Query("SELECT * FROM Job")
fun getAll(): LiveData<List<JobWrapper>>
}
As Kraigolas has pointed out you can only use JobWrapper directly to extract data you need to insert/delete/update via the actual underlying Entities.
Consider the following
(unlike Kraigolas's solultion the extended functions are in the JobDao rather than in a repository(swings and roundabouts argument as to which is better))
note to test I've made some changes for brevity and convenience so the you would have to amend to suit.
JobDao
#Dao
interface JobDAO {
/* Core/Underlying DAO's */
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(job: Job): Long
#Insert()
fun insert(location: List<Location>): List<Long>
#Transaction
#Delete
fun delete(location: List<Location>)
#Delete
fun delete(job: Job)
#Update
fun update(job: Job)
#Update
fun update(location: List<Location>)
#Transaction
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(job: JobWrapper): Long {
val rv =insert(job.job)
insert(job.locations)
return rv
}
#Transaction
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(jobs: List<JobWrapper>) {
for (jw: JobWrapper in jobs) {
insert(jw)
}
}
#Transaction
#Update
fun update(job: JobWrapper) {
update(job.locations)
update(job.job)
}
/* Delete according to JobWrapper allowing optional deletion of locations */
#Transaction
#Delete
fun delete(job: JobWrapper, deleteChildLocations: Boolean) {
if (deleteChildLocations) {
delete(job.locations)
}
delete(job.job)
}
/* will orphan locations as is */
/* Note using Foreign Keys in Location (to Job) with ON DELETE CASCADE */
#Transaction
#Query("DELETE FROM Job")
fun deleteAll()
#Transaction
#Query("SELECT * FROM Job")
fun getAll(): List<JobWrapper>
#Transaction
#Query("SELECT * FROM job WHERE jobid = :jobid")
fun getJobWrapperByJobId(jobid: String ): JobWrapper
}
As can be seen the Core Dao's include Job and Location actions
The JobWrapper actions invoke the Core actions
List have been used instead of Collections for my convenience (aka I only dabble with Kotlin)
Instead of JobType, type has been changed to use String for convenience
As I've tested this the demo/example used follows (obviously JobDao is as above)
The POJO's and Entities used are/were :-
JobWrapper
data class JobWrapper(
#Embedded val job: Job,
#Relation(
parentColumn = "jobid",
entityColumn = "parentId",
entity = Location::class
) var locations : List<Location>
)
List instead of Collection
Job
#Entity
data class Job (
#PrimaryKey
#NonNull
var jobid : String,
#NonNull
#ColumnInfo(name = "job_status")
var status : String,
#NonNull
var creator : String,
var note : String?,
#NonNull
var organisationId : String,
#NonNull
var type : String,
#NonNull
#ColumnInfo(name = "working_at_customer_id")
var workingAtCustomerId : String,
#NonNull
#ColumnInfo(name = "working_to_customer_id")
var workingToCustomerId : String,
)
largely the same but String rather than objects for convenience
Location
#Entity(
foreignKeys = [
ForeignKey(
entity = Job::class,
parentColumns = ["jobid"],
childColumns = ["parentId"],
onUpdate = ForeignKey.CASCADE,
onDelete = ForeignKey.CASCADE
)
])
data class Location (
#PrimaryKey(autoGenerate = true)
var entityId: Long,
#NonNull
var parentId: String,
#NonNull
var locationId: String,
#NonNull
var type: String
) {
constructor() : this(0, "", "", "")
}
Foreign Key added for referential integrity and also CASCADE deletes (UPDATE CASCADE not really required unless you change a jobid, other updates aren't cascaded (and wouldn't need to be))
#Database used JobDatabase
#Database(entities = [Location::class,Job::class],version = 1)
abstract class JobDatabase: RoomDatabase() {
abstract fun getJobDao(): JobDAO
companion object {
var instance: JobDatabase? = null
fun getInstance(context: Context): JobDatabase {
if (instance == null) {
instance = Room.databaseBuilder(context, JobDatabase::class.java, "job.db")
.allowMainThreadQueries()
.build()
}
return instance as JobDatabase
}
}
}
allowMainThreadQueries used to allow testing on main thread
The test/demo activity MainActivity
class MainActivity : AppCompatActivity() {
lateinit var db: JobDatabase
lateinit var dao: JobDAO
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = JobDatabase.getInstance(this)
dao = db.getJobDao()
var jobId: String = "Job1"
var jw = JobWrapper(
Job(
jobId,
"x",
"Fred",
"Note for Job1",
"Org1",
"A","Cust1",
"Cust1"),
listOf(
Location(0,jobId,"loc1","J"),
Location(0,jobId,"Loc2","K"),
Location(0,jobId,"Loc3","L")
)
)
dao.insert(jw)
dao.insertAll(createJobWrapperList(10))
dao.delete(dao.getJobWrapperByJobId("job6"),true)
var jobWrapper = dao.getJobWrapperByJobId("job7")
jobWrapper.job.creator = "update creator"
for (l in jobWrapper.locations) {
if (l.type == "M") l.type= "UPDATED"
}
dao.update(jobWrapper)
}
fun createJobWrapperList(numberToCreate: Int): List<JobWrapper> {
val l = mutableListOf<JobWrapper>()
for(i in 1..numberToCreate) {
var jid = "job$i"
l.add(
JobWrapper(
Job(jid,"X","Creator$i","Note for $jid","org$jid","T","custA","custT"),
arrayListOf(
Location(0,jid,"loc_$jid.1","L"),
Location(0,jid,"loc_$jid.2","M"),
Location(0,jid,"loc_$jid.3","N")
)
)
)
}
return l.toList()
}
}
This :-
gets DB instance and the dao.
adds a job and it's location via a single JobWrapper
adds x (10) jobs and 3 locations per Job via a List of JobWrappers generated via the createJobWrapperList function.
4.deletes the a JobWrapper obtained via getJobWrapperByJobId including deleting the underlying locations (true) using delete for the JobWrapper associated with the jobid "job6".
obtains the JobWrapper associated with "job7" changes the creator and changes the locations that have a type of "M" to "UPDATED" (just the one) and then uses the update(JobWrapper) to apply the updates.
WARNING
Inserting using a JobWrapper, as it has REPLACE conflict strategy will result in additional locations if it replaces as the entityId will always be generated.
Result
Runinng the above results in :-
Job Table :-
As can be seen job6 has been deleted (11 rows added 10 left) and job7's creator has been updated.
Location Table :-
As can be seen no job6 locations (was 33 locations (11 * 3) now 30) and the location that was type M has been updated according to the JobWrapper passed.
You asked :-
How do I ensure the relationship to the right parent (job) when inserting the childs (locations)?
I think the above demonstrates how.
JobWrapper does not have an associated table with it. Note that it is nice for pulling from your database because it will grab from the Location table and the Job table, but it makes no sense to insert into your database with a JobWrapper because there is no associated table for it.
Instead, you need to insert Jobs, and Locations separately. The database can query for them together because they are related by jobid and parentid, so you don't have to worry about them losing their relationship to each other, and you can still query for your JobWrapper.
If given my above explanation you still think that you should be able to insert a JobWrapper, then you might create a repository with a method like:
suspend fun insertWrapper(wrapper : JobWrapper){
dao.insertJob(wrapper.job)
dao.insertLocations(wrapper.locations)
}
Where insertJob inserts a single Job, and insertLocations inserts a list of Location.
I am building a database using Room and I can't figure out how to insert the new elements that have a relationship (one to many in my case) into the database. No solution has ever talked about the insertion (they only talk about querying the data).
Here is the DAO:
#Dao
abstract class ShoppingListsDao {
#Insert
abstract suspend fun addNewShoppingList(newShoppingList: ShoppingList)
#Insert
abstract suspend fun addNewItem(newItem: Item)
// This is how I thought it would work but it didn't
#Insert
#Transaction
abstract suspend fun addNewShoppingListWithItems(newShoppingListWithItems: ShoppingListWithItems)
}
Here are my entities:
#Entity
class ShoppingList(
#PrimaryKey(autoGenerate = true)
val listID: Int,
val ListName: String
)
#Entity(foreignKeys = [ForeignKey(
entity = ShoppingList::class,
parentColumns = ["listID"],
childColumns = ["parentListID"]
)])
class Item(
#PrimaryKey(autoGenerate = true)
var itemID: Int,
val name: String,
val quantity: Int,
val parentListID: Int
)
There isn't a way that I am aware of that lets you directly insert a compound entity (like ShoppingListWithItems). You have to just insert the individual entities to their tables.
In your example, you would want to define an insert method for your ShoppingList entity which returns the generated primary key (so you can use it for your other items) and an insert method for your Item entities which can insert a whole list of them.
#Insert
suspend fun addNewShoppingList(newShoppingList: ShoppingList): Long
#Insert
suspend fun addNewItems(newItems: List<Item>)
Then you can run a transaction to insert them in a batch.
#Transaction
suspend fun addNewShoppingListWithItems(shoppingList: ShoppingList, items: List<Item>) {
val listId = addNewShoppingList(shoppingList)
items.forEach { it.parentListId = listId }
addNewItems(items)
}
Here's a good resource to understand one-to-many relationships-> https://developer.android.com/training/data-storage/room/relationships#one-to-many
You can create an embedded object for 'ShoppingListWithItems' as (more on embedded objects - https://developer.android.com/training/data-storage/room/relationships#nested-objects):
data class ShoppingListWithItems(
#Embedded val shoppingList: ShoppingList,
#Relation(parentColumn = "listID", entityColumn = "parentListID") val itemList: List<Item>
)
To store them in the database, you can simply use a transaction:
#Transaction
suspend fun createTransaction(shoppingList: ShoppingList, itemList: List<Item>) {
addNewShoppingList(shoppingList)
addNewItem(*itemList) // or create an alternate to accept the list itself.
}
To retrieve the 'ShoppingListWithItems' instance:
#Query("SELECT * from ShoppingList where listID =:id")
suspend fun getShoppingListWithItemsById(id: String): List<ShoppingListWithItems>
Given the following data model (i.e. one customer can have many orders and one order can have many shipments), how do I get a list of all orders with their associated customer and shipments for a certain order date?
In Kotlin, I'd like to retrieve a list of type List<OrderWithCustomerAndShipments> with OrderWithCustomerAndShipments being a POJO like this:
data class OrderWithCustomerAndShipments(
val order: Order,
val category: Category,
val shipments: List<Shipment>
)
Approach
So far, I've got a method that returns a List<OrderWithShipments>.
Note: For brevity, I omit #TypeConverter
, #ForeignKey, #ColumnInfo, etc.
#Dao
interface Dao {
#Transaction
#Query("SELECT * FROM orders WHERE orderDate = :date")
fun getOrdersWithShipments(date: Date): LiveData<List<OrderWithShipments>>
}
data class OrderWithShipments(
#Embedded
val order: Order
#Relation(parentColumn = "id", entityColumn = "orderId")
val shipments = List<Shipment>
)
#Entity
data class Customer(
#PrimaryKey val id: Int,
val customerName: String
)
#Entity
data class Order(
#PrimaryKey val id: Int,
val customerId: Int,
val orderDate: Date,
)
#Entity
data class Order(
#PrimaryKey val id: Int,
val orderId: Int,
val shipmentDate: Date,
)
One would assume that it is easier to resolve an order's foreign key to the parent customer than to get all child shipments. However, my attempts to get the parent customer haven't been successful so far.
Have you tried approach below? You could use several #Relation in Room
#Dao
interface Dao {
#Transaction
#Query("SELECT * FROM orders WHERE orderDate = :date")
fun getOrdersWithCustomerAndShipments(date: Date): LiveData<List<OrderWithCustomerAndShipments>>
}
data class OrderWithCustomerAndShipments(
#Embedded
val order: Order
#Relation(parentColumn = "customerId", entityColumn = "id")
val customer: Customer
#Relation(parentColumn = "id", entityColumn = "orderId")
val shipments: List<Shipment>
)