Exposed ORM: DSL vs DAO in Many-to many relationships best practices - kotlin

I am setting up some many-to-many relationships and have so far been using the Exposed DSL pattern (as opposed to DAO). However, creating many-to-many relationships seem to only be possible using the DAO approach.
I know it is probably fine to use the two patterns interchangeably, but as I set up my project and move forward, I'm wondering what the best approach is from the perspective of code quality. Use them both or switch to DAO? Or the third option is that this question represents a misguided understanding of Kotlin and/or Exposed (new to both), in which case, where am I going wrong? Thanks in advance

It is possible to use DSL to create many-to-many relationships for tables. However whether or not you should use DSL or DAO or both together would really be up to you and whether or not it makes the code easier or harder to read and maintain.
Here is a basic example of a many to many relationship with DSL.
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
object Users : IntIdTable() {
val username = varchar("username", 50)
}
object Permissions : IntIdTable() {
val name = varchar("name", 50)
}
object UserPermissionsJunctionTable : IntIdTable() {
val user = reference("user", Users)
val permission = reference("permission", Permissions)
}
fun main(args: Array<String>) {
val db = Database.connect("jdbc:sqlite:test.db", "org.sqlite.JDBC")
transaction {
addLogger(StdOutSqlLogger)
SchemaUtils.create(Users, Permissions, UserPermissionsJunctionTable)
val userId = Users.insertAndGetId {
it[username] = "john.smith"
}
val readPermissionId = Permissions.insertAndGetId {
it[name] = "read"
}
val writePermissionId = Permissions.insertAndGetId {
it[name] = "write"
}
UserPermissionsJunctionTable.insert {
it[user] = userId
it[permission] = readPermissionId
}
UserPermissionsJunctionTable.insert {
it[user] = userId
it[permission] = writePermissionId
}
val result = Users
.join(UserPermissionsJunctionTable, JoinType.INNER, additionalConstraint = {
Users.id eq UserPermissionsJunctionTable.user
})
.join(Permissions, JoinType.INNER, additionalConstraint = {
UserPermissionsJunctionTable.permission eq Permissions.id
})
.slice(Users.username, Permissions.name).selectAll().map {
it[Users.username] to it[Permissions.name]
}
println(result)
}
}
This prints [(john.smith, read), (john.smith, write)]

Related

Kotlin Room repository calls to DAO 'unresolved reference' in Android studio

I'm building my first Room project and need a fresh pair of eyes to see what I'm doing wrong.
Android studio keeps telling me the call to insertBopa or deleteBopa in the BopaRoomDao is an unresolved reference. My code seeme to match other examples I've looked at and tutorials but I just can't work out what I'm doing wrong.
This is my repository.kt
package com.example.mytestapp
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
class BopaRepository(private val bopaRoomDao: BopaRoomDao) {
val allBopaRoomEntry: LiveData<List<BopaRoomEntry>> = bopaRoomDao.allBopas()
val searchResults = MutableLiveData<List<BopaRoomEntry>>()
private val coroutineScope = CoroutineScope(Dispatchers.Main)
fun insertBopaEntry(newbopa: BopaRoomEntry) {
coroutineScope.launch(Dispatchers.IO) {
BopaRoomDao.insertBopa(newbopa)
}
}
fun deleteBopa(name: String) {
coroutineScope.launch(Dispatchers.IO) {
BopaRoomDao.deleteBopa(name)
}
}
fun findBopa(name: String) {
coroutineScope.launch(Dispatchers.Main) {
searchResults.value = asyncFind(name).await()
}
}
fun allBopas(): LiveData<List<BopaRoomEntry>> {
return bopaRoomDao.allBopas()
}
private fun asyncFind(name: String): Deferred<Flow<List<BopaRoomEntry>>> =
coroutineScope.async(Dispatchers.IO) {
return#async bopaRoomDao.findBopa(name)
}
}
This is my Dao
package com.example.mytestapp
import androidx.lifecycle.LiveData
import androidx.room.*
//import java.util.concurrent.Flow
import kotlinx.coroutines.flow.Flow
#Dao
interface BopaRoomDao {
//add new entry to db
#Insert
fun insertBopa(bopaRoomEntry: BopaRoomEntry)
//change entry on db
#Update
fun updateBopa(bopaRoomEntry: BopaRoomEntry)
#Delete
fun deleteBopa(bopaRoomEntry: BopaRoomEntry)
//open list of previous entries from db
#Query("SELECT * FROM bopa_table")
fun findBopa(name: String): Flow<List<BopaRoomEntry>>
#Query("SELECT * FROM bopa_table")
fun allBopas(): LiveData<List<BopaRoomEntry>>
}
This is the BopaRoomEntry class
package com.example.mytestapp
import androidx.annotation.NonNull
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.lang.reflect.Constructor
#Entity (tableName = "BOPA_TABLE")
class BopaRoomEntry {
#PrimaryKey(autoGenerate = true)
#NonNull
#ColumnInfo(name = "bopaId")
var id: Int = 0
#ColumnInfo(name = "bopa_topic")
var bopaTopic: String = ""
#ColumnInfo(name = "bopa_content")
var bopaContent: String = ""
constructor(){}
constructor(bopatopic: String, bopacontent: String) {
//this.id = id
this.bopaTopic = bopatopic
this.bopaContent = bopacontent
}
}
I'm adding the database class to see if it helps clarify one of the answers...
package com.example.mytestapp
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
#Database(entities = [(BopaRoomEntry::class)], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun bopaRoomDao(): BopaRoomDao
companion object {
#Volatile
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase? {
synchronized(this) {
var instance = INSTANCE
if (INSTANCE == null) {
instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"bopa-database.db"
).fallbackToDestructiveMigration()
.build()
INSTANCE = instance
}
return instance
}
}
}
}
Any help appreciated :-P
After a closer look:
Could it be you're missing the BopaRoomDao.insertBopa(newbopa) vs the lower-case version: bopaRoomDao.insertBopa(newbopa)?
Do you have a abstract class XXXX : RoomDatabase() { where you define your abstract bopaDao() = BopaRoomDao and is annotated with
#Database(
entities = [
BopaRoomEntry::class,
],
version = 1,
exportSchema = false
)
If so you should be using the "daos" provided by this:
val db = ... //obtain your DB
db.bopaDao().allBopas()
Update
After cloning your project, I see a few issues:
MainViewModel:
You obtain your DB here, in an attempt to construct the Repository. This is fine (though with Hilt/DependencyInjection you would not need to worry) but your Repository is -correctly- expecting a non-nullable version of your DB. So
val bopaDb = AppDatabase.getInstance(application)
val bopaDao = bopaDb.bopaRoomDao()
repository = BopaRepository(bopaDao)
Should really be changed to ensure getInstance cannot return null.
(maybe make INSTANCE a lateinit since you must have a DB to function it appears).
If having a DB is optional, then the repository must either deal with it or the viewmodel must not attempt to use/create a repository. As you can see this can get weird really fast. I'd say having a DB cannot fail or you have other issues.
If you still leave it as optional, then the sake of this demo, change it to:
val bopaDb = AppDatabase.getInstance(application)
val bopaDao = bopaDb?.bopaRoomDao() //add the required `?`
repository = BopaRepository(bopaDao!!) //not good to force unwrap !! but will work.
Alternatively you can make your BopaRepository nullable BopaRepository? and use
repository = bopaDao?.let { BopaRepository(it) } ?: null
but then you have to add ? every time you want to use it... and this in turn will make this more messy.
I'd say your DB method should not return null, if it is null for some random other problem (say the filesystem is full and the DB cannot be created) then you should handle this gracefully elsewhere as this is an exception outside of your control. OR... your repository should fetch the DB and work with a different storage internally, you, the caller, should not care.
Anyway.. after taking care of that...
Let's look at BopaRepository
You have it defined like
class BopaRepository(private val bopaRoomDao: BopaRoomDao) {
The important bit is bopaRoomDao.
(note: I would pass the DB here, not a specific DAO, since the repo may need access to other Daos (though you could argue then it should receive the other Repositories instead) so... your choice).
Red Line 1:
val allBopaRoomEntry: LiveData<List<BopaRoomEntry>> = bopaRoomDao.allBopaEntries()
The problem is that allBopaEntries doesn't exist. In the BopaRoomDao interface, the method is called: allBopas()
So change that to
val allBopaRoomEntry: LiveData<List<BopaRoomEntry>> = bopaRoomDao.allBopas()
Red Line #2
In fun insertBopaEntry(newbopa: BopaRoomEntry) {
BopaRoomDao.insertBopaEntry(newbopa) should be:
bopaRoomDao.insertBopa(newbopa)
Red Line #3:
coroutineScope.launch(Dispatchers.IO) {
BopaRoomDao.deleteBopaEntry(name)
}
}
The DAO in the repo doesn't have a delete method (forgot?)
but should look like bopaRoomDao.delete(theBopaYouWantToDelete)
So:
#Delete
fun deleteBopa(bopaRoomEntry: BopaRoomEntry)
This means you cannot pass a name to the delete method (you could) but then because as far as I remember Room doesn't support a #Delete(...), you need to change it to a "custom" query:
#Query("DELETE FROM bopa_table WHERE bopa_topic=:name")
fun deleteByTopic(topic: String);
In truth, you should probably FETCH the row you want to delete and pass that to the original method.
For more info take a look at this SO answer.
Red Line #4
fun findBopa(name: String) {
You need to collect the flow:
fun findBopa(name: String){
coroutineScope.launch(Dispatchers.Main) {
val result = asyncFind(name).await()
result.collect {
searchResults.postValue(it)
}
}
}
This will have another issue though. You're not using the name you pass to find:
So it should look like:
//open list of previous entries from db
#Query("SELECT * FROM bopa_table WHERE bopa_topic=:name")
fun findBopa(name: String): Flow<List<BopaRoomEntry>>
(assuming name is the bopa_topic).
Red Line #5
fun allBopas(): LiveData<List<BopaRoomEntry>> {
Should do return bopaRoomDao.allBopas() (incorrect name)
This one is strange as allBopaRoomEntry is a public variable, you should either make that one private or remove it, since you have this method that returns the reference to the same thing.
Red Line #6
Last but not least,
fun asyncFind(name: String): Deferred<Flow<List<BopaRoomEntry>>>
returns a Flow (deferred but flow) so I think you'd want to do this:
= coroutineScope.async(Dispatchers.IO) {
return#async bopaRoomDao.findBopa(name)
}
Given that findBopa returns a Flow<List<BopaRoomEntry>> already.
With these changes, the project almost built correctly, but there's another issue in MainActivity:
//button actions
binding.saveBopaEntry.setOnClickListener{
//code for sending editText to db
BopaRoomDao.updateBopa(bopaTopic = R.id.bopaTopic, bopaContent = R.id.bopaContent)
}
This shouldn't be there. The click listener should tell the ViewModel: The User pressed save on this item.
viewModel.onSaveBopa(...)
And the ViewModel should launch a coroutine in its scope:
fun onSaveBopa(bopa: Bopa) {
viewModelScope.launch {
repo.updateBopa(bopa)
}
}
Keep in mind this is pseudo-code. If you pass the topic/content directly, then also pass the ID so the viewModel knows what BOPA must be updated in the database...
fun onSaveBopa(id: String, topic: String, content: String)
That's a more plausible method to call from your activity. But it really depends on what you're trying to do. in any case the activity should not need to deal with DB, Room, Daos, etc. Rely on your ViewModel, that's what it's doing there.
Anyway, commenting that in the Activity... made the project finally build
I hope that helps you ;) Good Luck.

Syncing data fetched from retrofit (MutableLiveData) with data from Room DB (LiveData) in an android app

The basic idea of the app is the following: fetch data from server using retrofit whenever its up, fall back on local room database whenever the server is unreachable. I have an issue with the way I'm saving data in my view models however. I'm using LiveData for the data fetched from Room and MutableLiveData for the data fetched from the server. Not sure how to have a single source for the data though. The Retrofit API returns my entities (Recipe) as List<Recipe>. Only way I know to persist that is using MutableLiveData:
var readAllIngredients = MutableLiveData<List<Ingredient>>().apply { value = emptyList() }
...
readAllIngredients.postValue(NetworkService.service.getIngredients())
Changing readAllIngredients to LiveData<List<Ingredient>> and then trying to set the value field of the list doesn't work since apparently value is not assignable to.
Can't make the DAO return MutableLiveData<List<Ingredient>> either (getting some compile errors). And I heard trying to cast one of these types to the other isn't exactly best practice. So I'm not sure what else I could try.
class InventoryViewModel(application: Application): AndroidViewModel(application) {
var readAllIngredients = MutableLiveData<List<Ingredient>>().apply { value = emptyList() }
private val ingredientRepository: IngredientRepository
init {
val ingredientDAO = ShoppingAppDatabase.getDatabase(application).ingredientDAO()
ingredientRepository = IngredientRepository(ingredientDAO)
viewModelScope.launch(Dispatchers.IO) {
// logic moved from repository in order to make example more concise
if(NetworkService.serverReachable()) {
readAllIngredients.postValue(NetworkService.service.getIngredients())
}
else {
//save the data from the database somehow; used to do it like
//readAllIngredients = ingredientRepository.allIngredients
//when readAllIngredients was of type `LiveData<List<Ingredient>>`
}
}
}
MediatorLiveData is used when you want to combine multiple sources. Example:
class InventoryViewModel(application: Application): AndroidViewModel(application) {
var readAllIngredients = MediatorLiveData<List<Ingredient>>().apply { value = emptyList() }
private val ingredientRepository: IngredientRepository
private var gotNetworkIngredients = false
init {
val ingredientDAO = ShoppingAppDatabase.getDatabase(application).ingredientDAO()
ingredientRepository = IngredientRepository(ingredientDAO)
readAllIngredients.addSource(ingredientRepository.allIngredients) { repoValue ->
if (!gotNetworkIngredients) {
readAllIngredients.value = repoValue
}
}
viewModelScope.launch(Dispatchers.IO) {
// logic moved from repository in order to make example more concise
if(NetworkService.serverReachable()) {
gotNetworkIngredients = true
readAllIngredients.postValue(NetworkService.service.getIngredients())
}
else {
//save the data from the database somehow; used to do it like
//readAllIngredients = ingredientRepository.allIngredients
//when readAllIngredients was of type `LiveData<List<Ingredient>>`
}
}
}
}

How to adjust table name in Kotlin Exposed on runtime

We are using database table names which are prefixed with environment names e.g:
instead of just 'Cities' we have 'ci_Cities', 'dev_Cities' and 'prod_Cities'.
The problem is that Schema definitions are based on Kotlin objects, which is nice in an usage, but doesn't allow me to simply inject table prefix in e.g. constructor.
So the question is how to implement such a functionality in Kotlin-Exposed?
In the end I have found solution, which seems to be quite elegant.
But I think, that some improvements could be done also in Kotlin Exposed, so that in most cases solution is more concise.
City.kt
data class City(val id: Int, val name: String, val timestamp: Instant)
Schema.kt
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.`java-time`.timestamp
class CitiesSchema(environment: String) {
val cities = CitiesTable(environment)
}
class CitiesTable(environment: String) : Table(environment + "_Cities") {
val id = varchar("id", 99)
val name = varchar("name", 99)
val timestamp = timestamp("timestamp")
}
CitiesRepository.kt
class CitiesRepository(dataSource: DataSource, private val schema: CitiesSchema) {
private val database = Database.connect(dataSource).defaultFetchSize(10000)
override fun save(city: City): City {
transaction(database) {
schema.cities.insert {
it[schema.cities.id] = city.id
it[schema.cities.name] = city.name
it[schema.cities.timestamp] = city.timestamp
}
}
return city
}
Then in e.g. Spring you can instantiate your schema:
#Bean
public CitiesSchema schema(#Value("${spring.application.env}") String environment) {
return new CitiesSchema(environment);
}
It would be nice to have in Kotlin Exposed ability to rename tables/columns on runtime. Then it would be possible to access Kotlin objects without additional ceremonies.
Such a feature could look like in Jooq:
https://www.jooq.org/doc/3.14/manual-single-page/#settings-render-mapping

Dynamic/Sectioned RecyclerView, Room and Single Data Class

New to Android development and Kotlin. I’m hoping to use different views based on the properties of my data class, but I’m not really sure how and I’m uncertain if what I want to do is even possible. I know I need to override getItemViewType, and leverage that in onCreateViewHolder, but I’m confused with the code for getItemViewType.
Room Data Class
data class PersonMessages(
#Embedded
val Person: Person,
#Relation(
parentColumn = "id",
entityColumn = "person_id"
)
val Messages: List<Messages>
)
RecyclerView Adapter
class PeopleViewAdapter: ListAdapter<PersonMessages, PeopleViewAdapter.ViewHolder>(PeopleDiffCallback()) {
// ...
override fun getItemViewType(position: Int): Int =
when (getItem(position)) {
is Messages -> R.layout.fragment_message_detail
is Person -> R.layout.fragment_people_detail
else -> throw IllegalStateException("Illegal item view type")
}
}
For getItemViewType, Android Studio correctly complains that Messages and Person are Incompatible types with PersonMessages, but I have no idea as to what I need to change, and where, to get this to work.
All clue sticks appreciated.
I chose to address this by transforming the data in the ViewModel into a list of the embedded classes. While I’m still not certain if this is the best direction, and would appreciate some commentary here, the relevant changes are below.
class MainViewModel(private val repository: DataRepository) : ViewModel() {
//...
init {
_people = Transformations.map(repository.livePeopleMessages()) { peopleWithMessages ->
peopleWithMessages.flatMap {
mutableListOf<Any>(it.Person).also { personWithMessages ->
personWithMessages.addAll(it.Messages)
}
}
}
}
//...
}
class PeopleViewAdapter: ListAdapter<Any, RecyclerView.ViewHolder>(PeopleDiffCallback()) {//...}

What is the benefit of using primarykey and references method in class jooq

I'm start the learn jooq. I have mssql server. I create some class the represent table on my server. But I don't understand what is the benefit when I was using getPrimaryKey and getReferences methods in my table class?
class User : TableImpl<Record>("users") {
companion object {
val USER = User()
}
val id: TableField<Record, Int> = createField("id", SQLDataType.INTEGER)
val name: TableField<Record, String> = createField("name", SQLDataType.NVARCHAR(50))
val countryId: TableField<Record, Short> = createField("country_id", SQLDataType.SMALLINT)
override fun getPrimaryKey(): UniqueKey<Record> = Internal.createUniqueKey(this, id)
override fun getReferences(): MutableList<ForeignKey<Record, *>> =
mutableListOf(Internal.createForeignKey(primaryKey, COUNTRY, COUNTRY.id))
}
class Country : TableImpl<Record>("country") {
companion object {
val COUNTRY = Country()
}
val id: TableField<Record, Short> = createField("id", SQLDataType.SMALLINT)
val name: TableField<Record, String> = createField("name", SQLDataType.NVARCHAR(100))
override fun getPrimaryKey(): UniqueKey<Record> =
Internal.createUniqueKey(this, id)
}
The generated meta data is a mix of stuff that's useful...
to you, the API user
to jOOQ, which can reflect on that meta data for a few internal features
For instance, in the case of getPrimaryKey(), that method helps with all sorts of CRUD related operations as you can see in the manual:
https://www.jooq.org/doc/latest/manual/sql-execution/crud-with-updatablerecords/simple-crud
If you're not using the code generator (which would generate all of these methods for you), then there is no need to add them to your classes. You could shorten them to this:
class User : TableImpl<Record>("users") {
companion object {
val USER = User()
}
val id: Field<Int> = createField("id", SQLDataType.INTEGER)
val name: Field<String> = createField("name", SQLDataType.NVARCHAR(50))
val countryId: Field<Short> = createField("country_id", SQLDataType.SMALLINT)
}
However, using the code generator is strongly recommended for a variety of advanced jOOQ features which you might not get, otherwise.