I have been investigating using Micronaut Data JDBC as an augmentation for existing jdbi queries. Is it possible to do nested MappedEntity's and return the children?
Example is for a simple Warehouse Management System with a relation:
company
+- warehouse
+- inventory
What I'm wanting to be able to do is query up from the inventory table and get the parent of warehouse and grandparent of company. It is a one-to-many relationship with a company able to have multiple warehouses and a warehouse able to have multiple pieces of inventory.
My Entities look like
The example project I've been playing with is at wms-api
#JdbcRepository
interface CompanyRepository : PageableRepository<Company, UUID>
#MappedEntity
data class Company(
#field:Id
#field:GeneratedValue
var id: UUID? = null,
#field:GeneratedValue
var timeCreated: OffsetDateTime? = null,
#field:GeneratedValue
var timeUpdated: OffsetDateTime? = null,
var name: String
)
#JdbcRepository
interface WarehouseRepository : GenericRepository<Warehouse, UUID> { // used GenericRepository to allow the mapping of Company
#Join(value = "company")
fun findById(id: UUID): Warehouse?
#Join(value = "company")
fun findByCompany(company: Company, pageable: Pageable): Slice<Warehouse>
#Join(value = "company")
fun findAllByCompany(company: Company, pageable: Pageable): Slice<Warehouse>
#Join(value = "company")
fun existsByLocationAndCompany(location: String, company: Company): Boolean
fun save(warehouse: Warehouse): Warehouse
fun update(warehouse: Warehouse): Warehouse
}
#MappedEntity
data class Warehouse(
#field:Id #field:GeneratedValue
var id: UUID? = null,
#field:GeneratedValue
var timeCreated: OffsetDateTime? = null,
#field:GeneratedValue
var timeUpdated: OffsetDateTime? = null,
var location: String,
#field:Relation(value = MANY_TO_ONE)
var company: Company
)
#JdbcRepository
interface InventoryRepository : GenericRepository<Inventory, UUID> {
fun save(inventory: Inventory): Inventory
#Join(value = "warehouse")
// FIXME need some way to tell repo to also pull company which is attached to warehouse
fun findById(id: UUID): Inventory?
}
#MappedEntity
data class Inventory(
#field:Id
#field:GeneratedValue
var id: UUID? = null,
#field:GeneratedValue
var timeCreated: OffsetDateTime? = null,
#field:GeneratedValue
var timeUpdated: OffsetDateTime? = null,
var manufacturer: String,
var barcode: String,
var name: String,
#field:Relation(value = MANY_TO_ONE) // FIXME the problem is caused by this.
var warehouse: Warehouse,
)
Relevant part of the stacktrace
Caused by: io.micronaut.core.reflect.exception.InstantiationException: Null argument specified for [company]. If this argument is allowed to be null annotate it with #Nullable
at io.micronaut.core.beans.AbstractBeanIntrospection.instantiate(AbstractBeanIntrospection.java:121)
at io.micronaut.core.beans.BeanIntrospection.instantiate(BeanIntrospection.java:81)
at io.micronaut.data.runtime.mapper.sql.SqlResultEntityTypeMapper.readEntity(SqlResultEntityTypeMapper.java:345)
I would like to not have to make the Warehouse.company property optional if possible.
In the linked project there is a docker-compose.yml under support that can be used to fire up Postgres and then run the test suite, and the problem should pop up as a failure.
Thanks!
Figured it out. Had to change the InventoryRepository's findById method to look like
#JoinSpecifications(
Join(value = "warehouse"),
Join(value = "warehouse.company")
)
fun findById(id: UUID): Inventory?
Related
I have three Room entities. An entity Person includes id and name, entity Holiday includes id, personId, onHoliday and Sickness entity includes id, personId and onSickness.
Also, I have a POJO Entity called ScreenPOJO that includes viewTypeId and personName.
My goal is to get the person's name (from Person) and viewTypeId that I can observe in Activity.
The viewTypeId is an Integer that depends on the onHoliday - true/false and onSickness true/false. So let's say when the onHoliday is false and onSickness is false the viewTypeId = 1, when onHoliday is true then viewTypeId = 2 and when onSickness is true then viewTypeId = 2.
In this example, it is achievable by creating a query and returning the result by using the POJO Entity.
Sometimes the query can be too complex and for that reason, I would like to somehow merge all three Live/Flow data together using the person_id. I read that I can use MediatorLiveData, however, I do not have experience yet to put all the data together and return only the result that I need (person name and viewTypeId).
Person Entity:
#Entity(tableName = "person_table")
data class Person(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "id") val id: Int,
#SerializedName("name")
#ColumnInfo(name = "name") val name: String,
)
Sickness Entity:
#Entity(tableName = "sickness_table")
data class Sickness(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "id") val id: Int,
#SerializedName("person_id")
#ColumnInfo(name = "person_id") val personId: Int,
#SerializedName("on_sickness")
#ColumnInfo(name = "on_sickness") val onSickness: Boolean
)
Holiday Entity:
#Entity(tableName = "holiday_table")
data class Holiday(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "id") val id: Int,
#SerializedName("person_id")
#ColumnInfo(name = "person_id") val personId: Int,
#SerializedName("on_holiday")
#ColumnInfo(name = "on_holiday") val onHoliday: Boolean
)
ScreenPOJO Entity:
data class ScreenPOJO(
val viewTypeId: Int,
val personName: String
)
ViewModel, the data are originally as Flow :
val allPersons: LiveData<List<Person>> = repository.allPersons.asLiveData()
val allHolidays: LiveData<List<Holiday>> = repository.allHolidays.asLiveData()
val allSickness: LiveData<List<Sickness>> = repository.allSickness.asLiveData()
ViewModel, example of insert Person:
private val _insertPersonStatus = MutableLiveData<ViewModelStatus>()
val insertPersonStatus: LiveData<ViewModelStatus> = _insertPersonStatus
fun insertPerson(person: Person) = viewModelScope.launch {
try {
val insertedRowId = repository.insertPerson(person)
if (insertedRowId > -1) {
_insertPersonStatus.postValue(ViewModelStatus.SUCCESS(insertedRowId,ViewModelStatus.SUCCESS.Type.VM_INSERT))
} else {
_insertPersonStatus.value = ViewModelStatus.FAIL("Fail",ViewModelStatus.FAIL.Type.VM_INSERT)
}
} catch (ex: Exception) {
_insertPersonStatus.value = ViewModelStatus.EXCEPTION(ex.localizedMessage.toString(),ViewModelStatus.EXCEPTION.Type.VM_INSERT)
}
}
I think it is better to modify the data layer in a way, which will allow you to write less code in the business-logic and view layers, therefore the code will be simpler and cleaner which is always a good sign.
Therefore, instead of merging LiveData you should think about redesigning Person data class.
Since I don't see much sense in storing holidays and sickness separately from persons, I assume you should somehow store holiday and sickness related data in the person object.
I suggest something like this (I will omit room annotations for simplicity):
data class Person(
val id: Long,
val name: String,
val onHoliday: Boolean,
val onSickness: Boolean
)
In case when the person has multiple holidays or sickness days, you can do something like this (which is kinda common practice nowadays):
data class Person(
val id: Long,
val name: String,
val holidays: List<Holiday>,
val sicknessDays: List<Sickness>
)
I am not sure that it is easy to insert a collection in Room, but there are workarounds here. Maybe you will also need #Embedded annotation to keep the POJO's inside of the POJO
I've a realm class A in my android project -
open class A(
var id: Int? = null,
var level: String? = null,
var state: String? = null,
var updated_at: String? = null,
var created_at: String? = null,
var person: Person? = null
): RealmObject()
and Person -
open class Person(
var id: Int? = null,
var name: String? = null,
var email: String? = null,
var url: String? = null
): RealmObject()
I can retrieve list of items of class A sorted by Class A's id attribute -
fun getItemsOfA() : List<A> {
val listToBeReturn: MutableList<A> = mutableListOf()
DBManager.getRealm().executeTransection { realm ->
val tempList = realm.where(A::class.java).sort("id").findAll()
for (item in tempList) {
listToBeReturn.add(item)
}
And I'm getting sorted list. But I want to have a sorted list by the Person's id attribute instead of A's id. Any insight on this.
How can I achieve these?
Thanks & Regards
I want to create a simple financial report app.
With opening balance, inventory purchase, other expenses, sales and banking all as double numbers I want to use room database, jetpack components
You can't ask this type of questions but look at this sample (simplest way to use roomDB)
First of all you need to define you database class
#Database(entities = [CompaniesModel::class, UserPoint::class], version = 15) //here is the models which will have the structure of your database
abstract class DataBase : RoomDatabase() {
/**
* define companies dao to make some quires
*/
abstract fun homeDao(): HomeDao
abstract fun companiesDao(): CompaniesListDao
companion object {
#Volatile
private var databaseInstance: DataBase? = null
fun getDatabaseInstance(mContext: Context): DataBase =
databaseInstance ?: synchronized(this) {
databaseInstance ?: buildDatabaseInstance(mContext).also {
databaseInstance = it
}
}
private fun buildDatabaseInstance(mContext: Context) =
Room.databaseBuilder(mContext, DataBase::class.java, "crm")
.fallbackToDestructiveMigration()
.allowMainThreadQueries()
.build()
}
}
and the models which contain the structure of your database
#Entity(tableName = "companiesModel")
data class CompaniesModel(
#PrimaryKey
#ColumnInfo(name = "id")
#SerializedName("id")
var id: Int,
#ColumnInfo(name = "name")
#SerializedName("name")
var name: String,
#ColumnInfo(name = "image")
#SerializedName("image")
var image: String
)
and the Dao which have all your queres
#Dao
interface CompaniesListDao {
/**
* this fun to insert data in room db after fetch data from server
*/
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertCompanies(contactModel: List<CompaniesModel>)
/**
* this fun to get data from room db to make some caching in app
*/
#Query("SELECT * FROM companiesModel")
fun getCompaniesList(): List<CompaniesModel>
/**
* this fun to clear companies list from room db
*/
#Query("DELETE FROM companiesModel")
fun clearCompaniesList()
#Update
fun update(contactModel: List<CompaniesModel>)
}
to access the data you should call it like this
DataBase.getDatabaseInstance(App.instance).companiesDao().something
So I was working on this silly little app for practicing MVVM and Repository Pattern. I have two model classes at the moment. They are Category and SubCategory for which I have defined the following data classes:
#Entity(tableName = "categories")
data class Category(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "id")
val id: Int,
#ColumnInfo(name = "name")
val name: String
) {
}
And
/**
* One to many Relationship from Category to SubCategory
*/
#Entity(
tableName = "sub_categories", foreignKeys = arrayOf(
ForeignKey(
entity = Category::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("category_id")
)
)
)
data class SubCategory(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "id")
val id: Int,
#ColumnInfo(name = "name")
val name: String,
#ColumnInfo(name = "category_id")
val categoryId: Int
) {
}
As you can see, I have modeled the resources such that we will need categoryId to be passed to get SubCategories related to a Category.
Now I am pretty new with this MVVM and LiveData and Repository Pattern.
My Problem is that I am using an ExpandableListView to populate SubCategories under Categories and the Adapter for it requires a HashMap<Category, List<SubCategory> for it to display the expandable listview.
So my question is how do I get a HashMap<Category, List<SubCategory> from my database using an approach of db->dao->repository->viewmodel and wherever the adpater goes.
I suppose creating a separate repository like CategorySubCategoryRespository whereby I can do something like following is not going to help?:
class CategorySubCategoryRepository(
private val categoryDao: CategoryDao,
private val subCategoryDao: SubCategoryDao
) {
val allCategoriesSubCategories: LiveData<HashMap<Category, List<SubCategory>>>
get() {
val hashMap: HashMap<Category, List<SubCategory>> = hashMapOf()
for (category in categoryDao.getList()) {
hashMap[category] = subCategoryDao.getSubCategoriesListForCategory(category.id)
}
return hashMap
}
}
}
PS: I think I want to use LiveData wherever possible
So what I ended up doing was, in my CategorySubcategoryRepository I constructed the Hashmap from the CategoryDao and SubcategoryDao as follows:
class CategorySubCategoryRepository(
private val categoryDao: CategoryDao,
private val subCategoryDao: SubCategoryDao
) {
fun getHashMap(): LiveData<HashMap<Category, List<SubCategory>>> {
val data = MutableLiveData<HashMap<Category, List<SubCategory>>>()
val hashMap: HashMap<Category, List<SubCategory>> = hashMapOf()
Executors.newSingleThreadExecutor().execute {
for (category in categoryDao.getList()) {
hashMap[category] = subCategoryDao.getSubCategoriesListForCategory(category.id)
}
}
data.value = hashMap
return data
}
}
Then I used this in my viewmodel's init{} block like:
hashMap = categorySubCategoryRepository.getHashMap()
Then I observed it in my Fragment's onCreateView as:
myViewModel.hashMap.observe(this, Observer {
adapter.setCategoryList(it.keys.toList())
adapter.setCategorySubCategoriesMap(it)
elv_categories.setAdapter(adapter)
adapter.notifyDataSetChanged()
})
Do Comment if this is not the right thing to do. I am doing this only to increase my skills and would love to hear if there's a better way to go about things or if my approach is completely absurd.
Edit:
As per #SanlokLee's comment. The getHashMap function has been changed to:
fun getHashMap(): LiveData<HashMap<Category, List<SubCategory>>> {
val data = MutableLiveData<HashMap<Category, List<SubCategory>>>()
Executors.newSingleThreadExecutor().execute {
val hashMap: HashMap<Category, List<SubCategory>> = hashMapOf()
for (category in categoryDao.getList()) {
hashMap[category] = subCategoryDao.getSubCategoriesListForCategory(category.id)
}
data.postValue(hashMap)
}
return data
}
Given an update request for a record in DB, I have to find a difference between the payload and existing data in DB then create a new Object which has updated fields with Payload values and rest as Null.
I have created a function which gives me a list of field names which were updated, But I'm unable to create a new object which has values for only these updated fields.The problem is that the function uses "field: Field in cpayload.javaClass.declaredFields" which is kind of generic so I'm unable to set these fields.
fun findupdatedFieldsList(cpayload: Customer, cEntity: Customer): List<String> {
// var customerToPublish = Customer()
val updatedFieldsList: MutableList<String>
updatedFieldsList = ArrayList()
for (field: Field in cpayload.javaClass.declaredFields) {
field.isAccessible = true
val value1 = field.get(cpayload).toString()
val value2 = field.get(cEntity).toString()
!Objects.equals(value1, value2).apply {
if (this) {
// customerToPublish.birthDate=field.get(cpayload).toString()
updatedFieldsList.add(field.name)
}
}
}
return updatedFieldsList
}
#Entity
#Table
data class Customer(
#Id
val partyKey: UUID,
var preferredName: String?,
var givenName: String?,
var lastName: String?,
var middleName: String?,
var emailAddress: String,
var mobileNumber: String,
val birthDate: String?,
val loginOnRegister: Boolean,
var gender: Gender?,
var placeOfBirth: String?,
var createdDate: LocalDateTime = LocalDateTime.now(),
var updatedDate: LocalDateTime = LocalDateTime.now()
)
Desired Output
val customer = Customer(
preferredName = Updated name,
partyKey = partyKey.value,
givenName = Updated name,
lastName = null,
middleName = null,
emailAddress = Updated email,
mobileNumber = null,
birthDate = null,
gender = null,
placeOfBirth = null
)
I was able to construct a solution using Kotlin's reflect. It is generic and can be applied to any Kotlin class that have primary constructor. Unfortunately it won't work with Java classes
You would need to add kotlin-reflect package to your build tool config, e.g. for Gradle:
implementation 'org.jetbrains.kotlin:kotlin-reflect:XXXXXX'
First we will build a function to extract updated properties. Please take a note that we also need to extract properties that are mandatory (non-nullable and without default). We add them to a map of propertyName -> propertyValue:
fun Map<String?, KParameter>.isOptional(name: String) = this[name]?.isOptional ?: false
fun <T : Any> findUpdatedProperties(payload: T, entity: T): Map<String, Any?> {
val ctorParams = payload::class.primaryConstructor!!.parameters.associateBy { it.name }
return payload::class.memberProperties.map { property ->
val payloadValue = property.call(payload)
val entityValue = property.call(entity)
if (!Objects.equals(payloadValue, entityValue) || (!ctorParams.isOptional(property.name))) {
property.name to payloadValue
} else {
null
}
}
.filterNotNull()
.toMap()
}
Then we call this function and construct a new instance of provided class:
fun <T : Any> constructCustomerDiff(clazz: KClass<T>, payload: T, entity: T): T {
val ctor = clazz.primaryConstructor!!
val params = ctor.parameters
val updatedProperties = findUpdatedProperties(payload, entity)
val values = params.map { it to updatedProperties[it.name] }.toMap()
return ctor.callBy(values)
}
Take a note that missing primary constructor will throw NullPointerException because of use of !!.
We could call this funcion as constructCustomerDiff(Customer::class, payload, entity), but we can do better with reified types:
inline fun <reified T : Any> constructCustomerDiff(payload: T, entity: T): T {
return constructCustomerDiff(T::class, payload, entity)
}
Now we can use this function in convenient Kotlin style:
val id = UUID.randomUUID()
val payload = Customer(
partyKey = id,
preferredName = "newName",
givenName = "givenName"
)
val entity = Customer(
partyKey = id,
preferredName = "oldName",
givenName = "givenName" // this is the same as in payload
)
val x = constructCustomerDiff(payload, entity)
assert(x.partyKey == id && x.givenName == null || x.preferredName == "newName")