Android room data relations - gson

I have multiple object tyes inside a parent class.Say I have a College class as below
data class College(
#PrimaryKey
val id:String,
val name: String? = null,
val description: String? = null,
val groups: List<Group>? = null,
val status: String? = null,
)
data class Group(
#PrimaryKey
val id:String,
val name: String? = null,
val description: String? = null,
val students: List<Students>? = null,
)
The problem here is there is not relational id in child tables.I mean JSON received don't give relation of groups inside college with collegeId in Groups table or relation of students in group table.
The JSON received is as below
"college": [
{
"id": "collegeid",
"groups": [
"id": "groupid"
"name": "BCOM" // Here no collegeId is mentioned inside it
]
}
If is use #Embedded keyword it is throwing "Entities and POJOs must have a usable public constructor".
Is there anyway with above JSON I can set the id of college inside group and use it as foreign key for relations.
I have used Typeconverters and is working fine but now I need to create relations between these tables with above type of JSON.
I use Gson parsing

I believe that you may be getting mixed up with how to utilise relationships/tables.
IF you have
#Entity
data class College(
#PrimaryKey
val id:String,
val name: String? = null,
val description: String? = null,
val groups: List<Group>? = null,
val status: String? = null,
)
this will be a table where the groups column will (in theory) contain a List of Groups, there is no relationship as there is just a column with a single stream of data (probably as JSON string).
However, if you wanted to have a table with the Colleges, a table with the Groups and a table with the Students then you wouldn't embed the Groups within the College and subsequently the Students within the Groups.
Rather, if you want to approach this from a db relationship aspect. Assuming that a Group MUST belong to one and only one College and that a Student MUST only belong to a single Group then you would have something along the lines of:-
#Entity
data class College(
#PrimaryKey
val id:String,
val name: String? = null,
val description: String? = null,
//val groups: List<Group>? = null, //<<<<< NO NEED (see Group)
val status: String? = null,
)
#Entity
data class Group(
#PrimaryKey
val id:String,
val name: String? = null,
val description: String? = null,
//val students: List<Students>? = null, //<<<<< NO NEED (similar to Group)
val collegeId: String //<<<<< ADDED for relationships to parent College
)
similar with Students
the crucial factor here is the new column/field/variable collegeId, which should contain the id of the parent college.
To get the College(s) with the related groups then you can have a class that has #Embedded annotation for the parent (the College) and #Relation annotation for the children (Groups). e.g.
data class CollegeWithRelatedGroups(
#Embedded
val college: College,
#Relation(
entity = Group::class,
parentColumn = "id",
entityColumn = "collegeId"
)
val groups: List<Group>
)
Working Example
Here's a working example that uses the above and adds (inserts) 3 Colleges, and then 6 Groups. The first College has 3 related Groups, the second 2 and the third 1.
The example then extracts the Colleges with the related Groups outputting the result to the log.
College and also CollegeWithRelatedGroups
#Entity
data class College(
#PrimaryKey
val id:String,
val name: String? = null,
val description: String? = null,
//val groups: List<Group>? = null,
val status: String? = null
)
data class CollegeWithRelatedGroups(
#Embedded
val college: College,
#Relation(
entity = Group::class,
parentColumn = "id",
entityColumn = "collegeId"
)
val groups: List<Group>
)
Group (with Foreign Key constraint)
#Entity(
/* Optional but suggested Foreign Key to enforce referential integrity
*/
foreignKeys = [
ForeignKey(
entity = College::class,
parentColumns = ["id"],
childColumns = ["collegeId"],
/* Optional within Foreign Key - helps maintain referential integrity (if CASCADE)
ON DELETE will delete the Groups that are related to a College if the College is deleted
ON UPDATE will change the collegeId of Groups that are related to a College if the id of the College is changed
*/
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
data class Group(
#PrimaryKey
val id:String,
val name: String? = null,
val description: String? = null,
//val students: List<Student>? = null,
#ColumnInfo(index = true) /* faster to access via relationship if indexed */
val collegeId: String
)
AllDao
#Dao
interface AllDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(college: College): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(group: Group): Long
#Transaction
#Query("SELECT * FROM college")
fun getCollegesWithRelatedGroups(): List<CollegeWithRelatedGroups>
}
TheDatabase (note for brevity and convenience allows main thread processing)
#Database(entities = [College::class, Group::class], version = 1, exportSchema = false)
abstract class TheDatabase : RoomDatabase() {
abstract fun getAllDao(): AllDao
companion object {
#Volatile
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
}
}
}
Note there is no need for TypeConverters as objects are not being stored (aka simple types are stored)
Activity Code (MainActivity in this case)
const val TAG = "DBINFO"
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()
dao.insert(College("College1","The first College","Opened"))
dao.insert(College("College2","The second College","Opened"))
dao.insert(College("New College","A new College","Being built"))
dao.insert(Group("GroupA","Group A or something","The first Group - hence A","College1",))
dao.insert(Group("GroupB","Group B or whatever"," The second Group ...", "College2"))
dao.insert(Group("GroupC","Group C on so on","The third Group ...","New College"))
dao.insert(Group("GroupX","Group X ...","The Xth group","College1"))
dao.insert(Group("GroupY","...","...","College1"))
dao.insert(Group("GroupZ","...","...","College2"))
for (cwrg in dao.getCollegesWithRelatedGroups()) {
Log.d(TAG,"College is ID=${cwrg.college.id} Desc=${cwrg.college.description} Name=${cwrg.college.name} Status=${cwrg.college.name}\nIt has ${cwrg.groups.size} Groups. They are:-")
for (g in cwrg.groups) {
Log.d(TAG,"\n\t\tGroup is ID=${g.id} Name=${g.name} Desc=${g.description} it reference CollegeID=${g.collegeId}")
}
}
}
}
Note The status has not been supplied for the Colleges. so they will be null.
RESULT the log includes :-
D/DBINFO: College is ID=College1 Desc=Opened Name=The first College Status=The first College
It has 3 Groups. They are:-
D/DBINFO: Group is ID=GroupA Name=Group A or something Desc=The first Group - hence A it reference CollegeID=College1
D/DBINFO: Group is ID=GroupX Name=Group X ... Desc=The Xth group it reference CollegeID=College1
D/DBINFO: Group is ID=GroupY Name=... Desc=... it reference CollegeID=College1
D/DBINFO: College is ID=College2 Desc=Opened Name=The second College Status=The second College
It has 2 Groups. They are:-
D/DBINFO: Group is ID=GroupB Name=Group B or whatever Desc= The second Group ... it reference CollegeID=College2
D/DBINFO: Group is ID=GroupZ Name=... Desc=... it reference CollegeID=College2
D/DBINFO: College is ID=New College Desc=Being built Name=A new College Status=A new College
It has 1 Groups. They are:-
D/DBINFO: Group is ID=GroupC Name=Group C on so on Desc=The third Group ... it reference CollegeID=New College
The Database via App Inspection :-
and

Related

#OneToMany field is wrongly included in JPA query

I have a small project to tinker with Spring, where I have two entities with a one to many association: 1 Restaurant -> N Dishes.
I have the following PostgreSQL schema for that:
create table if not exists restaurants (
restaurant_id uuid primary key,
name varchar(512) not null,
description varchar(1024) not null,
address varchar(512) not null,
photo_url varchar(1024)
);
create table if not exists dishes (
dish_id uuid primary key,
name varchar(512) not null,
description varchar(1024),
photo_url varchar(1024),
restaurant_id uuid references restaurants(restaurant_id) not null,
price int not null check (price > 0)
);
With the following JPA Entities:
#Entity
#Table(name = "restaurants")
class Restaurants(
#Id
var restaurantId: UUID,
var name: String,
var description: String,
var photoUrl: String?,
) {
#OneToMany(mappedBy = "restaurant")
#JoinColumn(name = "restaurant_id", nullable = false)
var dishes: MutableList<Dishes> = mutableListOf()
}
#Entity
#Table(name = "dishes")
class Dishes(
#Id
var dishId: UUID,
var name: String,
var description: String,
var photoUrl: String?,
var price: Int,
#ManyToOne(optional = false)
#JoinColumn(name = "restaurant_id", nullable = false)
var restaurant: Restaurants
)
I have defined a RestaurantsRepository as follows:
interface RestaurantsRepository: R2dbcRepository<Restaurants, UUID> {
fun findByRestaurantId(restaurantId: UUID): Mono<Restaurants>
}
The problem I'm having is that when I call findByRestaurantId I have the following exception:
org.springframework.r2dbc.BadSqlGrammarException: executeMany; bad SQL grammar [SELECT restaurants.restaurant_id, restaurants.name, restaurants.description, restaurants.photo_url, restaurants.dishes FROM restaurants WHERE restaurants.restaurant_id = $1]; nested exception is io.r2dbc.postgresql.ExceptionFactory$PostgresqlBadGrammarException: [42703] column restaurants.dishes does not exist
at org.springframework.r2dbc.connection.ConnectionFactoryUtils.convertR2dbcException(ConnectionFactoryUtils.java:235) ~[spring-r2dbc-5.3.21.jar:5.3.21]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Why is the #OneToMany field included in the SQL query?
You are trying to use Spring Data R2DBC (R2dbcRepository) in conjunction with JPA annotations. It won't work: these are two different technologies. R2DBC does not support #ManyToOne nor #JoinColumn so the annotations are simply ignored.

Insert data and get back id in mybatis

I should save the record in the database and get the record id in the response. After a long search and research I came up with the following option.
data class User(
val id: UUID? = null,
val username: String,
...
)
UserRepo:
#Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
#Insert("""
INSERT INTO "user" (
username,
...
) VALUES (
#{username},
...
)
""")
fun save(user: User): User
in response I get the following.
org.apache.ibatis.binding.BindingException: Mapper method '...UserRepository.save' has an unsupported return type: class ...entity.User
in the following case, I don't get an error, but I don't get an id either, how can I do it correctly? Used Select instead of Insert
#Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
#Select("""
INSERT INTO "user" (
username
) VALUES (
#{username}
)
""")
fun save(user: User): User
if we solve my first question, then I would like to know if it is possible to get the answer not in the User class, but in another one, for example, UserResponse? That is, send a request to the User class, and receive a response in the UserResponse
UPDATE
I was able to get the id after changing the id type to String.
Like this:
data class User(
val id: String? = null,
val username: String,
...
)
Apparently some settings are needed for UUID?
Has anyone faced such a problem?

When setting DB column to allow NULL in Database the app complains about the Schema

I am trying to use Room in Android Studio using Kotlin with a pre-packaged database. The database does not set NOT NULL. Using DB SQL Browser it shows that the column has these properties
"Reference" TEXT
There is no NOT NULL. All the other columns in the table do have NOT NULL set.
In the Entity that maps that table I have:
#Entity
data class Meaning (
#PrimaryKey(autoGenerate = true) #ColumnInfo(name = "Id") val id: Int,
#NonNull #ColumnInfo(name = "Contents") val contents: String,
/*
* It's OK for Reference to be Null
*/
#ColumnInfo(name = "Reference") val reference: String,
#NonNull #ColumnInfo(name = "SymbolId") val symbolId: Int,
#NonNull #ColumnInfo(name = "Local") val local: Int
)
It builds and installs, but fails when running with this error:
java.lang.IllegalStateException: Pre-packaged database has an invalid schema: Meaning(<stuff>.Meaning).
Expected:
TableInfo{name='Meaning', <unimportant columns>, Reference=Column{name='Reference', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null'},<more unimportant columns>}
Found:
TableInfo{name='Meaning', <unimportant columns>, Reference=Column{name='Reference', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, <more unimportant columns}
Note that the Found has notNull=false, which seems correct because NOT NULL is not specified in the database.
The Expected has notNull=true even though in the Entity, #NonNull was not specified for the Reference column.
So, I am confused why the Entity is expecting Reference column to be notNull=true.
Any pointers are welcome.
#ColumnInfo(name = "Reference") val reference: String?,
would equate to notNull = false.
i.e. the ? indicates null allowed.
So, I am confused why the Entity is expecting Reference column to be notNull=true.
Because the annotation processing sees String which cannot be null. Only if it sees String? then can the value be nullable.
That is if you you use:-
#Entity
data class Meaning (
#PrimaryKey(autoGenerate = true) #ColumnInfo(name = "Id") val id: Int,
#NonNull #ColumnInfo(name = "Contents") val contents: String,
/*
* It's OK for Reference to be Null
*/
#ColumnInfo(name = "Reference") val reference: String?,
#NonNull #ColumnInfo(name = "SymbolId") val symbolId: Int,
#NonNull #ColumnInfo(name = "Local") val local: Int
)
and then compile, the generated Java (expected) for the #Database annotated class suffixed with _Impl includes:-
_db.execSQL("CREATE TABLE IF NOT EXISTS `Meaning` (`Id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `Contents` TEXT NOT NULL, `Reference` TEXT, `SymbolId` INTEGER NOT NULL, `Local` INTEGER NOT NULL)");
i.e.
, `Reference` TEXT,
However without the ? then :-
_db.execSQL("CREATE TABLE IF NOT EXISTS `Meaning` (`Id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `Contents` TEXT NOT NULL, `Reference` TEXT NOT NULL, `SymbolId` INTEGER NOT NULL, `Local` INTEGER NOT NULL)");
i.e.
`Reference` TEXT NOT NULL,

Android Room Multimap issue for the same column names

As stated in official documentation, it's preferable to use the Multimap return type for the Android Room database.
With the next very simple example, it's not working correctly!
#Entity
data class User(#PrimaryKey(autoGenerate = true) val _id: Long = 0, val name: String)
#Entity
data class Book(#PrimaryKey(autoGenerate = true) val _id: Long = 0, val bookName: String, val userId: Long)
(I believe a loooot of the developers have the _id primary key in their tables)
Now, in the Dao class:
#Query(
"SELECT * FROM user " +
"JOIN book ON user._id = book.userId"
)
fun allUserBooks(): Flow<Map<User, List<Book>>>
The database tables:
Finally, when I run the above query, here is what I get:
While it should have 2 entries, as there are 2 users in the corresponding table.
PS. I'm using the latest Room version at this point, Version 2.4.0-beta02.
PPS. The issue is in how UserDao_Impl.java is being generated:
all the _id columns have the same index there.
Is there a chance to do something here? (instead of switching to the intermediate data classes).
all the _id columns have the same index there.
Is there a chance to do something here?
Yes, use unique column names e.g.
#Entity
data class User(#PrimaryKey(autoGenerate = true) val userid: Long = 0, val name: String)
#Entity
data class Book(#PrimaryKey(autoGenerate = true) valbookid: Long = 0, val bookName: String, val useridmap: Long)
as used in the example below.
or
#Entity
data class User(#PrimaryKey(autoGenerate = true) #ColumnInfo(name="userid")val _id: Long = 0, val name: String)
#Entity
data class Book(#PrimaryKey(autoGenerate = true) #ColumnInfo(name="bookid")val _id: Long = 0, val bookName: String, val #ColumnInfo(name="userid_map")userId: Long)
Otherwise, as you may have noticed, Room uses the value of the last found column with the duplicated name and the User's _id is the value of the Book's _id column.
Using the above and replicating your data using :-
db = TheDatabase.getInstance(this)
dao = db.getAllDao()
var currentUserId = dao.insert(User(name = "Eugene"))
dao.insert(Book(bookName = "Eugene's book #1", useridmap = currentUserId))
dao.insert(Book(bookName = "Eugene's book #2", useridmap = currentUserId))
dao.insert(Book(bookName = "Eugene's book #3", useridmap = currentUserId))
currentUserId = dao.insert(User(name = "notEugene"))
dao.insert(Book(bookName = "not Eugene's book #4", useridmap = currentUserId))
dao.insert(Book(bookName = "not Eugene's book #5", useridmap = currentUserId))
var mapping = dao.allUserBooks() //<<<<<<<<<< BREAKPOINT HERE
for(m: Map.Entry<User,List<Book>> in mapping) {
}
for convenience and brevity a Flow hasn't been used and the above was run on the main thread.
Then the result is what I believe you are expecting :-
Additional
What if we already have the database structure with a lot of "_id" fields?
Then you have some decisions to make.
You could
do a migration to rename columns to avoid the ambiguous/duplicate column names.
use alternative POJO's in conjunction with changing the extract output column names accordingly
e.g. have :-
data class Alt_User(val userId: Long, val name: String)
and
data class Alt_Book (val bookId: Long, val bookName: String, val user_id: Long)
along with :-
#Query("SELECT user._id AS userId, user.name, book._id AS bookId, bookName, user_id " +
"FROM user JOIN book ON user._id = book.user_id")
fun allUserBooksAlt(): Map<Alt_User, List<Alt_Book>>
so user._id is output with the name as per the Alt_User POJO
other columns output specifically (although you could use * as per allUserBookAlt2)
:-
#Query("SELECT *, user._id AS userId, book._id AS bookId " +
"FROM user JOIN book ON user._id = book.user_id")
fun allUserBooksAlt2(): Map<Alt_User, List<Alt_Book>>
same as allUserBooksAlt but also has the extra columns
you would get a warning warning: The query returns some columns [_id, _id] which are not used by any of [a.a.so70190116kotlinroomambiguouscolumnsfromdocs.Alt_User, a.a.so70190116kotlinroomambiguouscolumnsfromdocs.Alt_Book]. You can use #ColumnInfo annotation on the fields to specify the mapping. You can annotate the method with #RewriteQueriesToDropUnusedColumns to direct Room to rewrite your query to avoid fetching unused columns. You can suppress this warning by annotating the method with #SuppressWarnings(RoomWarnings.CURSOR_MISMATCH). Columns returned by the query: _id, name, _id, bookName, user_id, userId, bookId. public abstract java.util.Map<a.a.so70190116kotlinroomambiguouscolumnsfromdocs.Alt_User, java.util.List<a.a.so70190116kotlinroomambiguouscolumnsfromdocs.Alt_Book>> allUserBooksAlt2();
Due to Note that Room will not rewrite the query if it has multiple columns that have the same name as it does not yet have a way to distinguish which one is necessary. the #RewriteQueriesToDropUnusedColumns doesn't do away with the warning.
if using :-
var mapping = dao.allUserBooksAlt() //<<<<<<<<<< BREAKPOINT HERE
for(m: Map.Entry<Alt_User,List<Alt_Book>> in mapping) {
}
Would result in :-
possibly other options.
However, I'd suggest fixing the issue once and for all by using a migration to rename columns to all have unique names. e.g.

Springboot Mongo reactive repository unable to update nested list

I wanted to update a nested list but I experience a strange behavior where I have to call method twice to get it done...
Here is my POJO:
#Document(collection = "company")
data class Company (
val id: ObjectId,
#Indexed(unique=true)
val name: String,
val customers: MutableList<Customer> = mutableListOf()
//other fields
)
Below is my function from custom repository to do the job which I based on this tutorial
override fun addCustomer(customer: Customer): Mono<Company> {
val query = Query(Criteria.where("employees.keycloakId").`is`(customer.createdBy))
val update = Update().addToSet("customers", customer)
val upsertOption = FindAndModifyOptions.options().upsert(true)
//if I uncomment below this will work...
//mongoTemplate.findAndModify(query, update, upsertOption, Company::class.java).block()
return mongoTemplate.findAndModify(query, update, upsertOption, Company::class.java)
}
In order to actually add this customer I have to either uncomment the block call above or call the method two times in the debugger while running integration tests which is quite confusing to me
Here is the failing test
#Test
fun addCustomer() {
//given
val company = fixture.company
val initialCustomerSize = company.customers.size
companyRepository.save(company).block()
val customerToAdd = CustomerReference(id = ObjectId.get(),
keycloakId = "dummy",
username = "customerName",
email = "email",
createdBy = company.employees[0].keycloakId)
//when, then
StepVerifier.create(companyCustomRepositoryImpl.addCustomer(customerToAdd))
.assertNext { updatedCompany -> assertThat(updatedCompany.customers).hasSize(initialCustomerSize + 1) }
.verifyComplete()
}
java.lang.AssertionError:
Expected size:<3> but was:<2> in:
I found out the issue.
By default mongo returns entity with state of before update. To override it I had to add:
val upsertOption = FindAndModifyOptions.options()
.returnNew(true)
.upsert(true)

Resources