Blo
Blo

Reputation: 11988

Generic getter for multiple Collection in Mongo with Kotlin

I use Kotlin and Mongo (with KMongo) and I have multiple models as UserEntity, MovieEntity and so on. Each of them use a specific Dao class to do (actually) the same methods. Therefore, I'm trying to avoid any duplication by using a BaseDao which should have these methods instead.

So I pass the specific entity in the generic base as:

class UserDao : BaseDao<UserEntity>() { ... }

This base class implements the generic methods as follows:

open class BaseDao<T: Any>() {

    fun get(id: String): T? {
        return getCollection().findOneById(id)
    }

    fun save(entity: T): T {
        return getCollection().save(entity)
    }

    fun delete(id: String) {
        getCollection().deleteOneById(id)
    }
    ...
}

However, a problem occurs on getCollection() method:

private inline fun <reified T: Any> getCollection(): MongoCollection<T> {
    return MongoDb.getDatabase().getCollection<T>()
}

This gets a compilation error each time I call it:

Type inference failed: Not enough information to infer parameter T in 
inline fun <reified T : Any> getCollection(): MongoCollection<T#1 (type parameter of app.api.db.dao.BaseDao.getCollection)>  
Please specify it explicitly.

I can't find the right way to do this. I already checked these threads but I didn't make it work: Generic class type usage in Kotlin & Kotlin abstract class with generic param and methods which use type param.

Question:

How can I achieve this generic BaseDao which should get any collection of each child Dao?

Upvotes: 2

Views: 1215

Answers (3)

ruX
ruX

Reputation: 7472

(For KMongo 4.0.+) no need to use reified generics for the each method, instead this base class can be used as as a starting point:

open class BaseDao<T: Any>(
    protected val collection: CoroutineCollection<T>
) {

    suspend fun get(id: Id<T>): T? {
        return collection.findOneById(id)
    }

    suspend fun save(entity: T): UpdateResult? {
        return collection.save(entity)
    }

    suspend fun delete(id: Id<T>) {
        collection.deleteOneById(id)
    }
}

And implemented in the particular DAO, say SessionDao:

class SessionDao(collection: CoroutineCollection<DbSession>) 
      : BaseDao<DbSession>(collection)

(note: inheritance can be replaced with delegation by using by keyword if one feel better this way

This and other dao can be created via DI or some sort of dao factory:

class DbInstance(mongodbConnectionString: String = "mongodb://localhost:27017/myproject") {
    private val connectionInfo = ConnectionString(mongodbConnectionString)
    val client = KMongo.createClient().coroutine
    val db = client.getDatabase(
        connectionInfo.database ?: throw IllegalArgumentException("mongodb connection string must include db name")
    )


    val sessions = SessionDao(db.getCollection())
}


Notes:

  1. This example is for the coroutine based kmongo, it can be easly converted to blocking kmongo by replacing CoroutineCollection to MongoCollection
  2. I assume documents id's are annotated via Id container which helps to mitigate errors, so documents should be created in this fashion:
     data class DbSession(
         @BsonId
         val id: Id<DbSession>,
    
         val name: String,
     ) 
    
    

Upvotes: 1

Blo
Blo

Reputation: 11988

The solution is to use reflection as Zigzago mentioned by using KMongoUtil:

protected fun getCollection(): MongoCollection<T> =
    getDaoEntityClass().let { k ->
        MongoDb.getDatabase().getCollection(
            KMongoUtil.defaultCollectionName(k), k.java)
    }

@Suppress("UNCHECKED_CAST")
private fun getDaoEntityClass(): KClass<T>
    = ((this::class.java.genericSuperclass
        as ParameterizedType).actualTypeArguments[0] as Class<T>).kotlin

Upvotes: 0

J. Leander
J. Leander

Reputation: 415

the JVM forgets the type of the generic T in BaseDao<T: Any>() at runtime, which is why type inference fails. A solution to this could be to pass the KClass of T in the constructor of BaseDao:

open class BaseDao<T: Any>(val kClass: KClass<T>) {
    ...
}

After this, give your reified function an argument that accepts a `KClass:

private inline fun <reified T: Any> getCollection(val kClass: KClass<T>):  MongoCollection<T> {
    return MongoDb.getDatabase().getCollection<T>()
}

I'm unaware of a method to do this without passing the KClass as a argument to the function, but this should work, as the generic T can be derived from the provided kClass. `

Another way would be to make all methods in BaseDao inline function with reified generics and dropping the generic on the class.

open class BaseDao() {

    inline fun <reified T: Any> get(id: String): T? {
        return getCollection().findOneById(id)
    }

    inline fun <reified T: Any> save (entity: T): T {
        return getCollection().save(entity)
    }

    inline fun <reified T: Any> delete(id: String) {
        getCollection().deleteOneById(id)
    }
    ...
}

This way the generic T can be derived since the method calling getCollection is also reified.

Upvotes: 1

Related Questions