Move stuff in base
This commit is contained in:
170
src/jvmMain/kotlin/nl/astraeus/vst/base/db/BaseDao.kt
Normal file
170
src/jvmMain/kotlin/nl/astraeus/vst/base/db/BaseDao.kt
Normal file
@@ -0,0 +1,170 @@
|
||||
package nl.astraeus.vst.base.db
|
||||
|
||||
import kotlinx.datetime.Instant
|
||||
import nl.astraeus.vst.string.logger.log
|
||||
import java.sql.PreparedStatement
|
||||
import java.sql.ResultSet
|
||||
import java.sql.Timestamp
|
||||
|
||||
fun Instant.toSqlTimestamp() = Timestamp(this.toEpochMilliseconds())
|
||||
fun Timestamp.toDateTimeInstant() = Instant.fromEpochMilliseconds(this.time)
|
||||
|
||||
data class SqlStatement<T : Entity>(
|
||||
val sql: String,
|
||||
val prepareParameters: T.(PreparedStatement) -> Unit
|
||||
)
|
||||
|
||||
data class SqlQuery<T : Entity>(
|
||||
val sql: String,
|
||||
val resultMapper: (ResultSet) -> T
|
||||
)
|
||||
|
||||
abstract class QueryProvider<T : Entity> {
|
||||
abstract val tableName: String
|
||||
open val idQuery: String
|
||||
get() = "SELECT * FROM $tableName WHERE ID = ?"
|
||||
abstract val resultSetMapper: (ResultSet) -> T
|
||||
open val find: SqlQuery<T>
|
||||
get() = SqlQuery(
|
||||
idQuery,
|
||||
resultSetMapper
|
||||
)
|
||||
abstract val insert: SqlStatement<T>
|
||||
abstract val update: SqlStatement<T>
|
||||
open val delete: SqlStatement<T>
|
||||
get() = SqlStatement(
|
||||
"DELETE FROM $tableName WHERE ID = ?"
|
||||
) { ps ->
|
||||
ps.setLong(1, getPK()[0] as Long)
|
||||
}
|
||||
}
|
||||
|
||||
abstract class BaseDao<T : Entity> {
|
||||
abstract val queryProvider: QueryProvider<T>
|
||||
open val autogeneratedPrimaryKey: Boolean = true
|
||||
|
||||
open fun insert(entity: T) {
|
||||
executeInsert(entity, "insert", queryProvider.insert)
|
||||
}
|
||||
|
||||
open fun update(entity: T): Int = executeUpdate(
|
||||
entity,
|
||||
"update",
|
||||
queryProvider.update,
|
||||
true
|
||||
)
|
||||
|
||||
open fun upsert(entity: T) {
|
||||
if ((entity.getPK()[0] as Long) == 0L) {
|
||||
insert(entity)
|
||||
} else {
|
||||
update(entity)
|
||||
}
|
||||
}
|
||||
|
||||
open fun delete(entity: T) {
|
||||
executeUpdate(entity, "delete", queryProvider.delete, true)
|
||||
}
|
||||
|
||||
open fun find(
|
||||
id: Long
|
||||
): T? {
|
||||
return executeQuery(
|
||||
"find",
|
||||
queryProvider.find
|
||||
) { ps ->
|
||||
ps.setLong(1, id)
|
||||
}.firstOrNull()
|
||||
}
|
||||
|
||||
protected fun executeSQLUpdate(
|
||||
sql: String,
|
||||
parameterSetter: (PreparedStatement) -> Unit
|
||||
): Int {
|
||||
return Database.transaction { con ->
|
||||
con.prepareStatement(sql).use { ps ->
|
||||
parameterSetter(ps)
|
||||
|
||||
ps.executeUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun executeQuery(
|
||||
label: String,
|
||||
statement: SqlQuery<T>,
|
||||
prepareParameters: (PreparedStatement) -> Unit,
|
||||
): List<T> {
|
||||
return Database.transaction { con ->
|
||||
log.debug { "Executing query [$label] - [${statement.sql}]" }
|
||||
val result = mutableListOf<T>()
|
||||
|
||||
con.prepareStatement(statement.sql)?.use { ps ->
|
||||
prepareParameters(ps)
|
||||
|
||||
val rs = ps.executeQuery()
|
||||
|
||||
while (rs.next()) {
|
||||
result.add(statement.resultMapper(rs))
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
protected fun executeInsert(
|
||||
entity: T,
|
||||
label: String,
|
||||
statement: SqlStatement<T>,
|
||||
checkSingleRow: Boolean = false
|
||||
) {
|
||||
Database.transaction { con ->
|
||||
log.debug { "Executing insert [$label] - [${statement.sql}] - [$entity]" }
|
||||
con.prepareStatement(statement.sql)?.use { ps ->
|
||||
statement.prepareParameters(entity, ps)
|
||||
|
||||
val rows = if (checkSingleRow) {
|
||||
ps.execute()
|
||||
1
|
||||
} else {
|
||||
ps.executeUpdate()
|
||||
}
|
||||
|
||||
if (autogeneratedPrimaryKey) {
|
||||
val keyResult = ps.generatedKeys
|
||||
if (keyResult.next()) {
|
||||
entity.setPK(arrayOf(keyResult.getLong(1)))
|
||||
}
|
||||
}
|
||||
|
||||
check(rows == 1) {
|
||||
"Statement [$label] affected more than 1 row! [${statement.sql}]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun executeUpdate(
|
||||
entity: T,
|
||||
label: String,
|
||||
statement: SqlStatement<T>,
|
||||
checkSingleRow: Boolean = false
|
||||
): Int = Database.transaction { con ->
|
||||
var rows = 1
|
||||
|
||||
log.debug { "Executing update [$label] - [${statement.sql}] - [$entity]" }
|
||||
con.prepareStatement(statement.sql)?.use { ps ->
|
||||
statement.prepareParameters(entity, ps)
|
||||
|
||||
rows = ps.executeUpdate()
|
||||
|
||||
check(checkSingleRow || rows == 1) {
|
||||
"Statement [$label] affected more than 1 row! [${statement.sql}]"
|
||||
}
|
||||
}
|
||||
|
||||
rows
|
||||
}
|
||||
|
||||
}
|
||||
84
src/jvmMain/kotlin/nl/astraeus/vst/base/db/Database.kt
Normal file
84
src/jvmMain/kotlin/nl/astraeus/vst/base/db/Database.kt
Normal file
@@ -0,0 +1,84 @@
|
||||
package nl.astraeus.vst.base.db
|
||||
|
||||
import com.zaxxer.hikari.HikariConfig
|
||||
import com.zaxxer.hikari.HikariDataSource
|
||||
import nl.astraeus.vst.base.Settings
|
||||
import java.sql.Connection
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
val DATABASE_MIGRATIONS = arrayOf<Migration>(
|
||||
Migration.Query(
|
||||
"""
|
||||
CREATE TABLE DATABASE_VERSION (
|
||||
ID INTEGER PRIMARY KEY,
|
||||
QUERY TEXT,
|
||||
EXECUTED TIMESTAMP
|
||||
)
|
||||
""".trimIndent()
|
||||
),
|
||||
Migration.Query(PATCH_CREATE_QUERY),
|
||||
)
|
||||
|
||||
object Database {
|
||||
|
||||
private var ds: HikariDataSource? = null
|
||||
private val currentConnection = ThreadLocal<Connection>()
|
||||
|
||||
fun start() {
|
||||
Class.forName("nl.astraeus.jdbc.Driver")
|
||||
|
||||
val properties = Properties()
|
||||
properties["journal_mode"] = "WAL"
|
||||
|
||||
val config = HikariConfig().apply {
|
||||
driverClassName = Settings.jdbcDriver
|
||||
jdbcUrl = Settings.jdbcConnectionUrl
|
||||
username = Settings.jdbcUser
|
||||
password = Settings.jdbcPassword
|
||||
maximumPoolSize = 25
|
||||
isAutoCommit = false
|
||||
|
||||
validate()
|
||||
}
|
||||
|
||||
config.dataSourceProperties = properties
|
||||
config.addDataSourceProperty("cachePrepStmts", "true")
|
||||
config.addDataSourceProperty("prepStmtCacheSize", "250")
|
||||
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048")
|
||||
|
||||
ds = HikariDataSource(config)
|
||||
Migrations.databaseVersionTableCreated = AtomicBoolean(false)
|
||||
Migrations.updateDatabaseIfNeeded(DATABASE_MIGRATIONS)
|
||||
}
|
||||
|
||||
private fun getConnection() = ds?.connection ?: error("Database has not been initialized!")
|
||||
|
||||
fun <T> transaction(
|
||||
block: (Connection) -> T
|
||||
): T {
|
||||
val hasConnection = currentConnection.get() != null
|
||||
|
||||
if (!hasConnection) {
|
||||
currentConnection.set(getConnection())
|
||||
}
|
||||
|
||||
val connection = currentConnection.get()
|
||||
|
||||
try {
|
||||
val result = block(connection)
|
||||
|
||||
if (!hasConnection) {
|
||||
connection.commit()
|
||||
}
|
||||
|
||||
return result
|
||||
} finally {
|
||||
if (!hasConnection) {
|
||||
connection.close()
|
||||
currentConnection.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
16
src/jvmMain/kotlin/nl/astraeus/vst/base/db/Entity.kt
Normal file
16
src/jvmMain/kotlin/nl/astraeus/vst/base/db/Entity.kt
Normal file
@@ -0,0 +1,16 @@
|
||||
package nl.astraeus.vst.base.db
|
||||
|
||||
interface Entity {
|
||||
fun getPK(): Array<Any>
|
||||
fun setPK(pks: Array<Any>)
|
||||
}
|
||||
|
||||
interface EntityId : Entity {
|
||||
var id: Long
|
||||
|
||||
override fun getPK(): Array<Any> = arrayOf(id)
|
||||
|
||||
override fun setPK(pks: Array<Any>) {
|
||||
id = pks[0] as Long
|
||||
}
|
||||
}
|
||||
97
src/jvmMain/kotlin/nl/astraeus/vst/base/db/Migrations.kt
Normal file
97
src/jvmMain/kotlin/nl/astraeus/vst/base/db/Migrations.kt
Normal file
@@ -0,0 +1,97 @@
|
||||
package nl.astraeus.vst.base.db
|
||||
|
||||
import nl.astraeus.vst.base.db.Database.transaction
|
||||
import java.sql.Connection
|
||||
import java.sql.SQLException
|
||||
import java.sql.Timestamp
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
sealed class Migration {
|
||||
class Query(
|
||||
val query: String
|
||||
) : Migration() {
|
||||
override fun toString(): String {
|
||||
return query
|
||||
}
|
||||
}
|
||||
|
||||
class Code(
|
||||
val code: (Connection) -> Unit
|
||||
) : Migration() {
|
||||
override fun toString(): String {
|
||||
return code.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Migrations {
|
||||
var databaseVersionTableCreated = AtomicBoolean(false)
|
||||
|
||||
fun updateDatabaseIfNeeded(
|
||||
migrations: Array<Migration>
|
||||
) {
|
||||
try {
|
||||
Database.transaction { con ->
|
||||
con.prepareStatement(
|
||||
"""
|
||||
SELECT MAX(ID) FROM DATABASE_VERSION
|
||||
""".trimIndent()
|
||||
).use { ps ->
|
||||
ps.executeQuery().use { rs ->
|
||||
databaseVersionTableCreated.compareAndSet(false, true)
|
||||
|
||||
if (rs.next()) {
|
||||
val maxId = rs.getInt(1)
|
||||
|
||||
for (index in maxId + 1..<migrations.size) {
|
||||
executeMigration(index, migrations[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: SQLException) {
|
||||
if (databaseVersionTableCreated.compareAndSet(false, true)) {
|
||||
executeMigration(0, migrations[0])
|
||||
updateDatabaseIfNeeded(migrations)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun executeMigration(
|
||||
index: Int,
|
||||
migration: Migration
|
||||
) {
|
||||
transaction { con ->
|
||||
/* log.debug {
|
||||
"Executing migration $index - [${migration}]"
|
||||
}*/
|
||||
val description = when (migration) {
|
||||
is Migration.Query -> {
|
||||
@Suppress("SqlSourceToSinkFlow")
|
||||
con.prepareStatement(migration.query).use { ps ->
|
||||
ps.execute()
|
||||
}
|
||||
|
||||
migration.query
|
||||
}
|
||||
|
||||
is Migration.Code -> {
|
||||
migration.code(con)
|
||||
|
||||
migration.code.toString()
|
||||
}
|
||||
}
|
||||
con.prepareStatement("INSERT INTO DATABASE_VERSION VALUES (?, ?, ?)").use { ps ->
|
||||
ps.setInt(1, index)
|
||||
ps.setString(2, description)
|
||||
ps.setTimestamp(3, Timestamp(System.currentTimeMillis()))
|
||||
|
||||
ps.execute()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
31
src/jvmMain/kotlin/nl/astraeus/vst/base/db/PatchDao.kt
Normal file
31
src/jvmMain/kotlin/nl/astraeus/vst/base/db/PatchDao.kt
Normal file
@@ -0,0 +1,31 @@
|
||||
package nl.astraeus.vst.base.db
|
||||
|
||||
object PatchDao : BaseDao<PatchEntity>() {
|
||||
|
||||
override val queryProvider: QueryProvider<PatchEntity>
|
||||
get() = PatchEntityQueryProvider
|
||||
|
||||
fun create(
|
||||
patchId: String,
|
||||
patch: String
|
||||
): PatchEntity {
|
||||
val result = PatchEntity(
|
||||
0,
|
||||
patchId,
|
||||
patch
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun findById(patchId: String): PatchEntity? = executeQuery(
|
||||
"findById",
|
||||
SqlQuery(
|
||||
"SELECT * FROM ${queryProvider.tableName} WHERE PATCH_ID = ?",
|
||||
queryProvider.resultSetMapper
|
||||
)
|
||||
) { ps ->
|
||||
ps.setString(1, patchId)
|
||||
}.firstOrNull()
|
||||
|
||||
}
|
||||
12
src/jvmMain/kotlin/nl/astraeus/vst/base/db/PatchEntity.kt
Normal file
12
src/jvmMain/kotlin/nl/astraeus/vst/base/db/PatchEntity.kt
Normal file
@@ -0,0 +1,12 @@
|
||||
package nl.astraeus.vst.base.db
|
||||
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
|
||||
data class PatchEntity(
|
||||
override var id: Long,
|
||||
var patchId: String,
|
||||
var patch: String,
|
||||
var created: Instant = Clock.System.now(),
|
||||
var updated: Instant = Clock.System.now(),
|
||||
) : EntityId
|
||||
@@ -0,0 +1,64 @@
|
||||
package nl.astraeus.vst.base.db
|
||||
|
||||
import java.sql.ResultSet
|
||||
import java.sql.Types
|
||||
|
||||
val PATCH_CREATE_QUERY = """
|
||||
CREATE TABLE INSTRUMENTS (
|
||||
ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
PATCH_ID TEXT,
|
||||
PATCH TEXT,
|
||||
CREATED TIMESTAMP,
|
||||
UPDATED TIMESTAMP
|
||||
)
|
||||
""".trimIndent()
|
||||
|
||||
object PatchEntityQueryProvider : QueryProvider<PatchEntity>() {
|
||||
override val tableName: String
|
||||
get() = "INSTRUMENTS"
|
||||
override val resultSetMapper: (ResultSet) -> PatchEntity
|
||||
get() = { rs ->
|
||||
PatchEntity(
|
||||
rs.getLong(1),
|
||||
rs.getString(2),
|
||||
rs.getString(3),
|
||||
rs.getTimestamp(4).toDateTimeInstant(),
|
||||
rs.getTimestamp(5).toDateTimeInstant()
|
||||
)
|
||||
}
|
||||
override val insert: SqlStatement<PatchEntity>
|
||||
get() = SqlStatement(
|
||||
"""
|
||||
INSERT INTO $tableName (
|
||||
ID,
|
||||
PATCH_ID,
|
||||
PATCH,
|
||||
CREATED,
|
||||
UPDATED
|
||||
) VALUES (
|
||||
?,?,?,?,?
|
||||
)
|
||||
""".trimIndent()
|
||||
) { ps ->
|
||||
ps.setNull(1, Types.BIGINT)
|
||||
ps.setString(2, patchId)
|
||||
ps.setString(3, patch)
|
||||
ps.setTimestamp(4, created.toSqlTimestamp())
|
||||
ps.setTimestamp(5, updated.toSqlTimestamp())
|
||||
}
|
||||
override val update: SqlStatement<PatchEntity>
|
||||
get() = SqlStatement(
|
||||
"""
|
||||
UPDATE $tableName
|
||||
SET PATCH_ID = ?,
|
||||
PATCH = ?,
|
||||
UPDATED = ?
|
||||
WHERE ID = ?
|
||||
""".trimIndent()
|
||||
) { ps ->
|
||||
ps.setString(1, patchId)
|
||||
ps.setString(2, patch)
|
||||
ps.setTimestamp(3, updated.toSqlTimestamp())
|
||||
ps.setLong(4, id)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user