Files
postgres-json/src/main/kotlin/fr/postgresjson/connexion/Connection.kt
2021-07-17 14:05:50 +02:00

270 lines
8.7 KiB
Kotlin

package fr.postgresjson.connexion
import com.fasterxml.jackson.core.type.TypeReference
import com.github.jasync.sql.db.Connection
import com.github.jasync.sql.db.QueryResult
import com.github.jasync.sql.db.pool.ConnectionPool
import com.github.jasync.sql.db.postgresql.PostgreSQLConnection
import com.github.jasync.sql.db.postgresql.PostgreSQLConnectionBuilder
import com.github.jasync.sql.db.util.length
import fr.postgresjson.entity.EntityI
import fr.postgresjson.entity.Serializable
import fr.postgresjson.serializer.Serializer
import fr.postgresjson.utils.LoggerDelegate
import org.slf4j.Logger
import java.util.concurrent.CompletableFuture
typealias SelectOneCallback<T> = QueryResult.(T?) -> Unit
typealias SelectCallback<T> = QueryResult.(List<T>) -> Unit
typealias SelectPaginatedCallback<T> = QueryResult.(Paginated<T>) -> Unit
class Connection(
private val database: String,
private val username: String,
private val password: String,
private val host: String = "localhost",
private val port: Int = 5432
) : Executable {
private var connection: ConnectionPool<PostgreSQLConnection>? = null
private val serializer = Serializer()
private val logger: Logger? by LoggerDelegate()
internal fun connect(): ConnectionPool<PostgreSQLConnection> {
return connection.let { connectionPool ->
if (connectionPool == null || !connectionPool.isConnected()) {
PostgreSQLConnectionBuilder.createConnectionPool(
"jdbc:postgresql://$host:$port/$database?user=$username&password=$password"
).also {
connection = it
}
} else {
connectionPool
}
}
}
fun disconnect() {
connection?.disconnect()
}
fun <A> inTransaction(f: (Connection) -> CompletableFuture<A>) = connect().inTransaction(f)
/**
* Select One [EntityI] with [List] of parameters
*/
override fun <R : EntityI> selectOne(
sql: String,
typeReference: TypeReference<R>,
values: List<Any?>,
block: (QueryResult, R?) -> Unit
): R? {
val result = exec(sql, compileArgs(values))
val json = result.rows.firstOrNull()?.getString(0)
return if (json === null) {
null
} else {
serializer.deserialize(json, typeReference)
}.also {
block(result, it)
}
}
/**
* Select One [EntityI] with named parameters
*/
override fun <R : EntityI> selectOne(
sql: String,
typeReference: TypeReference<R>,
values: Map<String, Any?>,
block: (QueryResult, R?) -> Unit
): R? {
return replaceArgs(sql, values) {
selectOne(this.sql, typeReference, parameters, block)
}
}
/* Select Multiples */
/**
* Select multiple [EntityI] with [List] of parameters
*/
override fun <R : EntityI> select(
sql: String,
typeReference: TypeReference<List<R>>,
values: List<Any?>,
block: (QueryResult, List<R>) -> Unit
): List<R> {
val result = exec(sql, values)
val json = result.rows[0].getString(0)
return if (json === null) {
listOf<EntityI>() as List<R>
} else {
serializer.deserializeList(json, typeReference)
}.also {
block(result, it)
}
}
/**
* Select multiple [EntityI] with [Map] of parameters
*/
override fun <R : EntityI> select(
sql: String,
typeReference: TypeReference<List<R>>,
values: Map<String, Any?>,
block: (QueryResult, List<R>) -> Unit
): List<R> {
return replaceArgs(sql, values) {
select(this.sql, typeReference, this.parameters, block)
}
}
/* Select Paginated */
/**
* Select Multiple [EntityI] with pagination
*/
override fun <R : EntityI> select(
sql: String,
page: Int,
limit: Int,
typeReference: TypeReference<List<R>>,
values: Map<String, Any?>,
block: (QueryResult, Paginated<R>) -> Unit
): Paginated<R> {
val offset = (page - 1) * limit
val newValues = values
.plus("offset" to offset)
.plus("limit" to limit)
val line = replaceArgs(sql, newValues) {
exec(this.sql, this.parameters)
}
return line.run {
val json = rows[0].getString(0)
val entities = if (json === null) {
listOf<EntityI>() as List<R>
} else {
serializer.deserializeList(json, typeReference)
}
Paginated(
entities,
offset,
limit,
rows[0].getInt("total") ?: error("The query not return total")
)
}.also {
block(line, it)
}
}
override fun exec(sql: String, values: List<Any?>): QueryResult {
val compiledValues = compileArgs(values)
return stopwatchQuery(sql, compiledValues) {
connect().sendPreparedStatement(sql, compiledValues).join()
}
}
override fun exec(sql: String, values: Map<String, Any?>): QueryResult {
return replaceArgs(sql, values) {
exec(this.sql, this.parameters)
}
}
override fun sendQuery(sql: String, values: List<Any?>): Int {
val compiledValues = compileArgs(values)
return stopwatchQuery(sql, compiledValues) {
replaceArgsIntoSql(sql, compiledValues) {
connect().sendQuery(it).join().rowsAffected.toInt()
}
}
}
override fun sendQuery(sql: String, values: Map<String, Any?>): Int {
return replaceArgs(sql, values) {
sendQuery(this.sql, this.parameters)
}
}
private fun compileArgs(values: List<Any?>): List<Any?> {
return values.map {
if (it is Serializable || (it is List<*> && it.firstOrNull() is Serializable)) {
serializer.serialize(it)
} else {
it
}
}
}
private fun <T> replaceArgs(sql: String, values: Map<String, Any?>, block: ParametersQuery.() -> T): T {
val paramRegex = "(?<!:):([a-zA-Z0-9_-]+)".toRegex(RegexOption.IGNORE_CASE)
val newArgs = paramRegex.findAll(sql).map { match ->
val name = match.groups[1]!!.value
values[name] ?: values[name.trimStart('_')] ?: error("Parameter $name missing")
}.toList()
var newSql = sql
values.forEach { (key, _) ->
val regex = ":_?$key".toRegex()
newSql = newSql.replace(regex, "?")
}
return block(ParametersQuery(newSql, newArgs))
}
private fun <T> replaceArgsIntoSql(sql: String, values: List<Any?>, block: (String) -> T): T {
val paramRegex = "(?<!\\?)(\\?)(?!\\?)".toRegex(RegexOption.IGNORE_CASE)
var i = 0
if (values.isNotEmpty()) {
val newSql = paramRegex.replace(sql) {
values[i] ?: error("Parameter $i missing")
val valToReplace = values[i].toString()
++i
"'$valToReplace'"
}
return block(newSql)
}
return block(sql)
}
data class ParametersQuery(val sql: String, val parameters: List<Any?>)
private fun <T> stopwatchQuery(sql: String, values: List<Any?> = emptyList(), callback: () -> T): T {
try {
val start = System.currentTimeMillis()
val result = callback()
val duration = (System.currentTimeMillis() - start)
val resultText = when (result) {
null -> "with no result"
is QueryResult -> result.rows.firstOrNull()?.joinToString(", ")?.let { text ->
if (text.length > 100) "${text.take(100)}... (size: ${text.length})" else text
} ?: "with no result"
else -> "unknown"
}
val args = """
|Query ($duration ms):
|${sql.trimIndent().prependIndent()}
|Arguments (${values.length}):
|${values.joinToString("\n").ifBlank { "No arguments" }.prependIndent()}
|Result:
|${resultText.trimIndent().prependIndent()}
""".trimMargin().prependIndent(" > ")
logger?.debug("Query executed in $duration ms \n{}", args)
return result
} catch (e: Throwable) {
logger?.info(
"""
Query Error:
${sql.prependIndent()},
${values.joinToString(", ").prependIndent()}
""".trimIndent(),
e
)
throw e
}
}
}