mirror of
https://github.com/jlengrand/Exposed.git
synced 2026-03-10 08:11:20 +00:00
exposed-dao module introduced
This commit is contained in:
@@ -1,832 +0,0 @@
|
||||
package org.jetbrains.exposed.dao
|
||||
|
||||
import org.jetbrains.exposed.exceptions.EntityNotFoundException
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.statements.EntityBatchUpdate
|
||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.properties.Delegates
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KProperty
|
||||
import kotlin.reflect.full.primaryConstructor
|
||||
|
||||
/**
|
||||
* @author max
|
||||
*/
|
||||
class EntityID<T:Comparable<T>>(id: T?, val table: IdTable<T>) : Comparable<EntityID<T>> {
|
||||
var _value: Any? = id
|
||||
val value: T get() {
|
||||
if (_value == null) {
|
||||
TransactionManager.current().entityCache.flushInserts(table)
|
||||
assert(_value != null) { "Entity must be inserted" }
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return _value!! as T
|
||||
}
|
||||
|
||||
override fun toString() = value.toString()
|
||||
|
||||
override fun hashCode() = value.hashCode()
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is EntityID<*>) return false
|
||||
|
||||
return other._value == _value && other.table == table
|
||||
}
|
||||
|
||||
override fun compareTo(other: EntityID<T>): Int = value.compareTo(other.value)
|
||||
}
|
||||
|
||||
open class ColumnWithTransform<TColumn, TReal>(val column: Column<TColumn>, val toColumn: (TReal) -> TColumn, val toReal: (TColumn) -> TReal)
|
||||
|
||||
class View<out Target: Entity<*>> (val op : Op<Boolean>, val factory: EntityClass<*, Target>) : SizedIterable<Target> {
|
||||
override fun limit(n: Int, offset: Int): SizedIterable<Target> = factory.find(op).limit(n, offset)
|
||||
override fun count(): Int = factory.find(op).count()
|
||||
override fun empty(): Boolean = factory.find(op).empty()
|
||||
override fun forUpdate(): SizedIterable<Target> = factory.find(op).forUpdate()
|
||||
override fun notForUpdate(): SizedIterable<Target> = factory.find(op).notForUpdate()
|
||||
|
||||
override operator fun iterator(): Iterator<Target> = factory.find(op).iterator()
|
||||
operator fun getValue(o: Any?, desc: KProperty<*>): SizedIterable<Target> = factory.find(op)
|
||||
override fun copy(): SizedIterable<Target> = View(op, factory)
|
||||
override fun orderBy(vararg order: Pair<Expression<*>, SortOrder>): SizedIterable<Target> = factory.find(op).orderBy(*order)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class InnerTableLink<SID:Comparable<SID>, Source: Entity<SID>, ID:Comparable<ID>, Target: Entity<ID>>(
|
||||
val table: Table,
|
||||
val target: EntityClass<ID, Target>,
|
||||
val sourceColumn: Column<EntityID<SID>>? = null,
|
||||
_targetColumn: Column<EntityID<ID>>? = null) : ReadWriteProperty<Source, SizedIterable<Target>> {
|
||||
init {
|
||||
_targetColumn?.let {
|
||||
requireNotNull(sourceColumn) { "Both source and target columns should be specified"}
|
||||
require(_targetColumn.referee?.table == target.table) {
|
||||
"Column $_targetColumn point to wrong table, expected ${target.table.tableName}"
|
||||
}
|
||||
require(_targetColumn.table == sourceColumn.table) {
|
||||
"Both source and target columns should be from the same table"
|
||||
}
|
||||
}
|
||||
sourceColumn?.let {
|
||||
requireNotNull(_targetColumn) { "Both source and target columns should be specified"}
|
||||
}
|
||||
}
|
||||
|
||||
private val targetColumn = _targetColumn
|
||||
?: table.columns.singleOrNull { it.referee == target.table.id } as? Column<EntityID<ID>>
|
||||
?: error("Table does not reference target")
|
||||
|
||||
private fun getSourceRefColumn(o: Source): Column<EntityID<SID>> {
|
||||
return sourceColumn ?: table.columns.singleOrNull { it.referee == o.klass.table.id } as? Column<EntityID<SID>> ?: error("Table does not reference source")
|
||||
}
|
||||
|
||||
override operator fun getValue(o: Source, unused: KProperty<*>): SizedIterable<Target> {
|
||||
if (o.id._value == null) return emptySized()
|
||||
val sourceRefColumn = getSourceRefColumn(o)
|
||||
val alreadyInJoin = (target.dependsOnTables as? Join)?.alreadyInJoin(table)?: false
|
||||
val entityTables = if (alreadyInJoin) target.dependsOnTables else target.dependsOnTables.join(table, JoinType.INNER, target.table.id, targetColumn)
|
||||
|
||||
val columns = (target.dependsOnColumns + (if (!alreadyInJoin) table.columns else emptyList())
|
||||
- sourceRefColumn).distinct() + sourceRefColumn
|
||||
|
||||
val query = {target.wrapRows(entityTables.slice(columns).select{sourceRefColumn eq o.id})}
|
||||
return TransactionManager.current().entityCache.getOrPutReferrers(o.id, sourceRefColumn, query)
|
||||
}
|
||||
|
||||
override fun setValue(o: Source, unused: KProperty<*>, value: SizedIterable<Target>) {
|
||||
val sourceRefColumn = getSourceRefColumn(o)
|
||||
|
||||
val tx = TransactionManager.current()
|
||||
val entityCache = tx.entityCache
|
||||
entityCache.flush()
|
||||
val oldValue = getValue(o, unused)
|
||||
val existingIds = oldValue.map { it.id }.toSet()
|
||||
entityCache.clearReferrersCache()
|
||||
|
||||
val targetIds = value.map { it.id }
|
||||
table.deleteWhere { (sourceRefColumn eq o.id) and (targetColumn notInList targetIds) }
|
||||
table.batchInsert(targetIds.filter { !existingIds.contains(it) }) { targetId ->
|
||||
this[sourceRefColumn] = o.id
|
||||
this[targetColumn] = targetId
|
||||
}
|
||||
|
||||
// current entity updated
|
||||
EntityHook.registerChange(tx, EntityChange(o.klass, o.id, EntityChangeType.Updated))
|
||||
|
||||
// linked entities updated
|
||||
val targetClass = (value.firstOrNull() ?: oldValue.firstOrNull())?.klass
|
||||
if (targetClass != null) {
|
||||
existingIds.plus(targetIds).forEach {
|
||||
EntityHook.registerChange(tx,EntityChange(targetClass, it, EntityChangeType.Updated))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class Entity<ID:Comparable<ID>>(val id: EntityID<ID>) {
|
||||
var klass: EntityClass<ID, Entity<ID>> by Delegates.notNull()
|
||||
internal set
|
||||
|
||||
var db: Database by Delegates.notNull()
|
||||
internal set
|
||||
|
||||
val writeValues = LinkedHashMap<Column<Any?>, Any?>()
|
||||
var _readValues: ResultRow? = null
|
||||
val readValues: ResultRow
|
||||
get() = _readValues ?: run {
|
||||
val table = klass.table
|
||||
_readValues = klass.searchQuery( Op.build {table.id eq id }).firstOrNull() ?: table.select { table.id eq id }.first()
|
||||
_readValues!!
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates entity fields from database.
|
||||
* Override function to refresh some additional state if any.
|
||||
*
|
||||
* @param flush whether pending entity changes should be flushed previously
|
||||
* @throws EntityNotFoundException if entity no longer exists in database
|
||||
*/
|
||||
open fun refresh(flush: Boolean = false) {
|
||||
if (flush) flush() else writeValues.clear()
|
||||
|
||||
klass.removeFromCache(this)
|
||||
val reloaded = klass[id]
|
||||
TransactionManager.current().entityCache.store(this)
|
||||
_readValues = reloaded.readValues
|
||||
}
|
||||
|
||||
operator fun <REF:Comparable<REF>, RID:Comparable<RID>, T: Entity<RID>> Reference<REF, RID, T>.getValue(o: Entity<ID>, desc: KProperty<*>): T {
|
||||
val refValue = reference.getValue(o, desc)
|
||||
return when {
|
||||
refValue is EntityID<*> && reference.referee<REF>() == factory.table.id -> factory.findById(refValue.value as RID)
|
||||
else -> factory.findWithCacheCondition({ reference.referee!!.getValue(this, desc) == refValue }) { reference.referee<REF>()!! eq refValue }.singleOrNull()
|
||||
} ?: error("Cannot find ${factory.table.tableName} WHERE id=$refValue")
|
||||
}
|
||||
|
||||
operator fun <REF:Comparable<REF>, RID:Comparable<RID>, T: Entity<RID>> Reference<REF, RID, T>.setValue(o: Entity<ID>, desc: KProperty<*>, value: T) {
|
||||
if (db != value.db) error("Can't link entities from different databases.")
|
||||
value.id.value // flush before creating reference on it
|
||||
val refValue = value.run { reference.referee<REF>()!!.getValue(this, desc) }
|
||||
reference.setValue(o, desc, refValue)
|
||||
}
|
||||
|
||||
operator fun <REF:Comparable<REF>, RID:Comparable<RID>, T: Entity<RID>> OptionalReference<REF, RID, T>.getValue(o: Entity<ID>, desc: KProperty<*>): T? {
|
||||
val refValue = reference.getValue(o, desc)
|
||||
return when {
|
||||
refValue == null -> null
|
||||
refValue is EntityID<*> && reference.referee<REF>() == factory.table.id -> factory.findById(refValue.value as RID)
|
||||
else -> factory.findWithCacheCondition({ reference.referee!!.getValue(this, desc) == refValue }) { reference.referee<REF>()!! eq refValue }.singleOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
operator fun <REF:Comparable<REF>, RID:Comparable<RID>, T: Entity<RID>> OptionalReference<REF, RID, T>.setValue(o: Entity<ID>, desc: KProperty<*>, value: T?) {
|
||||
if (value != null && db != value.db) error("Can't link entities from different databases.")
|
||||
value?.id?.value // flush before creating reference on it
|
||||
val refValue = value?.run { reference.referee<REF>()!!.getValue(this, desc) }
|
||||
reference.setValue(o, desc, refValue)
|
||||
}
|
||||
operator fun <T> Column<T>.getValue(o: Entity<ID>, desc: KProperty<*>): T = lookup()
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T, R:Any> Column<T>.lookupInReadValues(found: (T?) -> R?, notFound: () -> R?): R? =
|
||||
if (_readValues?.hasValue(this) == true)
|
||||
found(readValues[this])
|
||||
else
|
||||
notFound()
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "USELESS_CAST")
|
||||
fun <T> Column<T>.lookup(): T = when {
|
||||
writeValues.containsKey(this as Column<out Any?>) -> writeValues[this as Column<out Any?>] as T
|
||||
id._value == null && _readValues?.hasValue(this)?.not() ?: true -> defaultValueFun?.invoke() as T
|
||||
columnType.nullable -> readValues[this]
|
||||
else -> readValues[this]!!
|
||||
}
|
||||
|
||||
operator fun <T> Column<T>.setValue(o: Entity<ID>, desc: KProperty<*>, value: T) {
|
||||
klass.invalidateEntityInCache(o)
|
||||
val currentValue = _readValues?.getOrNull(this)
|
||||
if (writeValues.containsKey(this as Column<out Any?>) || currentValue != value) {
|
||||
if (referee != null) {
|
||||
val entityCache = TransactionManager.current().entityCache
|
||||
if (value is EntityID<*> && value.table == referee!!.table) value.value // flush
|
||||
|
||||
listOfNotNull<Any>(value, currentValue).forEach {
|
||||
entityCache.referrers[it]?.remove(this)
|
||||
}
|
||||
entityCache.removeTablesReferrers(listOf(referee!!.table))
|
||||
}
|
||||
writeValues[this as Column<Any?>] = value
|
||||
}
|
||||
}
|
||||
|
||||
operator fun <TColumn, TReal> ColumnWithTransform<TColumn, TReal>.getValue(o: Entity<ID>, desc: KProperty<*>): TReal =
|
||||
toReal(column.getValue(o, desc))
|
||||
|
||||
operator fun <TColumn, TReal> ColumnWithTransform<TColumn, TReal>.setValue(o: Entity<ID>, desc: KProperty<*>, value: TReal) {
|
||||
column.setValue(o, desc, toColumn(value))
|
||||
}
|
||||
|
||||
infix fun <TID:Comparable<TID>, Target:Entity<TID>> EntityClass<TID, Target>.via(table: Table): InnerTableLink<ID, Entity<ID>, TID, Target> =
|
||||
InnerTableLink(table, this@via)
|
||||
|
||||
fun <TID:Comparable<TID>, Target:Entity<TID>> EntityClass<TID, Target>.via(sourceColumn: Column<EntityID<ID>>, targetColumn: Column<EntityID<TID>>) =
|
||||
InnerTableLink(sourceColumn.table, this@via,sourceColumn, targetColumn)
|
||||
|
||||
/**
|
||||
* Delete this entity.
|
||||
*
|
||||
* This will remove the entity from the database as well as the cache.
|
||||
*/
|
||||
open fun delete(){
|
||||
klass.removeFromCache(this)
|
||||
val table = klass.table
|
||||
table.deleteWhere {table.id eq id}
|
||||
EntityHook.registerChange(TransactionManager.current(), EntityChange(klass, id, EntityChangeType.Removed))
|
||||
}
|
||||
|
||||
open fun flush(batch: EntityBatchUpdate? = null): Boolean {
|
||||
if (writeValues.isNotEmpty()) {
|
||||
if (batch == null) {
|
||||
val table = klass.table
|
||||
// Store values before update to prevent flush inside UpdateStatement
|
||||
val _writeValues = writeValues.toMap()
|
||||
storeWrittenValues()
|
||||
table.update({table.id eq id}) {
|
||||
for ((c, v) in _writeValues) {
|
||||
it[c] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
batch.addBatch(id)
|
||||
for ((c, v) in writeValues) {
|
||||
batch[c] = v
|
||||
}
|
||||
storeWrittenValues()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun storeWrittenValues() {
|
||||
// move write values to read values
|
||||
if (_readValues != null) {
|
||||
for ((c, v) in writeValues) {
|
||||
_readValues!![c] = v
|
||||
}
|
||||
if (klass.dependsOnColumns.any { it.table == klass.table && !_readValues!!.hasValue(it) } ) {
|
||||
_readValues = null
|
||||
}
|
||||
}
|
||||
// clear write values
|
||||
writeValues.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class EntityCache(private val transaction: Transaction) {
|
||||
val data = LinkedHashMap<IdTable<*>, MutableMap<Any, Entity<*>>>()
|
||||
val inserts = LinkedHashMap<IdTable<*>, MutableList<Entity<*>>>()
|
||||
val referrers = HashMap<EntityID<*>, MutableMap<Column<*>, SizedIterable<*>>>()
|
||||
|
||||
private fun getMap(f: EntityClass<*, *>) : MutableMap<Any, Entity<*>> = getMap(f.table)
|
||||
|
||||
private fun getMap(table: IdTable<*>) : MutableMap<Any, Entity<*>> = data.getOrPut(table) {
|
||||
LinkedHashMap()
|
||||
}
|
||||
|
||||
fun <ID: Any, R: Entity<ID>> getOrPutReferrers(sourceId: EntityID<*>, key: Column<*>, refs: ()-> SizedIterable<R>): SizedIterable<R> =
|
||||
referrers.getOrPut(sourceId){HashMap()}.getOrPut(key) {LazySizedCollection(refs())} as SizedIterable<R>
|
||||
|
||||
fun <ID:Comparable<ID>, T: Entity<ID>> find(f: EntityClass<ID, T>, id: EntityID<ID>): T? = getMap(f)[id.value] as T? ?: inserts[f.table]?.firstOrNull { it.id == id } as? T
|
||||
|
||||
fun <ID:Comparable<ID>, T: Entity<ID>> findAll(f: EntityClass<ID, T>): Collection<T> = getMap(f).values as Collection<T>
|
||||
|
||||
fun <ID:Comparable<ID>, T: Entity<ID>> store(f: EntityClass<ID, T>, o: T) {
|
||||
getMap(f)[o.id.value] = o
|
||||
}
|
||||
|
||||
fun store(o: Entity<*>) {
|
||||
getMap(o.klass.table)[o.id.value] = o
|
||||
}
|
||||
|
||||
fun <ID:Comparable<ID>, T: Entity<ID>> remove(table: IdTable<ID>, o: T) {
|
||||
getMap(table).remove(o.id.value)
|
||||
}
|
||||
|
||||
fun <ID:Comparable<ID>, T: Entity<ID>> scheduleInsert(f: EntityClass<ID, T>, o: T) {
|
||||
inserts.getOrPut(f.table) { arrayListOf() }.add(o as Entity<*>)
|
||||
}
|
||||
|
||||
fun flush() {
|
||||
flush(inserts.keys + data.keys)
|
||||
}
|
||||
|
||||
fun flush(tables: Iterable<IdTable<*>>) {
|
||||
val insertedTables = inserts.keys
|
||||
|
||||
SchemaUtils.sortTablesByReferences(tables).filterIsInstance<IdTable<*>>().forEach(::flushInserts)
|
||||
|
||||
for (t in tables) {
|
||||
data[t]?.let { map ->
|
||||
if (map.isNotEmpty()) {
|
||||
val updatedEntities = HashSet<Entity<*>>()
|
||||
val batch = EntityBatchUpdate(map.values.first().klass)
|
||||
for ((_, entity) in map) {
|
||||
if (entity.flush(batch)) {
|
||||
check(entity.klass !is ImmutableEntityClass<*,*>) { "Update on immutable entity ${entity.javaClass.simpleName} ${entity.id}" }
|
||||
updatedEntities.add(entity)
|
||||
}
|
||||
}
|
||||
batch.execute(transaction)
|
||||
updatedEntities.forEach {
|
||||
EntityHook.registerChange(transaction, EntityChange(it.klass, it.id, EntityChangeType.Updated))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (insertedTables.isNotEmpty()) {
|
||||
removeTablesReferrers(insertedTables)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun removeTablesReferrers(insertedTables: Collection<Table>) {
|
||||
referrers.filterValues { it.any { it.key.table in insertedTables } }.map { it.key }.forEach {
|
||||
referrers.remove(it)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun flushInserts(table: IdTable<*>) {
|
||||
inserts.remove(table)?.let {
|
||||
var toFlush: List<Entity<*>> = it
|
||||
do {
|
||||
val partition = toFlush.partition {
|
||||
it.writeValues.none {
|
||||
val (key, value) = it
|
||||
key.referee == table.id && value is EntityID<*> && value._value == null
|
||||
}
|
||||
}
|
||||
toFlush = partition.first
|
||||
val ids = table.batchInsert(toFlush) { entry ->
|
||||
for ((c, v) in entry.writeValues) {
|
||||
this[c] = v
|
||||
}
|
||||
}
|
||||
|
||||
for ((entry, genValues) in toFlush.zip(ids)) {
|
||||
if (entry.id._value == null) {
|
||||
val id = genValues[table.id]
|
||||
entry.id._value = id._value
|
||||
entry.writeValues[entry.klass.table.id as Column<Any?>] = id
|
||||
}
|
||||
genValues.fieldIndex.keys.forEach { key ->
|
||||
entry.writeValues[key as Column<Any?>] = genValues[key]
|
||||
}
|
||||
|
||||
entry.storeWrittenValues()
|
||||
store(entry)
|
||||
EntityHook.registerChange(transaction, EntityChange(entry.klass, entry.id, EntityChangeType.Created))
|
||||
}
|
||||
toFlush = partition.second
|
||||
} while(toFlush.isNotEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
fun clearReferrersCache() {
|
||||
referrers.clear()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun invalidateGlobalCaches(created: List<Entity<*>>) {
|
||||
created.asSequence().mapNotNull { it.klass as? ImmutableCachedEntityClass<*,*>}.distinct().forEach {
|
||||
it.expireCache()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
abstract class EntityClass<ID : Comparable<ID>, out T: Entity<ID>>(val table: IdTable<ID>, entityType: Class<T>? = null) {
|
||||
internal val klass: Class<*> = entityType ?: javaClass.enclosingClass as Class<T>
|
||||
private val ctor = klass.kotlin.primaryConstructor!!
|
||||
|
||||
operator fun get(id: EntityID<ID>): T = findById(id) ?: throw EntityNotFoundException(id, this)
|
||||
|
||||
operator fun get(id: ID): T = get(EntityID(id, table))
|
||||
|
||||
protected open fun warmCache(): EntityCache = TransactionManager.current().entityCache
|
||||
|
||||
/**
|
||||
* Get an entity by its [id].
|
||||
*
|
||||
* @param id The id of the entity
|
||||
*
|
||||
* @return The entity that has this id or null if no entity was found.
|
||||
*/
|
||||
fun findById(id: ID): T? = findById(EntityID(id, table))
|
||||
|
||||
/**
|
||||
* Get an entity by its [id].
|
||||
*
|
||||
* @param id The id of the entity
|
||||
*
|
||||
* @return The entity that has this id or null if no entity was found.
|
||||
*/
|
||||
open fun findById(id: EntityID<ID>): T? = testCache(id) ?: find{table.id eq id}.firstOrNull()
|
||||
|
||||
/**
|
||||
* Reloads entity fields from database as new object.
|
||||
* @param flush whether pending entity changes should be flushed previously
|
||||
*/
|
||||
fun reload(entity: Entity<ID>, flush: Boolean = false): T? {
|
||||
if (flush) entity.flush()
|
||||
removeFromCache(entity)
|
||||
return findById(entity.id)
|
||||
}
|
||||
|
||||
internal open fun invalidateEntityInCache(o: Entity<ID>) {
|
||||
if (o.id._value != null && testCache(o.id) == null && TransactionManager.current().db == o.db) {
|
||||
get(o.id) // Check that entity is still exists in database
|
||||
warmCache().store(o)
|
||||
}
|
||||
}
|
||||
|
||||
fun testCache(id: EntityID<ID>): T? = warmCache().find(this, id)
|
||||
|
||||
fun testCache(cacheCheckCondition: T.()->Boolean): Sequence<T> = warmCache().findAll(this).asSequence().filter { it.cacheCheckCondition() }
|
||||
|
||||
fun removeFromCache(entity: Entity<ID>) {
|
||||
val cache = warmCache()
|
||||
cache.remove(table, entity)
|
||||
cache.referrers.remove(entity.id)
|
||||
cache.removeTablesReferrers(listOf(table))
|
||||
}
|
||||
|
||||
open fun forEntityIds(ids: List<EntityID<ID>>) : SizedIterable<T> {
|
||||
val distinctIds = ids.distinct()
|
||||
if (distinctIds.isEmpty()) return emptySized()
|
||||
|
||||
val cached = distinctIds.mapNotNull { testCache(it) }
|
||||
|
||||
if (cached.size == distinctIds.size) {
|
||||
return SizedCollection(cached)
|
||||
}
|
||||
|
||||
return wrapRows(searchQuery(Op.build { table.id inList distinctIds }))
|
||||
}
|
||||
|
||||
fun forIds(ids: List<ID>) : SizedIterable<T> = forEntityIds(ids.map {EntityID (it, table)})
|
||||
|
||||
fun wrapRows(rows: SizedIterable<ResultRow>): SizedIterable<T> = rows mapLazy {
|
||||
wrapRow(it)
|
||||
}
|
||||
|
||||
fun wrapRows(rows: SizedIterable<ResultRow>, alias: Alias<IdTable<*>>) = rows mapLazy {
|
||||
wrapRow(it, alias)
|
||||
}
|
||||
|
||||
fun wrapRows(rows: SizedIterable<ResultRow>, alias: QueryAlias) = rows mapLazy {
|
||||
wrapRow(it, alias)
|
||||
}
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
fun wrapRow(row: ResultRow) : T {
|
||||
val entity = wrap(row[table.id], row)
|
||||
if (entity._readValues == null)
|
||||
entity._readValues = row
|
||||
|
||||
return entity
|
||||
}
|
||||
|
||||
fun wrapRow(row: ResultRow, alias: Alias<IdTable<*>>) : T {
|
||||
require(alias.delegate == table) { "Alias for a wrong table ${alias.delegate.tableName} while ${table.tableName} expected"}
|
||||
val newFieldsMapping = row.fieldIndex.mapNotNull { (exp, _) ->
|
||||
val column = exp as? Column<*>
|
||||
val value = row[exp]
|
||||
val originalColumn = column?.let { alias.originalColumn(it) }
|
||||
when {
|
||||
originalColumn != null -> originalColumn to value
|
||||
column?.table == alias.delegate -> null
|
||||
else -> exp to value
|
||||
}
|
||||
}.toMap()
|
||||
return wrapRow(ResultRow.createAndFillValues(newFieldsMapping))
|
||||
}
|
||||
|
||||
fun wrapRow(row: ResultRow, alias: QueryAlias) : T {
|
||||
require(alias.columns.any { (it.table as Alias<*>).delegate == table }) { "QueryAlias doesn't have any column from ${table.tableName} table"}
|
||||
val originalColumns = alias.query.set.source.columns
|
||||
val newFieldsMapping = row.fieldIndex.mapNotNull { (exp, _) ->
|
||||
val value = row[exp]
|
||||
when {
|
||||
exp is Column && exp.table is Alias<*> -> {
|
||||
val column = originalColumns.single { exp.table.delegate == it.table && exp.name == it.name }
|
||||
column to value
|
||||
}
|
||||
exp is Column && exp.table == table -> null
|
||||
else -> exp to value
|
||||
}
|
||||
}.toMap()
|
||||
return wrapRow(ResultRow.createAndFillValues(newFieldsMapping))
|
||||
}
|
||||
|
||||
open fun all(): SizedIterable<T> = wrapRows(table.selectAll().notForUpdate())
|
||||
|
||||
/**
|
||||
* Get all the entities that conform to the [op] statement.
|
||||
*
|
||||
* @param op The statement to select the entities for. The statement must be of boolean type.
|
||||
*
|
||||
* @return All the entities that conform to the [op] statement.
|
||||
*/
|
||||
fun find(op: Op<Boolean>): SizedIterable<T> {
|
||||
warmCache()
|
||||
return wrapRows(searchQuery(op))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the entities that conform to the [op] statement.
|
||||
*
|
||||
* @param op The statement to select the entities for. The statement must be of boolean type.
|
||||
*
|
||||
* @return All the entities that conform to the [op] statement.
|
||||
*/
|
||||
fun find(op: SqlExpressionBuilder.()->Op<Boolean>): SizedIterable<T> = find(SqlExpressionBuilder.op())
|
||||
|
||||
fun findWithCacheCondition(cacheCheckCondition: T.()->Boolean, op: SqlExpressionBuilder.()->Op<Boolean>): Sequence<T> {
|
||||
val cached = testCache(cacheCheckCondition)
|
||||
return if (cached.any()) cached else find(op).asSequence()
|
||||
}
|
||||
|
||||
open val dependsOnTables: ColumnSet get() = table
|
||||
open val dependsOnColumns: List<Column<out Any?>> get() = dependsOnTables.columns
|
||||
|
||||
open fun searchQuery(op: Op<Boolean>): Query =
|
||||
dependsOnTables.slice(dependsOnColumns).select { op }.setForUpdateStatus()
|
||||
|
||||
/**
|
||||
* Count the amount of entities that conform to the [op] statement.
|
||||
*
|
||||
* @param op The statement to count the entities for. The statement must be of boolean type.
|
||||
*
|
||||
* @return The amount of entities that conform to the [op] statement.
|
||||
*/
|
||||
fun count(op: Op<Boolean>? = null): Int = with(TransactionManager.current()) {
|
||||
val query = table.slice(table.id.count())
|
||||
(if (op == null) query.selectAll() else query.select{op}).notForUpdate().first()[
|
||||
table.id.count()
|
||||
]
|
||||
}
|
||||
|
||||
protected open fun createInstance(entityId: EntityID<ID>, row: ResultRow?) : T = ctor.call(entityId) as T
|
||||
|
||||
fun wrap(id: EntityID<ID>, row: ResultRow?): T {
|
||||
val transaction = TransactionManager.current()
|
||||
return transaction.entityCache.find(this, id) ?: createInstance(id, row).also { new ->
|
||||
new.klass = this
|
||||
new.db = transaction.db
|
||||
warmCache().store(this, new)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new entity with the fields that are set in the [init] block. The id will be automatically set.
|
||||
*
|
||||
* @param init The block where the entities' fields can be set.
|
||||
*
|
||||
* @return The entity that has been created.
|
||||
*/
|
||||
open fun new(init: T.() -> Unit) = new(null, init)
|
||||
|
||||
/**
|
||||
* Create a new entity with the fields that are set in the [init] block and with a set [id].
|
||||
*
|
||||
* @param id The id of the entity. Set this to null if it should be automatically generated.
|
||||
* @param init The block where the entities' fields can be set.
|
||||
*
|
||||
* @return The entity that has been created.
|
||||
*/
|
||||
open fun new(id: ID?, init: T.() -> Unit): T {
|
||||
val entityId = if (id == null && table.id.defaultValueFun != null)
|
||||
table.id.defaultValueFun!!()
|
||||
else
|
||||
EntityID(id, table)
|
||||
val prototype: T = createInstance(entityId, null)
|
||||
prototype.klass = this
|
||||
prototype.db = TransactionManager.current().db
|
||||
prototype._readValues = ResultRow.createAndFillDefaults(dependsOnColumns)
|
||||
if (entityId._value != null) {
|
||||
prototype.writeValues[table.id as Column<Any?>] = entityId
|
||||
warmCache().scheduleInsert(this, prototype)
|
||||
}
|
||||
prototype.init()
|
||||
if (entityId._value == null) {
|
||||
val readValues = prototype._readValues!!
|
||||
val writeValues = prototype.writeValues
|
||||
table.columns.filter { col ->
|
||||
col.defaultValueFun != null && col !in writeValues && readValues.hasValue(col)
|
||||
}.forEach { col ->
|
||||
writeValues[col as Column<Any?>] = readValues[col]
|
||||
}
|
||||
warmCache().scheduleInsert(this, prototype)
|
||||
}
|
||||
return prototype
|
||||
}
|
||||
|
||||
inline fun view (op: SqlExpressionBuilder.() -> Op<Boolean>) = View(SqlExpressionBuilder.op(), this)
|
||||
|
||||
private val refDefinitions = HashMap<Pair<Column<*>, KClass<*>>, Any>()
|
||||
|
||||
private inline fun <reified R: Any> registerRefRule(column: Column<*>, ref:()-> R): R =
|
||||
refDefinitions.getOrPut(column to R::class, ref) as R
|
||||
|
||||
infix fun <REF:Comparable<REF>> referencedOn(column: Column<REF>) = registerRefRule(column) { Reference(column, this) }
|
||||
|
||||
infix fun <REF:Comparable<REF>> optionalReferencedOn(column: Column<REF?>) = registerRefRule(column) { OptionalReference(column, this) }
|
||||
|
||||
infix fun <TargetID: Comparable<TargetID>, Target:Entity<TargetID>, REF:Comparable<REF>> EntityClass<TargetID, Target>.backReferencedOn(column: Column<REF>)
|
||||
: ReadOnlyProperty<Entity<ID>, Target> = registerRefRule(column) { BackReference(column, this) }
|
||||
|
||||
@JvmName("backReferencedOnOpt")
|
||||
infix fun <TargetID: Comparable<TargetID>, Target:Entity<TargetID>, REF:Comparable<REF>> EntityClass<TargetID, Target>.backReferencedOn(column: Column<REF?>)
|
||||
: ReadOnlyProperty<Entity<ID>, Target> = registerRefRule(column) { BackReference(column, this) }
|
||||
|
||||
infix fun <TargetID: Comparable<TargetID>, Target:Entity<TargetID>, REF:Comparable<REF>> EntityClass<TargetID, Target>.optionalBackReferencedOn(column: Column<REF>)
|
||||
= registerRefRule(column) { OptionalBackReference<TargetID, Target, ID, Entity<ID>, REF>(column as Column<REF?>, this) }
|
||||
|
||||
@JvmName("optionalBackReferencedOnOpt")
|
||||
infix fun <TargetID: Comparable<TargetID>, Target:Entity<TargetID>, REF:Comparable<REF>> EntityClass<TargetID, Target>.optionalBackReferencedOn(column: Column<REF?>)
|
||||
= registerRefRule(column) { OptionalBackReference<TargetID, Target, ID, Entity<ID>, REF>(column, this) }
|
||||
|
||||
infix fun <TargetID: Comparable<TargetID>, Target:Entity<TargetID>, REF: Comparable<REF>> EntityClass<TargetID, Target>.referrersOn(column: Column<REF>)
|
||||
= registerRefRule(column) { Referrers<ID, Entity<ID>, TargetID, Target, REF>(column, this, false) }
|
||||
|
||||
fun <TargetID: Comparable<TargetID>, Target:Entity<TargetID>, REF: Comparable<REF>> EntityClass<TargetID, Target>.referrersOn(column: Column<REF>, cache: Boolean)
|
||||
= registerRefRule(column) { Referrers<ID, Entity<ID>, TargetID, Target, REF>(column, this, cache) }
|
||||
|
||||
infix fun <TargetID: Comparable<TargetID>, Target:Entity<TargetID>, REF: Comparable<REF>> EntityClass<TargetID, Target>.optionalReferrersOn(column : Column<REF?>)
|
||||
= registerRefRule(column) { OptionalReferrers<ID, Entity<ID>, TargetID, Target, REF>(column, this, false) }
|
||||
|
||||
fun <TargetID: Comparable<TargetID>, Target:Entity<TargetID>, REF: Comparable<REF>> EntityClass<TargetID, Target>.optionalReferrersOn(column: Column<REF?>, cache: Boolean = false) =
|
||||
registerRefRule(column) { OptionalReferrers<ID, Entity<ID>, TargetID, Target, REF>(column, this, cache) }
|
||||
|
||||
fun<TColumn: Any?,TReal: Any?> Column<TColumn>.transform(toColumn: (TReal) -> TColumn, toReal: (TColumn) -> TReal): ColumnWithTransform<TColumn, TReal> = ColumnWithTransform(this, toColumn, toReal)
|
||||
|
||||
private fun Query.setForUpdateStatus(): Query = if (this@EntityClass is ImmutableEntityClass<*,*>) this.notForUpdate() else this
|
||||
|
||||
@Suppress("CAST_NEVER_SUCCEEDS")
|
||||
fun <SID> warmUpOptReferences(references: List<SID>, refColumn: Column<SID?>, forUpdate: Boolean? = null): List<T>
|
||||
= warmUpReferences(references, refColumn as Column<SID>, forUpdate)
|
||||
|
||||
fun <SID> warmUpReferences(references: List<SID>, refColumn: Column<SID>, forUpdate: Boolean? = null): List<T> {
|
||||
val parentTable = refColumn.referee?.table as? IdTable<*>
|
||||
requireNotNull(parentTable) { "RefColumn should have reference to IdTable" }
|
||||
if (references.isEmpty()) return emptyList()
|
||||
val distinctRefIds = references.distinct()
|
||||
val cache = TransactionManager.current().entityCache
|
||||
if (refColumn.columnType is EntityIDColumnType<*>) {
|
||||
refColumn as Column<EntityID<*>>
|
||||
distinctRefIds as List<EntityID<ID>>
|
||||
val toLoad = distinctRefIds.filter {
|
||||
cache.referrers[it]?.containsKey(refColumn)?.not() ?: true
|
||||
}
|
||||
if (toLoad.isNotEmpty()) {
|
||||
val findQuery = find { refColumn inList toLoad }
|
||||
val entities = when(forUpdate) {
|
||||
true -> findQuery.forUpdate()
|
||||
false -> findQuery.notForUpdate()
|
||||
else -> findQuery
|
||||
}.toList()
|
||||
|
||||
val result = entities.groupBy { it.readValues[refColumn] }
|
||||
|
||||
distinctRefIds.forEach { id ->
|
||||
cache.getOrPutReferrers(id, refColumn) { result[id]?.let { SizedCollection(it) } ?: emptySized<T>() }
|
||||
}
|
||||
}
|
||||
|
||||
return distinctRefIds.flatMap { cache.referrers[it]?.get(refColumn)?.toList().orEmpty() } as List<T>
|
||||
} else {
|
||||
val baseQuery = searchQuery(Op.build{ refColumn inList distinctRefIds })
|
||||
val finalQuery = if (parentTable.id in baseQuery.set.fields)
|
||||
baseQuery
|
||||
else {
|
||||
baseQuery.adjustSlice{ slice(this.fields + parentTable.id) }.
|
||||
adjustColumnSet { innerJoin(parentTable, { refColumn }, { refColumn.referee!! }) }
|
||||
}
|
||||
|
||||
val findQuery = wrapRows(finalQuery)
|
||||
val entities = when(forUpdate) {
|
||||
true -> findQuery.forUpdate()
|
||||
false -> findQuery.notForUpdate()
|
||||
else -> findQuery
|
||||
}.toList().distinct()
|
||||
|
||||
entities.groupBy { it.readValues[parentTable.id] }.forEach { (id, values) ->
|
||||
cache.getOrPutReferrers(id, refColumn) { SizedCollection(values) }
|
||||
}
|
||||
return entities
|
||||
}
|
||||
}
|
||||
|
||||
fun warmUpLinkedReferences(references: List<EntityID<*>>, linkTable: Table, forUpdate: Boolean? = null): List<T> {
|
||||
if (references.isEmpty()) return emptyList()
|
||||
val distinctRefIds = references.distinct()
|
||||
val sourceRefColumn = linkTable.columns.singleOrNull { it.referee == references.first().table.id } as? Column<EntityID<*>> ?: error("Can't detect source reference column")
|
||||
val targetRefColumn = linkTable.columns.singleOrNull {it.referee == table.id} as? Column<EntityID<*>>?: error("Can't detect target reference column")
|
||||
|
||||
val transaction = TransactionManager.current()
|
||||
|
||||
val inCache = transaction.entityCache.referrers.filter { it.key in distinctRefIds && sourceRefColumn in it.value }.mapValues { it.value[sourceRefColumn]!! }
|
||||
val loaded = (distinctRefIds - inCache.keys).takeIf { it.isNotEmpty() }?.let { idsToLoad ->
|
||||
val alreadyInJoin = (dependsOnTables as? Join)?.alreadyInJoin(linkTable) ?: false
|
||||
val entityTables = if (alreadyInJoin) dependsOnTables else dependsOnTables.join(linkTable, JoinType.INNER, targetRefColumn, table.id)
|
||||
|
||||
val columns = (dependsOnColumns + (if (!alreadyInJoin) linkTable.columns else emptyList())
|
||||
- sourceRefColumn).distinct() + sourceRefColumn
|
||||
|
||||
val query = entityTables.slice(columns).select { sourceRefColumn inList idsToLoad }
|
||||
val entitiesWithRefs = when(forUpdate) {
|
||||
true -> query.forUpdate()
|
||||
false -> query.notForUpdate()
|
||||
else -> query
|
||||
}.map { it[sourceRefColumn] to wrapRow(it) }
|
||||
|
||||
val groupedBySourceId = entitiesWithRefs.groupBy { it.first }.mapValues { it.value.map { it.second } }
|
||||
|
||||
idsToLoad.forEach {
|
||||
transaction.entityCache.getOrPutReferrers(it, sourceRefColumn) { SizedCollection(groupedBySourceId[it] ?: emptyList()) }
|
||||
}
|
||||
entitiesWithRefs.map { it.second }
|
||||
}
|
||||
return inCache.values.flatMap { it.toList() as List<T> } + loaded.orEmpty()
|
||||
}
|
||||
|
||||
fun <ID : Comparable<ID>, T: Entity<ID>> isAssignableTo(entityClass: EntityClass<ID, T>) = entityClass.klass.isAssignableFrom(klass)
|
||||
}
|
||||
|
||||
abstract class ImmutableEntityClass<ID:Comparable<ID>, out T: Entity<ID>>(table: IdTable<ID>, entityType: Class<T>? = null) : EntityClass<ID, T>(table, entityType) {
|
||||
open fun <T> forceUpdateEntity(entity: Entity<ID>, column: Column<T>, value: T) {
|
||||
table.update({ table.id eq entity.id }) {
|
||||
it[column] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ImmutableCachedEntityClass<ID:Comparable<ID>, out T: Entity<ID>>(table: IdTable<ID>, entityType: Class<T>? = null) : ImmutableEntityClass<ID, T>(table, entityType) {
|
||||
|
||||
private val cacheLoadingState = Key<Any>()
|
||||
private var _cachedValues: MutableMap<Database, MutableMap<Any, Entity<*>>> = ConcurrentHashMap()
|
||||
|
||||
override fun invalidateEntityInCache(o: Entity<ID>) {
|
||||
warmCache()
|
||||
}
|
||||
|
||||
final override fun warmCache(): EntityCache {
|
||||
val tr = TransactionManager.current()
|
||||
val db = tr.db
|
||||
val transactionCache = super.warmCache()
|
||||
if (_cachedValues[db] == null) synchronized(this) {
|
||||
val cachedValues = _cachedValues[db]
|
||||
when {
|
||||
cachedValues != null -> {} // already loaded in another transaction
|
||||
tr.getUserData(cacheLoadingState) != null -> {
|
||||
return transactionCache // prevent recursive call to warmCache() in .all()
|
||||
}
|
||||
else -> {
|
||||
tr.putUserData(cacheLoadingState, this)
|
||||
super.all().toList() /* force iteration to initialize lazy collection */
|
||||
_cachedValues[db] = transactionCache.data[table] ?: mutableMapOf()
|
||||
tr.removeUserData(cacheLoadingState)
|
||||
}
|
||||
}
|
||||
}
|
||||
transactionCache.data[table] = _cachedValues[db]!!
|
||||
return transactionCache
|
||||
}
|
||||
|
||||
override fun all(): SizedIterable<T> = SizedCollection(warmCache().findAll(this))
|
||||
|
||||
@Synchronized fun expireCache() {
|
||||
if (TransactionManager.isInitialized() && TransactionManager.currentOrNull() != null) {
|
||||
_cachedValues.remove(TransactionManager.current().db)
|
||||
} else {
|
||||
_cachedValues.clear()
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T> forceUpdateEntity(entity: Entity<ID>, column: Column<T>, value: T) {
|
||||
super.forceUpdateEntity(entity, column, value)
|
||||
entity._readValues?.set(column, value)
|
||||
expireCache()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.jetbrains.exposed.dao
|
||||
|
||||
abstract class EntityID<T:Comparable<T>>(id: T?, val table: IdTable<T>) : Comparable<EntityID<T>> {
|
||||
var _value: Any? = id
|
||||
val value: T get() {
|
||||
if (_value == null) {
|
||||
invokeOnNoValue()
|
||||
assert(_value != null) { "Entity must be inserted" }
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return _value!! as T
|
||||
}
|
||||
|
||||
protected abstract fun invokeOnNoValue()
|
||||
|
||||
override fun toString() = value.toString()
|
||||
|
||||
override fun hashCode() = value.hashCode()
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is EntityID<*>) return false
|
||||
|
||||
return other._value == _value && other.table == table
|
||||
}
|
||||
|
||||
override fun compareTo(other: EntityID<T>): Int = value.compareTo(other.value)
|
||||
}
|
||||
|
||||
class SimpleEntityID<T:Comparable<T>>(id: T?, table: IdTable<T>) : EntityID<T>(id, table) {
|
||||
override fun invokeOnNoValue() {}
|
||||
}
|
||||
@@ -2,8 +2,37 @@ package org.jetbrains.exposed.dao
|
||||
|
||||
import org.jetbrains.exposed.sql.Column
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
import java.util.*
|
||||
|
||||
abstract class IdTable<T:Comparable<T>>(name: String=""): Table(name) {
|
||||
abstract val id : Column<EntityID<T>>
|
||||
|
||||
interface EntityIDFactory {
|
||||
fun <T:Comparable<T>> createEntityID(value: T, table: IdTable<T>) : EntityID<T>
|
||||
}
|
||||
|
||||
object EntityIDFunctionProvider {
|
||||
var factory : EntityIDFactory = object : EntityIDFactory {
|
||||
override fun <T : Comparable<T>> createEntityID(value: T, table: IdTable<T>): EntityID<T> {
|
||||
return SimpleEntityID(value, table)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T:Comparable<T>> createEntityID(value: T, table: IdTable<T>) = factory.createEntityID(value, table)
|
||||
}
|
||||
|
||||
abstract class IdTable<T:Comparable<T>>(name: String = ""): Table(name) {
|
||||
abstract val id : Column<EntityID<T>>
|
||||
}
|
||||
|
||||
open class IntIdTable(name: String = "", columnName: String = "id") : IdTable<Int>(name) {
|
||||
override val id: Column<EntityID<Int>> = integer(columnName).autoIncrement().primaryKey().entityId()
|
||||
}
|
||||
|
||||
open class LongIdTable(name: String = "", columnName: String = "id") : IdTable<Long>(name) {
|
||||
override val id: Column<EntityID<Long>> = long(columnName).autoIncrement().primaryKey().entityId()
|
||||
}
|
||||
|
||||
open class UUIDTable(name: String = "", columnName: String = "id") : IdTable<UUID>(name) {
|
||||
override val id: Column<EntityID<UUID>> = uuid(columnName).primaryKey()
|
||||
.clientDefault { UUID.randomUUID() }
|
||||
.entityId()
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.jetbrains.exposed.dao
|
||||
|
||||
import org.jetbrains.exposed.sql.Column
|
||||
|
||||
open class IntIdTable(name: String = "", columnName: String = "id") : IdTable<Int>(name) {
|
||||
override val id: Column<EntityID<Int>> = integer(columnName).autoIncrement().primaryKey().entityId()
|
||||
}
|
||||
|
||||
abstract class IntEntity(id: EntityID<Int>) : Entity<Int>(id)
|
||||
|
||||
abstract class IntEntityClass<out E:IntEntity>(table: IdTable<Int>, entityType: Class<E>? = null) : EntityClass<Int, E> (table, entityType)
|
||||
@@ -1,13 +0,0 @@
|
||||
package org.jetbrains.exposed.dao
|
||||
|
||||
import org.jetbrains.exposed.sql.Column
|
||||
|
||||
|
||||
open class LongIdTable(name: String = "", columnName: String = "id") : IdTable<Long>(name) {
|
||||
override val id: Column<EntityID<Long>> = long(columnName).autoIncrement().primaryKey().entityId()
|
||||
}
|
||||
|
||||
abstract class LongEntity(id: EntityID<Long>) : Entity<Long>(id)
|
||||
|
||||
abstract class LongEntityClass<out E:LongEntity>(table: IdTable<Long>, entityType: Class<E>? = null) : EntityClass<Long, E> (table, entityType)
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package org.jetbrains.exposed.dao
|
||||
|
||||
import org.jetbrains.exposed.sql.Column
|
||||
import java.util.*
|
||||
|
||||
|
||||
open class UUIDTable(name: String = "", columnName: String = "id") : IdTable<UUID>(name) {
|
||||
override val id: Column<EntityID<UUID>> = uuid(columnName).primaryKey()
|
||||
.clientDefault { UUID.randomUUID() }
|
||||
.entityId()
|
||||
}
|
||||
|
||||
abstract class UUIDEntity(id: EntityID<UUID>) : Entity<UUID>(id)
|
||||
|
||||
abstract class UUIDEntityClass<out E: UUIDEntity>(table: IdTable<UUID>, entityType: Class<E>? = null) : EntityClass<UUID, E> (table, entityType)
|
||||
|
||||
@@ -9,7 +9,7 @@ class Alias<out T:Table>(val delegate: T, val alias: String) : Table() {
|
||||
|
||||
private fun <T:Any?> Column<T>.clone() = Column<T>(this@Alias, name, columnType)
|
||||
|
||||
internal fun <R> originalColumn(column: Column<R>) : Column<R>? {
|
||||
fun <R> originalColumn(column: Column<R>) : Column<R>? {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return if (column.table == this)
|
||||
delegate.columns.first { column.name == it.name } as Column<R>
|
||||
|
||||
@@ -15,8 +15,8 @@ class Column<T>(val table: Table, val name: String, override val columnType: ICo
|
||||
get() = field ?: currentDialectIfAvailable?.defaultReferenceOption
|
||||
internal var onDelete: ReferenceOption? = null
|
||||
get() = field ?: currentDialectIfAvailable?.defaultReferenceOption
|
||||
internal var indexInPK: Int? = null
|
||||
internal var defaultValueFun: (() -> T)? = null
|
||||
var indexInPK: Int? = null
|
||||
var defaultValueFun: (() -> T)? = null
|
||||
internal var dbDefaultValue: Expression<T>? = null
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.jetbrains.exposed.sql
|
||||
|
||||
import org.jetbrains.exposed.dao.EntityID
|
||||
import org.jetbrains.exposed.dao.EntityIDFunctionProvider
|
||||
import org.jetbrains.exposed.dao.IdTable
|
||||
import org.jetbrains.exposed.sql.statements.DefaultValueMarker
|
||||
import org.jetbrains.exposed.sql.statements.api.ExposedBlob
|
||||
@@ -96,8 +97,8 @@ class EntityIDColumnType<T:Comparable<T>>(val idColumn: Column<T>) : ColumnType(
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun valueFromDB(value: Any): Any = when (value) {
|
||||
is EntityID<*> -> EntityID(value.value as T, idColumn.table as IdTable<T>)
|
||||
else -> EntityID(idColumn.columnType.valueFromDB(value) as T, idColumn.table as IdTable<T>)
|
||||
is EntityID<*> -> EntityIDFunctionProvider.createEntityID(value.value as T, idColumn.table as IdTable<T>)
|
||||
else -> EntityIDFunctionProvider.createEntityID(idColumn.columnType.valueFromDB(value) as T, idColumn.table as IdTable<T>)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
@file:Suppress("PackageDirectoryMismatch")
|
||||
package org.jetbrains.exposed.exceptions
|
||||
|
||||
import org.jetbrains.exposed.dao.EntityClass
|
||||
import org.jetbrains.exposed.dao.EntityID
|
||||
import org.jetbrains.exposed.sql.Query
|
||||
import org.jetbrains.exposed.sql.QueryBuilder
|
||||
import org.jetbrains.exposed.sql.Transaction
|
||||
@@ -11,8 +9,6 @@ import org.jetbrains.exposed.sql.statements.expandArgs
|
||||
import org.jetbrains.exposed.sql.vendors.DatabaseDialect
|
||||
import java.sql.SQLException
|
||||
|
||||
class EntityNotFoundException(val id: EntityID<*>, val entity: EntityClass<*, *>): Exception("Entity ${entity.klass.simpleName}, id=$id not found in database")
|
||||
|
||||
class ExposedSQLException(cause: Throwable?, val contexts: List<StatementContext>, private val transaction: Transaction) : SQLException(cause) {
|
||||
fun causedByQueries() : List<String> = contexts.map {
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.jetbrains.exposed.sql
|
||||
|
||||
import org.jetbrains.exposed.dao.IdTable
|
||||
import org.jetbrains.exposed.sql.statements.Statement
|
||||
import org.jetbrains.exposed.sql.statements.StatementType
|
||||
import org.jetbrains.exposed.sql.statements.api.PreparedStatementApi
|
||||
@@ -9,87 +8,6 @@ import org.jetbrains.exposed.sql.vendors.currentDialect
|
||||
import java.sql.ResultSet
|
||||
import java.util.*
|
||||
|
||||
class ResultRow(internal val fieldIndex: Map<Expression<*>, Int>) {
|
||||
private val data = arrayOfNulls<Any?>(fieldIndex.size)
|
||||
|
||||
/**
|
||||
* Retrieves value of a given expression on this row.
|
||||
*
|
||||
* @param c expression to evaluate
|
||||
* @throws IllegalStateException if expression is not in record set or if result value is uninitialized
|
||||
*
|
||||
* @see [getOrNull] to get null in the cases an exception would be thrown
|
||||
*/
|
||||
operator fun <T> get(c: Expression<T>): T {
|
||||
val d = getRaw(c)
|
||||
|
||||
if (d == null && c is Column<*> && c.dbDefaultValue != null && !c.columnType.nullable) {
|
||||
exposedLogger.warn("Column ${TransactionManager.current().fullIdentity(c)} is marked as not null, " +
|
||||
"has default db value, but returns null. Possible have to re-read it from DB.")
|
||||
}
|
||||
|
||||
return rawToColumnValue(d, c)
|
||||
}
|
||||
|
||||
operator fun <T> set(c: Expression<out T>, value: T) {
|
||||
val index = fieldIndex[c] ?: error("${c.toQueryBuilder(QueryBuilder(false))} is not in record set")
|
||||
data[index] = value
|
||||
}
|
||||
|
||||
fun <T> hasValue(c: Expression<T>): Boolean = fieldIndex[c]?.let{ data[it] != NotInitializedValue } ?: false
|
||||
|
||||
fun <T> getOrNull(c: Expression<T>): T? = if (hasValue(c)) rawToColumnValue(getRaw(c), c) else null
|
||||
|
||||
@Deprecated("Replaced with getOrNull to be more kotlinish", replaceWith = ReplaceWith("getOrNull(c)"))
|
||||
fun <T> tryGet(c: Expression<T>): T? = getOrNull(c)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T> rawToColumnValue(raw: T?, c: Expression<T>): T {
|
||||
return when {
|
||||
raw == null -> null
|
||||
raw == NotInitializedValue -> error("${c.toQueryBuilder(QueryBuilder(false))} is not initialized yet")
|
||||
c is ExpressionAlias<T> && c.delegate is ExpressionWithColumnType<T> -> c.delegate.columnType.valueFromDB(raw)
|
||||
c is ExpressionWithColumnType<T> -> c.columnType.valueFromDB(raw)
|
||||
else -> raw
|
||||
} as T
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T> getRaw(c: Expression<T>): T? =
|
||||
data[fieldIndex[c] ?: error("${c.toQueryBuilder(QueryBuilder(false))} is not in record set")] as T?
|
||||
|
||||
override fun toString(): String =
|
||||
fieldIndex.entries.joinToString { "${it.key.toQueryBuilder(QueryBuilder(false))}=${data[it.value]}" }
|
||||
|
||||
internal object NotInitializedValue
|
||||
|
||||
companion object {
|
||||
fun create(rs: ResultSet, fields: List<Expression<*>>): ResultRow {
|
||||
val fieldsIndex = fields.distinct().mapIndexed { i, field ->
|
||||
val value = (field as? Column<*>)?.columnType?.readObject(rs, i + 1) ?: rs.getObject(i + 1)
|
||||
(field to i) to value
|
||||
}.toMap()
|
||||
return ResultRow(fieldsIndex.keys.toMap()).apply {
|
||||
fieldsIndex.forEach{ (i, f) ->
|
||||
data[i.second] = f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun createAndFillValues(data: Map<Expression<*>, Any?>) : ResultRow =
|
||||
ResultRow(data.keys.mapIndexed { i, c -> c to i }.toMap()).also { row ->
|
||||
data.forEach { (c, v) -> row[c] = v }
|
||||
}
|
||||
|
||||
internal fun createAndFillDefaults(columns : List<Column<*>>): ResultRow =
|
||||
ResultRow(columns.mapIndexed { i, c -> c to i }.toMap()).apply {
|
||||
columns.forEach {
|
||||
this[it] = it.defaultValueFun?.invoke() ?: if (!it.columnType.nullable) NotInitializedValue else null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class SortOrder {
|
||||
ASC, DESC
|
||||
}
|
||||
@@ -301,14 +219,8 @@ open class Query(set: FieldSet, where: Op<Boolean>?): SizedIterable<ResultRow>,
|
||||
}
|
||||
}
|
||||
|
||||
private fun flushEntities() {
|
||||
// Flush data before executing query or results may be unpredictable
|
||||
val tables = set.source.columns.map { it.table }.filterIsInstance(IdTable::class.java).toSet()
|
||||
transaction.entityCache.flush(tables)
|
||||
}
|
||||
|
||||
override operator fun iterator(): Iterator<ResultRow> {
|
||||
flushEntities()
|
||||
val resultIterator = ResultIterator(transaction.exec(this)!!)
|
||||
return if (transaction.db.supportsMultipleResultSets)
|
||||
resultIterator
|
||||
@@ -323,8 +235,6 @@ open class Query(set: FieldSet, where: Op<Boolean>?): SizedIterable<ResultRow>,
|
||||
|
||||
private var count: Boolean = false
|
||||
override fun count(): Int {
|
||||
flushEntities()
|
||||
|
||||
return if (distinct || groupedByColumns.isNotEmpty() || limit != null) {
|
||||
fun Column<*>.makeAlias() = alias(transaction.db.identifierManager.quoteIfNecessary("${table.tableName}_$name"))
|
||||
|
||||
@@ -355,8 +265,6 @@ open class Query(set: FieldSet, where: Op<Boolean>?): SizedIterable<ResultRow>,
|
||||
}
|
||||
|
||||
override fun empty(): Boolean {
|
||||
flushEntities()
|
||||
|
||||
val oldLimit = limit
|
||||
try {
|
||||
limit = 1
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package org.jetbrains.exposed.sql
|
||||
|
||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||
import java.sql.ResultSet
|
||||
|
||||
class ResultRow(val fieldIndex: Map<Expression<*>, Int>) {
|
||||
private val data = arrayOfNulls<Any?>(fieldIndex.size)
|
||||
|
||||
/**
|
||||
* Retrieves value of a given expression on this row.
|
||||
*
|
||||
* @param c expression to evaluate
|
||||
* @throws IllegalStateException if expression is not in record set or if result value is uninitialized
|
||||
*
|
||||
* @see [getOrNull] to get null in the cases an exception would be thrown
|
||||
*/
|
||||
operator fun <T> get(c: Expression<T>): T {
|
||||
val d = getRaw(c)
|
||||
|
||||
if (d == null && c is Column<*> && c.dbDefaultValue != null && !c.columnType.nullable) {
|
||||
exposedLogger.warn("Column ${TransactionManager.current().fullIdentity(c)} is marked as not null, " +
|
||||
"has default db value, but returns null. Possible have to re-read it from DB.")
|
||||
}
|
||||
|
||||
return rawToColumnValue(d, c)
|
||||
}
|
||||
|
||||
operator fun <T> set(c: Expression<out T>, value: T) {
|
||||
val index = fieldIndex[c] ?: error("${c.toQueryBuilder(QueryBuilder(false))} is not in record set")
|
||||
data[index] = value
|
||||
}
|
||||
|
||||
fun <T> hasValue(c: Expression<T>): Boolean = fieldIndex[c]?.let{ data[it] != NotInitializedValue } ?: false
|
||||
|
||||
fun <T> getOrNull(c: Expression<T>): T? = if (hasValue(c)) rawToColumnValue(getRaw(c), c) else null
|
||||
|
||||
@Deprecated("Replaced with getOrNull to be more kotlinish", replaceWith = ReplaceWith("getOrNull(c)"))
|
||||
fun <T> tryGet(c: Expression<T>): T? = getOrNull(c)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T> rawToColumnValue(raw: T?, c: Expression<T>): T {
|
||||
return when {
|
||||
raw == null -> null
|
||||
raw == NotInitializedValue -> error("${c.toQueryBuilder(QueryBuilder(false))} is not initialized yet")
|
||||
c is ExpressionAlias<T> && c.delegate is ExpressionWithColumnType<T> -> c.delegate.columnType.valueFromDB(raw)
|
||||
c is ExpressionWithColumnType<T> -> c.columnType.valueFromDB(raw)
|
||||
else -> raw
|
||||
} as T
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T> getRaw(c: Expression<T>): T? =
|
||||
data[fieldIndex[c] ?: error("${c.toQueryBuilder(QueryBuilder(false))} is not in record set")] as T?
|
||||
|
||||
override fun toString(): String =
|
||||
fieldIndex.entries.joinToString { "${it.key.toQueryBuilder(QueryBuilder(false))}=${data[it.value]}" }
|
||||
|
||||
internal object NotInitializedValue
|
||||
|
||||
companion object {
|
||||
fun create(rs: ResultSet, fields: List<Expression<*>>): ResultRow {
|
||||
val fieldsIndex = fields.distinct().mapIndexed { i, field ->
|
||||
val value = (field as? Column<*>)?.columnType?.readObject(rs, i + 1) ?: rs.getObject(i + 1)
|
||||
(field to i) to value
|
||||
}.toMap()
|
||||
return ResultRow(fieldsIndex.keys.toMap()).apply {
|
||||
fieldsIndex.forEach{ (i, f) ->
|
||||
data[i.second] = f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createAndFillValues(data: Map<Expression<*>, Any?>) : ResultRow =
|
||||
ResultRow(data.keys.mapIndexed { i, c -> c to i }.toMap()).also { row ->
|
||||
data.forEach { (c, v) -> row[c] = v }
|
||||
}
|
||||
|
||||
fun createAndFillDefaults(columns : List<Column<*>>): ResultRow =
|
||||
ResultRow(columns.mapIndexed { i, c -> c to i }.toMap()).apply {
|
||||
columns.forEach {
|
||||
this[it] = it.defaultValueFun?.invoke() ?: if (!it.columnType.nullable) NotInitializedValue else null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
package org.jetbrains.exposed.sql
|
||||
|
||||
import org.jetbrains.exposed.dao.EntityID
|
||||
import org.jetbrains.exposed.dao.EntityIDFunctionProvider
|
||||
import org.jetbrains.exposed.dao.IdTable
|
||||
import org.jetbrains.exposed.sql.vendors.FunctionProvider
|
||||
import org.jetbrains.exposed.sql.vendors.currentDialect
|
||||
@@ -171,7 +172,7 @@ object SqlExpressionBuilder {
|
||||
@JvmName("inListIds")
|
||||
infix fun<T:Comparable<T>> Column<EntityID<T>>.inList(list: Iterable<T>): Op<Boolean> {
|
||||
val idTable = (columnType as EntityIDColumnType<T>).idColumn.table as IdTable<T>
|
||||
return inList(list.map { EntityID(it, idTable) })
|
||||
return inList(list.map { EntityIDFunctionProvider.createEntityID(it, idTable) })
|
||||
}
|
||||
|
||||
infix fun<T> ExpressionWithColumnType<T>.notInList(list: Iterable<T>): Op<Boolean> = InListOrNotInListOp(this, list, isInList = false)
|
||||
@@ -180,7 +181,7 @@ object SqlExpressionBuilder {
|
||||
@JvmName("notInListIds")
|
||||
infix fun<T:Comparable<T>> Column<EntityID<T>>.notInList(list: Iterable<T>): Op<Boolean> {
|
||||
val idTable = (columnType as EntityIDColumnType<T>).idColumn.table as IdTable<T>
|
||||
return notInList(list.map { EntityID(it, idTable) })
|
||||
return notInList(list.map { EntityIDFunctionProvider.createEntityID(it, idTable) })
|
||||
}
|
||||
|
||||
infix fun<T> ExpressionWithColumnType<T>.inSubQuery(query: Query): Op<Boolean> = InSubQueryOp(this, query)
|
||||
|
||||
@@ -238,7 +238,6 @@ object SchemaUtils {
|
||||
fun drop(vararg tables: Table, inBatch: Boolean = false) {
|
||||
if (tables.isEmpty()) return
|
||||
with(TransactionManager.current()) {
|
||||
flushCache()
|
||||
var tablesForDeletion =
|
||||
sortTablesByReferences(tables.toList())
|
||||
.reversed()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.jetbrains.exposed.sql
|
||||
|
||||
import org.jetbrains.exposed.dao.EntityID
|
||||
import org.jetbrains.exposed.dao.EntityIDFunctionProvider
|
||||
import org.jetbrains.exposed.dao.IdTable
|
||||
import org.jetbrains.exposed.sql.statements.api.ExposedBlob
|
||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||
@@ -207,7 +208,7 @@ open class Table(name: String = ""): ColumnSet(), DdlAware {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T:Comparable<T>> Column<T>.entityId(): Column<EntityID<T>> = replaceColumn(this, Column<EntityID<T>>(table, name, EntityIDColumnType(this)).also {
|
||||
it.indexInPK = this.indexInPK
|
||||
it.defaultValueFun = defaultValueFun?.let { { EntityID(it(), table as IdTable<T>) } }
|
||||
it.defaultValueFun = defaultValueFun?.let { { EntityIDFunctionProvider.createEntityID(it(), table as IdTable<T>) } }
|
||||
})
|
||||
|
||||
fun <ID:Comparable<ID>> entityId(name: String, table: IdTable<ID>) : Column<EntityID<ID>> {
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package org.jetbrains.exposed.sql
|
||||
|
||||
import org.jetbrains.exposed.dao.Entity
|
||||
import org.jetbrains.exposed.dao.EntityCache
|
||||
import org.jetbrains.exposed.dao.EntityChange
|
||||
import org.jetbrains.exposed.dao.EntityHook
|
||||
import org.jetbrains.exposed.sql.statements.Statement
|
||||
import org.jetbrains.exposed.sql.statements.StatementInterceptor
|
||||
import org.jetbrains.exposed.sql.statements.StatementType
|
||||
@@ -12,7 +8,6 @@ import org.jetbrains.exposed.sql.transactions.TransactionInterface
|
||||
import org.jetbrains.exposed.sql.vendors.inProperCase
|
||||
import java.sql.ResultSet
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
class Key<T>
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@@ -42,9 +37,9 @@ open class Transaction(private val transactionImpl: TransactionInterface): UserD
|
||||
var duration: Long = 0
|
||||
var warnLongQueriesDuration: Long? = null
|
||||
var debug = false
|
||||
val entityCache = EntityCache(this)
|
||||
// val entityCache = EntityCache(this)
|
||||
|
||||
internal val entityEvents = CopyOnWriteArrayList<EntityChange>()
|
||||
// internal val entityEvents = CopyOnWriteArrayList<EntityChange>()
|
||||
|
||||
// currently executing statement. Used to log error properly
|
||||
var currentStatement: PreparedStatementApi? = null
|
||||
@@ -59,32 +54,20 @@ open class Transaction(private val transactionImpl: TransactionInterface): UserD
|
||||
}
|
||||
|
||||
override fun commit() {
|
||||
val created = flushCache()
|
||||
EntityHook.alertSubscribers(this)
|
||||
val createdByHooks = flushCache()
|
||||
globalInterceptors.forEach { it.beforeCommit(this) }
|
||||
interceptors.forEach { it.beforeCommit(this) }
|
||||
transactionImpl.commit()
|
||||
userdata.clear()
|
||||
EntityCache.invalidateGlobalCaches(created + createdByHooks)
|
||||
globalInterceptors.forEach { it.afterCommit() }
|
||||
interceptors.forEach { it.afterCommit() }
|
||||
userdata.clear()
|
||||
}
|
||||
|
||||
override fun rollback() {
|
||||
globalInterceptors.forEach { it.beforeRollback(this) }
|
||||
interceptors.forEach { it.beforeRollback(this) }
|
||||
transactionImpl.rollback()
|
||||
userdata.clear()
|
||||
entityCache.clearReferrersCache()
|
||||
entityCache.data.clear()
|
||||
entityCache.inserts.clear()
|
||||
interceptors.forEach { it.afterRollback() }
|
||||
}
|
||||
|
||||
fun flushCache(): List<Entity<*>> {
|
||||
with(entityCache) {
|
||||
val newEntities = inserts.flatMap { it.value }
|
||||
flush()
|
||||
return newEntities
|
||||
}
|
||||
userdata.clear()
|
||||
}
|
||||
|
||||
private fun describeStatement(delta: Long, stmt: String): String = "[${delta}ms] ${stmt.take(1024)}\n\n"
|
||||
@@ -186,5 +169,14 @@ open class Transaction(private val transactionImpl: TransactionInterface): UserD
|
||||
executedStatements.clear()
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal val globalInterceptors = arrayListOf<StatementInterceptor>()
|
||||
fun registerGlobalIntercepter(statement: StatementInterceptor) {
|
||||
globalInterceptors.add(statement)
|
||||
}
|
||||
init {
|
||||
this::class.java.classLoader.getResources("org.jetbrains.exposed.intercepter") // iterate to load all intercepters
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.jetbrains.exposed.sql.statements
|
||||
|
||||
import org.jetbrains.exposed.dao.Entity
|
||||
import org.jetbrains.exposed.dao.EntityClass
|
||||
import org.jetbrains.exposed.dao.EntityID
|
||||
import org.jetbrains.exposed.dao.IdTable
|
||||
import org.jetbrains.exposed.sql.Column
|
||||
@@ -35,44 +33,14 @@ open class BatchUpdateStatement(val table: IdTable<*>): UpdateStatement(table, n
|
||||
data.add(id to values)
|
||||
}
|
||||
|
||||
override fun <T, S:T?> update(column: Column<T>, value: Expression<S>) = error("Expressions unsupported in batch update")
|
||||
override fun <T, S : T?> update(column: Column<T>, value: Expression<S>) = error("Expressions unsupported in batch update")
|
||||
|
||||
override fun prepareSQL(transaction: Transaction): String =
|
||||
"${super.prepareSQL(transaction)} WHERE ${transaction.identity(table.id)} = ?"
|
||||
"${super.prepareSQL(transaction)} WHERE ${transaction.identity(table.id)} = ?"
|
||||
|
||||
override fun PreparedStatementApi.executeInternal(transaction: Transaction): Int = if (data.size == 1) executeUpdate() else executeBatch().sum()
|
||||
|
||||
override fun arguments(): Iterable<Iterable<Pair<IColumnType, Any?>>> = data.map { (id, row) ->
|
||||
firstDataSet.map { it.first.columnType to row[it.first] } + (table.id.columnType to id)
|
||||
}
|
||||
}
|
||||
|
||||
class EntityBatchUpdate(val klass: EntityClass<*, Entity<*>>) {
|
||||
|
||||
private val data = ArrayList<Pair<EntityID<*>, SortedMap<Column<*>, Any?>>>()
|
||||
|
||||
fun addBatch(id: EntityID<*>) {
|
||||
if (id.table != klass.table) error("Table from Entity ID ${id.table.tableName} differs from entity class ${klass.table.tableName}")
|
||||
data.add(id to TreeMap())
|
||||
}
|
||||
|
||||
operator fun set(column: Column<*>, value: Any?) {
|
||||
val values = data.last().second
|
||||
|
||||
if (values.containsKey(column)) {
|
||||
error("$column is already initialized")
|
||||
}
|
||||
|
||||
values[column] = value
|
||||
}
|
||||
|
||||
fun execute(transaction: Transaction): Int {
|
||||
val updateSets = data.filterNot {it.second.isEmpty()}.groupBy { it.second.keys }
|
||||
return updateSets.values.fold(0) { acc, set ->
|
||||
acc + BatchUpdateStatement(klass.table).let {
|
||||
it.data.addAll(set)
|
||||
it.execute(transaction)!!
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,6 @@ import org.jetbrains.exposed.sql.statements.api.PreparedStatementApi
|
||||
open class DeleteStatement(val table: Table, val where: Op<Boolean>? = null, val isIgnore: Boolean = false, val limit: Int? = null, val offset: Int? = null): Statement<Int>(StatementType.DELETE, listOf(table)) {
|
||||
|
||||
override fun PreparedStatementApi.executeInternal(transaction: Transaction): Int {
|
||||
transaction.flushCache()
|
||||
transaction.entityCache.removeTablesReferrers(listOf(table))
|
||||
return executeUpdate()
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import java.sql.SQLException
|
||||
* isIgnore is supported for mysql only
|
||||
*/
|
||||
open class InsertStatement<Key:Any>(val table: Table, val isIgnore: Boolean = false) : UpdateBuilder<Int>(StatementType.INSERT, listOf(table)) {
|
||||
protected open val flushCache = true
|
||||
open val flushCache = true
|
||||
var resultedValues: List<ResultRow>? = null
|
||||
private set
|
||||
|
||||
@@ -113,9 +113,6 @@ open class InsertStatement<Key:Any>(val table: Table, val isIgnore: Boolean = fa
|
||||
}
|
||||
|
||||
override fun PreparedStatementApi.executeInternal(transaction: Transaction): Int {
|
||||
if (flushCache)
|
||||
transaction.flushCache()
|
||||
transaction.entityCache.removeTablesReferrers(listOf(table))
|
||||
val (inserted, rs) = execInsertFunction()
|
||||
return inserted.apply {
|
||||
resultedValues = processResults(rs, this)
|
||||
|
||||
@@ -33,11 +33,13 @@ abstract class Statement<out T>(val type: StatementType, val targets: List<Table
|
||||
val contexts = if (arguments.count() > 0) {
|
||||
arguments.map { args ->
|
||||
val context = StatementContext(this, args)
|
||||
Transaction.globalInterceptors.forEach { it.beforeExecution(transaction, context) }
|
||||
transaction.interceptors.forEach { it.beforeExecution(transaction, context) }
|
||||
context
|
||||
}
|
||||
} else {
|
||||
val context = StatementContext(this, emptyList())
|
||||
Transaction.globalInterceptors.forEach { it.beforeExecution(transaction, context) }
|
||||
transaction.interceptors.forEach { it.beforeExecution(transaction, context) }
|
||||
listOf(context)
|
||||
}
|
||||
@@ -63,6 +65,7 @@ abstract class Statement<out T>(val type: StatementType, val targets: List<Table
|
||||
transaction.currentStatement = null
|
||||
transaction.executedStatements.add(statement)
|
||||
|
||||
Transaction.globalInterceptors.forEach { it.afterExecution(transaction, contexts, statement) }
|
||||
transaction.interceptors.forEach { it.afterExecution(transaction, contexts, statement) }
|
||||
return result to contexts
|
||||
}
|
||||
|
||||
@@ -9,10 +9,7 @@ open class UpdateStatement(val targetsSet: ColumnSet, val limit: Int?, val where
|
||||
|
||||
override fun PreparedStatementApi.executeInternal(transaction: Transaction): Int {
|
||||
if (values.isEmpty()) return 0
|
||||
transaction.flushCache()
|
||||
return executeUpdate().apply {
|
||||
transaction.entityCache.removeTablesReferrers(targetsSet.targetTables())
|
||||
}
|
||||
return executeUpdate()
|
||||
}
|
||||
|
||||
override fun prepareSQL(transaction: Transaction): String =
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
package org.jetbrains.exposed.sql.transactions
|
||||
|
||||
import org.jetbrains.exposed.sql.Key
|
||||
import org.jetbrains.exposed.sql.Transaction
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T:Any> transactionScope(init: () -> T) = TransactionStore(init) as ReadWriteProperty<Any?, T>
|
||||
fun <T:Any> transactionScope(init: Transaction.() -> T) = TransactionStore(init) as ReadWriteProperty<Any?, T>
|
||||
fun <T:Any> nullableTransactionScope() = TransactionStore<T>()
|
||||
|
||||
class TransactionStore<T:Any>(val init: (() -> T)? = null) : ReadWriteProperty<Any?, T?> {
|
||||
class TransactionStore<T:Any>(val init: (Transaction.() -> T)? = null) : ReadWriteProperty<Any?, T?> {
|
||||
|
||||
private val key = Key<T>()
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun getValue(thisRef: Any?, property: KProperty<*>): T? {
|
||||
val currentOrNullTransaction = TransactionManager.currentOrNull()
|
||||
return init?.let { currentOrNullTransaction!!.getOrCreate(key, init) } ?: currentOrNullTransaction?.getUserData(key)
|
||||
return currentOrNullTransaction?.getUserData(key)
|
||||
?: init?.let {
|
||||
val value = currentOrNullTransaction!!.it()
|
||||
currentOrNullTransaction.putUserData(key, value)
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {
|
||||
|
||||
48
exposed-dao/build.gradle.kts
Normal file
48
exposed-dao/build.gradle.kts
Normal file
@@ -0,0 +1,48 @@
|
||||
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
|
||||
import org.gradle.api.tasks.testing.logging.TestLogEvent
|
||||
import org.jetbrains.exposed.gradle.setupDialectTest
|
||||
import tanvd.kosogor.proxy.publishJar
|
||||
|
||||
plugins {
|
||||
kotlin("jvm") apply true
|
||||
}
|
||||
|
||||
repositories {
|
||||
jcenter()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(project(":exposed-core"))
|
||||
}
|
||||
|
||||
publishJar {
|
||||
publication {
|
||||
artifactId = "exposed-dao"
|
||||
}
|
||||
|
||||
bintray {
|
||||
username = project.properties["bintrayUser"]?.toString() ?: System.getenv("BINTRAY_USER")
|
||||
secretKey = project.properties["bintrayApiKey"]?.toString() ?: System.getenv("BINTRAY_API_KEY")
|
||||
repository = "exposed"
|
||||
info {
|
||||
publish = false
|
||||
githubRepo = "https://github.com/JetBrains/Exposed.git"
|
||||
vcsUrl = "https://github.com/JetBrains/Exposed.git"
|
||||
userOrg = "kotlin"
|
||||
license = "Apache-2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(Test::class.java) {
|
||||
jvmArgs = listOf("-XX:MaxPermSize=256m")
|
||||
testLogging {
|
||||
events.addAll(listOf(TestLogEvent.PASSED, TestLogEvent.FAILED, TestLogEvent.SKIPPED))
|
||||
showStandardStreams = true
|
||||
exceptionFormat = TestExceptionFormat.FULL
|
||||
}
|
||||
}
|
||||
|
||||
val dialect: String by project
|
||||
|
||||
setupDialectTest(dialect)
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.jetbrains.exposed.dao
|
||||
|
||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||
|
||||
class DaoEntityID<T:Comparable<T>>(id: T?, table: IdTable<T>) : EntityID<T>(id, table) {
|
||||
override fun invokeOnNoValue() {
|
||||
TransactionManager.current().entityCache.flushInserts(table)
|
||||
}
|
||||
}
|
||||
|
||||
257
exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/Entity.kt
Normal file
257
exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/Entity.kt
Normal file
@@ -0,0 +1,257 @@
|
||||
package org.jetbrains.exposed.dao
|
||||
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||
import java.util.*
|
||||
import kotlin.properties.Delegates
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
open class ColumnWithTransform<TColumn, TReal>(val column: Column<TColumn>, val toColumn: (TReal) -> TColumn, val toReal: (TColumn) -> TReal)
|
||||
|
||||
class View<out Target: Entity<*>> (val op : Op<Boolean>, val factory: EntityClass<*, Target>) : SizedIterable<Target> {
|
||||
override fun limit(n: Int, offset: Int): SizedIterable<Target> = factory.find(op).limit(n, offset)
|
||||
override fun count(): Int = factory.find(op).count()
|
||||
override fun empty(): Boolean = factory.find(op).empty()
|
||||
override fun forUpdate(): SizedIterable<Target> = factory.find(op).forUpdate()
|
||||
override fun notForUpdate(): SizedIterable<Target> = factory.find(op).notForUpdate()
|
||||
|
||||
override operator fun iterator(): Iterator<Target> = factory.find(op).iterator()
|
||||
operator fun getValue(o: Any?, desc: KProperty<*>): SizedIterable<Target> = factory.find(op)
|
||||
override fun copy(): SizedIterable<Target> = View(op, factory)
|
||||
override fun orderBy(vararg order: Pair<Expression<*>, SortOrder>): SizedIterable<Target> = factory.find(op).orderBy(*order)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class InnerTableLink<SID:Comparable<SID>, Source: Entity<SID>, ID:Comparable<ID>, Target: Entity<ID>>(
|
||||
val table: Table,
|
||||
val target: EntityClass<ID, Target>,
|
||||
val sourceColumn: Column<EntityID<SID>>? = null,
|
||||
_targetColumn: Column<EntityID<ID>>? = null) : ReadWriteProperty<Source, SizedIterable<Target>> {
|
||||
init {
|
||||
_targetColumn?.let {
|
||||
requireNotNull(sourceColumn) { "Both source and target columns should be specified"}
|
||||
require(_targetColumn.referee?.table == target.table) {
|
||||
"Column $_targetColumn point to wrong table, expected ${target.table.tableName}"
|
||||
}
|
||||
require(_targetColumn.table == sourceColumn.table) {
|
||||
"Both source and target columns should be from the same table"
|
||||
}
|
||||
}
|
||||
sourceColumn?.let {
|
||||
requireNotNull(_targetColumn) { "Both source and target columns should be specified"}
|
||||
}
|
||||
}
|
||||
|
||||
private val targetColumn = _targetColumn
|
||||
?: table.columns.singleOrNull { it.referee == target.table.id } as? Column<EntityID<ID>>
|
||||
?: error("Table does not reference target")
|
||||
|
||||
private fun getSourceRefColumn(o: Source): Column<EntityID<SID>> {
|
||||
return sourceColumn ?: table.columns.singleOrNull { it.referee == o.klass.table.id } as? Column<EntityID<SID>> ?: error("Table does not reference source")
|
||||
}
|
||||
|
||||
override operator fun getValue(o: Source, unused: KProperty<*>): SizedIterable<Target> {
|
||||
if (o.id._value == null) return emptySized()
|
||||
val sourceRefColumn = getSourceRefColumn(o)
|
||||
val alreadyInJoin = (target.dependsOnTables as? Join)?.alreadyInJoin(table)?: false
|
||||
val entityTables = if (alreadyInJoin) target.dependsOnTables else target.dependsOnTables.join(table, JoinType.INNER, target.table.id, targetColumn)
|
||||
|
||||
val columns = (target.dependsOnColumns + (if (!alreadyInJoin) table.columns else emptyList())
|
||||
- sourceRefColumn).distinct() + sourceRefColumn
|
||||
|
||||
val query = {target.wrapRows(entityTables.slice(columns).select{sourceRefColumn eq o.id})}
|
||||
return TransactionManager.current().entityCache.getOrPutReferrers(o.id, sourceRefColumn, query)
|
||||
}
|
||||
|
||||
override fun setValue(o: Source, unused: KProperty<*>, value: SizedIterable<Target>) {
|
||||
val sourceRefColumn = getSourceRefColumn(o)
|
||||
|
||||
val tx = TransactionManager.current()
|
||||
val entityCache = tx.entityCache
|
||||
entityCache.flush()
|
||||
val oldValue = getValue(o, unused)
|
||||
val existingIds = oldValue.map { it.id }.toSet()
|
||||
entityCache.clearReferrersCache()
|
||||
|
||||
val targetIds = value.map { it.id }
|
||||
table.deleteWhere { (sourceRefColumn eq o.id) and (targetColumn notInList targetIds) }
|
||||
table.batchInsert(targetIds.filter { !existingIds.contains(it) }) { targetId ->
|
||||
this[sourceRefColumn] = o.id
|
||||
this[targetColumn] = targetId
|
||||
}
|
||||
|
||||
// current entity updated
|
||||
EntityHook.registerChange(tx, EntityChange(o.klass, o.id, EntityChangeType.Updated))
|
||||
|
||||
// linked entities updated
|
||||
val targetClass = (value.firstOrNull() ?: oldValue.firstOrNull())?.klass
|
||||
if (targetClass != null) {
|
||||
existingIds.plus(targetIds).forEach {
|
||||
EntityHook.registerChange(tx, EntityChange(targetClass, it, EntityChangeType.Updated))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class Entity<ID:Comparable<ID>>(val id: EntityID<ID>) {
|
||||
var klass: EntityClass<ID, Entity<ID>> by Delegates.notNull()
|
||||
internal set
|
||||
|
||||
var db: Database by Delegates.notNull()
|
||||
internal set
|
||||
|
||||
val writeValues = LinkedHashMap<Column<Any?>, Any?>()
|
||||
var _readValues: ResultRow? = null
|
||||
val readValues: ResultRow
|
||||
get() = _readValues ?: run {
|
||||
val table = klass.table
|
||||
_readValues = klass.searchQuery( Op.build {table.id eq id }).firstOrNull() ?: table.select { table.id eq id }.first()
|
||||
_readValues!!
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates entity fields from database.
|
||||
* Override function to refresh some additional state if any.
|
||||
*
|
||||
* @param flush whether pending entity changes should be flushed previously
|
||||
* @throws EntityNotFoundException if entity no longer exists in database
|
||||
*/
|
||||
open fun refresh(flush: Boolean = false) {
|
||||
if (flush) flush() else writeValues.clear()
|
||||
|
||||
klass.removeFromCache(this)
|
||||
val reloaded = klass[id]
|
||||
TransactionManager.current().entityCache.store(this)
|
||||
_readValues = reloaded.readValues
|
||||
}
|
||||
|
||||
operator fun <REF:Comparable<REF>, RID:Comparable<RID>, T: Entity<RID>> Reference<REF, RID, T>.getValue(o: Entity<ID>, desc: KProperty<*>): T {
|
||||
val refValue = reference.getValue(o, desc)
|
||||
return when {
|
||||
refValue is EntityID<*> && reference.referee<REF>() == factory.table.id -> factory.findById(refValue.value as RID)
|
||||
else -> factory.findWithCacheCondition({ reference.referee!!.getValue(this, desc) == refValue }) { reference.referee<REF>()!! eq refValue }.singleOrNull()
|
||||
} ?: error("Cannot find ${factory.table.tableName} WHERE id=$refValue")
|
||||
}
|
||||
|
||||
operator fun <REF:Comparable<REF>, RID:Comparable<RID>, T: Entity<RID>> Reference<REF, RID, T>.setValue(o: Entity<ID>, desc: KProperty<*>, value: T) {
|
||||
if (db != value.db) error("Can't link entities from different databases.")
|
||||
value.id.value // flush before creating reference on it
|
||||
val refValue = value.run { reference.referee<REF>()!!.getValue(this, desc) }
|
||||
reference.setValue(o, desc, refValue)
|
||||
}
|
||||
|
||||
operator fun <REF:Comparable<REF>, RID:Comparable<RID>, T: Entity<RID>> OptionalReference<REF, RID, T>.getValue(o: Entity<ID>, desc: KProperty<*>): T? {
|
||||
val refValue = reference.getValue(o, desc)
|
||||
return when {
|
||||
refValue == null -> null
|
||||
refValue is EntityID<*> && reference.referee<REF>() == factory.table.id -> factory.findById(refValue.value as RID)
|
||||
else -> factory.findWithCacheCondition({ reference.referee!!.getValue(this, desc) == refValue }) { reference.referee<REF>()!! eq refValue }.singleOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
operator fun <REF:Comparable<REF>, RID:Comparable<RID>, T: Entity<RID>> OptionalReference<REF, RID, T>.setValue(o: Entity<ID>, desc: KProperty<*>, value: T?) {
|
||||
if (value != null && db != value.db) error("Can't link entities from different databases.")
|
||||
value?.id?.value // flush before creating reference on it
|
||||
val refValue = value?.run { reference.referee<REF>()!!.getValue(this, desc) }
|
||||
reference.setValue(o, desc, refValue)
|
||||
}
|
||||
operator fun <T> Column<T>.getValue(o: Entity<ID>, desc: KProperty<*>): T = lookup()
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T, R:Any> Column<T>.lookupInReadValues(found: (T?) -> R?, notFound: () -> R?): R? =
|
||||
if (_readValues?.hasValue(this) == true)
|
||||
found(readValues[this])
|
||||
else
|
||||
notFound()
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "USELESS_CAST")
|
||||
fun <T> Column<T>.lookup(): T = when {
|
||||
writeValues.containsKey(this as Column<out Any?>) -> writeValues[this as Column<out Any?>] as T
|
||||
id._value == null && _readValues?.hasValue(this)?.not() ?: true -> defaultValueFun?.invoke() as T
|
||||
columnType.nullable -> readValues[this]
|
||||
else -> readValues[this]!!
|
||||
}
|
||||
|
||||
operator fun <T> Column<T>.setValue(o: Entity<ID>, desc: KProperty<*>, value: T) {
|
||||
klass.invalidateEntityInCache(o)
|
||||
val currentValue = _readValues?.getOrNull(this)
|
||||
if (writeValues.containsKey(this as Column<out Any?>) || currentValue != value) {
|
||||
if (referee != null) {
|
||||
val entityCache = TransactionManager.current().entityCache
|
||||
if (value is EntityID<*> && value.table == referee!!.table) value.value // flush
|
||||
|
||||
listOfNotNull<Any>(value, currentValue).forEach {
|
||||
entityCache.referrers[it]?.remove(this)
|
||||
}
|
||||
entityCache.removeTablesReferrers(listOf(referee!!.table))
|
||||
}
|
||||
writeValues[this as Column<Any?>] = value
|
||||
}
|
||||
}
|
||||
|
||||
operator fun <TColumn, TReal> ColumnWithTransform<TColumn, TReal>.getValue(o: Entity<ID>, desc: KProperty<*>): TReal =
|
||||
toReal(column.getValue(o, desc))
|
||||
|
||||
operator fun <TColumn, TReal> ColumnWithTransform<TColumn, TReal>.setValue(o: Entity<ID>, desc: KProperty<*>, value: TReal) {
|
||||
column.setValue(o, desc, toColumn(value))
|
||||
}
|
||||
|
||||
infix fun <TID:Comparable<TID>, Target: Entity<TID>> EntityClass<TID, Target>.via(table: Table): InnerTableLink<ID, Entity<ID>, TID, Target> =
|
||||
InnerTableLink(table, this@via)
|
||||
|
||||
fun <TID:Comparable<TID>, Target: Entity<TID>> EntityClass<TID, Target>.via(sourceColumn: Column<EntityID<ID>>, targetColumn: Column<EntityID<TID>>) =
|
||||
InnerTableLink(sourceColumn.table, this@via, sourceColumn, targetColumn)
|
||||
|
||||
/**
|
||||
* Delete this entity.
|
||||
*
|
||||
* This will remove the entity from the database as well as the cache.
|
||||
*/
|
||||
open fun delete(){
|
||||
klass.removeFromCache(this)
|
||||
val table = klass.table
|
||||
table.deleteWhere {table.id eq id}
|
||||
EntityHook.registerChange(TransactionManager.current(), EntityChange(klass, id, EntityChangeType.Removed))
|
||||
}
|
||||
|
||||
open fun flush(batch: EntityBatchUpdate? = null): Boolean {
|
||||
if (writeValues.isNotEmpty()) {
|
||||
if (batch == null) {
|
||||
val table = klass.table
|
||||
// Store values before update to prevent flush inside UpdateStatement
|
||||
val _writeValues = writeValues.toMap()
|
||||
storeWrittenValues()
|
||||
table.update({table.id eq id}) {
|
||||
for ((c, v) in _writeValues) {
|
||||
it[c] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
batch.addBatch(id)
|
||||
for ((c, v) in writeValues) {
|
||||
batch[c] = v
|
||||
}
|
||||
storeWrittenValues()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun storeWrittenValues() {
|
||||
// move write values to read values
|
||||
if (_readValues != null) {
|
||||
for ((c, v) in writeValues) {
|
||||
_readValues!![c] = v
|
||||
}
|
||||
if (klass.dependsOnColumns.any { it.table == klass.table && !_readValues!!.hasValue(it) } ) {
|
||||
_readValues = null
|
||||
}
|
||||
}
|
||||
// clear write values
|
||||
writeValues.clear()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.jetbrains.exposed.dao
|
||||
|
||||
import org.jetbrains.exposed.sql.Column
|
||||
import org.jetbrains.exposed.sql.Transaction
|
||||
import org.jetbrains.exposed.sql.statements.BatchUpdateStatement
|
||||
import java.util.*
|
||||
|
||||
class EntityBatchUpdate(val klass: EntityClass<*, Entity<*>>) {
|
||||
|
||||
private val data = ArrayList<Pair<EntityID<*>, SortedMap<Column<*>, Any?>>>()
|
||||
|
||||
fun addBatch(id: EntityID<*>) {
|
||||
if (id.table != klass.table) error("Table from Entity ID ${id.table.tableName} differs from entity class ${klass.table.tableName}")
|
||||
data.add(id to TreeMap())
|
||||
}
|
||||
|
||||
operator fun set(column: Column<*>, value: Any?) {
|
||||
val values = data.last().second
|
||||
|
||||
if (values.containsKey(column)) {
|
||||
error("$column is already initialized")
|
||||
}
|
||||
|
||||
values[column] = value
|
||||
}
|
||||
|
||||
fun execute(transaction: Transaction): Int {
|
||||
val updateSets = data.filterNot {it.second.isEmpty()}.groupBy { it.second.keys }
|
||||
return updateSets.values.fold(0) { acc, set ->
|
||||
acc + BatchUpdateStatement(klass.table).let {
|
||||
it.data.addAll(set)
|
||||
it.execute(transaction)!!
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package org.jetbrains.exposed.dao
|
||||
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.transactions.transactionScope
|
||||
import java.util.*
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
val Transaction.entityCache : EntityCache by transactionScope { EntityCache(this) }
|
||||
val Transaction.entityEvents : MutableList<EntityChange> by transactionScope { CopyOnWriteArrayList<EntityChange>() }
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class EntityCache(private val transaction: Transaction) {
|
||||
val data = LinkedHashMap<IdTable<*>, MutableMap<Any, Entity<*>>>()
|
||||
val inserts = LinkedHashMap<IdTable<*>, MutableList<Entity<*>>>()
|
||||
val referrers = HashMap<EntityID<*>, MutableMap<Column<*>, SizedIterable<*>>>()
|
||||
|
||||
private fun getMap(f: EntityClass<*, *>) : MutableMap<Any, Entity<*>> = getMap(f.table)
|
||||
|
||||
private fun getMap(table: IdTable<*>) : MutableMap<Any, Entity<*>> = data.getOrPut(table) {
|
||||
LinkedHashMap()
|
||||
}
|
||||
|
||||
fun <ID: Any, R: Entity<ID>> getOrPutReferrers(sourceId: EntityID<*>, key: Column<*>, refs: ()-> SizedIterable<R>): SizedIterable<R> =
|
||||
referrers.getOrPut(sourceId){ HashMap() }.getOrPut(key) { LazySizedCollection(refs()) } as SizedIterable<R>
|
||||
|
||||
fun <ID:Comparable<ID>, T: Entity<ID>> find(f: EntityClass<ID, T>, id: EntityID<ID>): T? = getMap(f)[id.value] as T? ?: inserts[f.table]?.firstOrNull { it.id == id } as? T
|
||||
|
||||
fun <ID:Comparable<ID>, T: Entity<ID>> findAll(f: EntityClass<ID, T>): Collection<T> = getMap(f).values as Collection<T>
|
||||
|
||||
fun <ID:Comparable<ID>, T: Entity<ID>> store(f: EntityClass<ID, T>, o: T) {
|
||||
getMap(f)[o.id.value] = o
|
||||
}
|
||||
|
||||
fun store(o: Entity<*>) {
|
||||
getMap(o.klass.table)[o.id.value] = o
|
||||
}
|
||||
|
||||
fun <ID:Comparable<ID>, T: Entity<ID>> remove(table: IdTable<ID>, o: T) {
|
||||
getMap(table).remove(o.id.value)
|
||||
}
|
||||
|
||||
fun <ID:Comparable<ID>, T: Entity<ID>> scheduleInsert(f: EntityClass<ID, T>, o: T) {
|
||||
inserts.getOrPut(f.table) { arrayListOf() }.add(o as Entity<*>)
|
||||
}
|
||||
|
||||
fun flush() {
|
||||
flush(inserts.keys + data.keys)
|
||||
}
|
||||
|
||||
fun flush(tables: Iterable<IdTable<*>>) {
|
||||
val insertedTables = inserts.keys
|
||||
|
||||
SchemaUtils.sortTablesByReferences(tables).filterIsInstance<IdTable<*>>().forEach(::flushInserts)
|
||||
|
||||
for (t in tables) {
|
||||
data[t]?.let { map ->
|
||||
if (map.isNotEmpty()) {
|
||||
val updatedEntities = HashSet<Entity<*>>()
|
||||
val batch = EntityBatchUpdate(map.values.first().klass)
|
||||
for ((_, entity) in map) {
|
||||
if (entity.flush(batch)) {
|
||||
check(entity.klass !is ImmutableEntityClass<*, *>) { "Update on immutable entity ${entity.javaClass.simpleName} ${entity.id}" }
|
||||
updatedEntities.add(entity)
|
||||
}
|
||||
}
|
||||
batch.execute(transaction)
|
||||
updatedEntities.forEach {
|
||||
EntityHook.registerChange(transaction, EntityChange(it.klass, it.id, EntityChangeType.Updated))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (insertedTables.isNotEmpty()) {
|
||||
removeTablesReferrers(insertedTables)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun removeTablesReferrers(insertedTables: Collection<Table>) {
|
||||
referrers.filterValues { it.any { it.key.table in insertedTables } }.map { it.key }.forEach {
|
||||
referrers.remove(it)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun flushInserts(table: IdTable<*>) {
|
||||
inserts.remove(table)?.let {
|
||||
var toFlush: List<Entity<*>> = it
|
||||
do {
|
||||
val partition = toFlush.partition {
|
||||
it.writeValues.none {
|
||||
val (key, value) = it
|
||||
key.referee == table.id && value is EntityID<*> && value._value == null
|
||||
}
|
||||
}
|
||||
toFlush = partition.first
|
||||
val ids = table.batchInsert(toFlush) { entry ->
|
||||
for ((c, v) in entry.writeValues) {
|
||||
this[c] = v
|
||||
}
|
||||
}
|
||||
|
||||
for ((entry, genValues) in toFlush.zip(ids)) {
|
||||
if (entry.id._value == null) {
|
||||
val id = genValues[table.id]
|
||||
entry.id._value = id._value
|
||||
entry.writeValues[entry.klass.table.id as Column<Any?>] = id
|
||||
}
|
||||
genValues.fieldIndex.keys.forEach { key ->
|
||||
entry.writeValues[key as Column<Any?>] = genValues[key]
|
||||
}
|
||||
|
||||
entry.storeWrittenValues()
|
||||
store(entry)
|
||||
EntityHook.registerChange(transaction, EntityChange(entry.klass, entry.id, EntityChangeType.Created))
|
||||
}
|
||||
toFlush = partition.second
|
||||
} while(toFlush.isNotEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
fun clearReferrersCache() {
|
||||
referrers.clear()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun invalidateGlobalCaches(created: List<Entity<*>>) {
|
||||
created.asSequence().mapNotNull { it.klass as? ImmutableCachedEntityClass<*, *> }.distinct().forEach {
|
||||
it.expireCache()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Transaction.flushCache(): List<Entity<*>> {
|
||||
with(entityCache) {
|
||||
val newEntities = inserts.flatMap { it.value }
|
||||
flush()
|
||||
return newEntities
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
package org.jetbrains.exposed.dao
|
||||
|
||||
import org.jetbrains.exposed.dao.exceptions.EntityNotFoundException
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.full.primaryConstructor
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
abstract class EntityClass<ID : Comparable<ID>, out T: Entity<ID>>(val table: IdTable<ID>, entityType: Class<T>? = null) {
|
||||
internal val klass: Class<*> = entityType ?: javaClass.enclosingClass as Class<T>
|
||||
private val ctor = klass.kotlin.primaryConstructor!!
|
||||
|
||||
operator fun get(id: EntityID<ID>): T = findById(id) ?: throw EntityNotFoundException(id, this)
|
||||
|
||||
operator fun get(id: ID): T = get(DaoEntityID(id, table))
|
||||
|
||||
protected open fun warmCache(): EntityCache = TransactionManager.current().entityCache
|
||||
|
||||
/**
|
||||
* Get an entity by its [id].
|
||||
*
|
||||
* @param id The id of the entity
|
||||
*
|
||||
* @return The entity that has this id or null if no entity was found.
|
||||
*/
|
||||
fun findById(id: ID): T? = findById(DaoEntityID(id, table))
|
||||
|
||||
/**
|
||||
* Get an entity by its [id].
|
||||
*
|
||||
* @param id The id of the entity
|
||||
*
|
||||
* @return The entity that has this id or null if no entity was found.
|
||||
*/
|
||||
open fun findById(id: EntityID<ID>): T? = testCache(id) ?: find{table.id eq id}.firstOrNull()
|
||||
|
||||
/**
|
||||
* Reloads entity fields from database as new object.
|
||||
* @param flush whether pending entity changes should be flushed previously
|
||||
*/
|
||||
fun reload(entity: Entity<ID>, flush: Boolean = false): T? {
|
||||
if (flush) entity.flush()
|
||||
removeFromCache(entity)
|
||||
return findById(entity.id)
|
||||
}
|
||||
|
||||
internal open fun invalidateEntityInCache(o: Entity<ID>) {
|
||||
if (o.id._value != null && testCache(o.id) == null && TransactionManager.current().db == o.db) {
|
||||
get(o.id) // Check that entity is still exists in database
|
||||
warmCache().store(o)
|
||||
}
|
||||
}
|
||||
|
||||
fun testCache(id: EntityID<ID>): T? = warmCache().find(this, id)
|
||||
|
||||
fun testCache(cacheCheckCondition: T.()->Boolean): Sequence<T> = warmCache().findAll(this).asSequence().filter { it.cacheCheckCondition() }
|
||||
|
||||
fun removeFromCache(entity: Entity<ID>) {
|
||||
val cache = warmCache()
|
||||
cache.remove(table, entity)
|
||||
cache.referrers.remove(entity.id)
|
||||
cache.removeTablesReferrers(listOf(table))
|
||||
}
|
||||
|
||||
open fun forEntityIds(ids: List<EntityID<ID>>) : SizedIterable<T> {
|
||||
val distinctIds = ids.distinct()
|
||||
if (distinctIds.isEmpty()) return emptySized()
|
||||
|
||||
val cached = distinctIds.mapNotNull { testCache(it) }
|
||||
|
||||
if (cached.size == distinctIds.size) {
|
||||
return SizedCollection(cached)
|
||||
}
|
||||
|
||||
return wrapRows(searchQuery(Op.build { table.id inList distinctIds }))
|
||||
}
|
||||
|
||||
fun forIds(ids: List<ID>) : SizedIterable<T> = forEntityIds(ids.map { DaoEntityID(it, table) })
|
||||
|
||||
fun wrapRows(rows: SizedIterable<ResultRow>): SizedIterable<T> = rows mapLazy {
|
||||
wrapRow(it)
|
||||
}
|
||||
|
||||
fun wrapRows(rows: SizedIterable<ResultRow>, alias: Alias<IdTable<*>>) = rows mapLazy {
|
||||
wrapRow(it, alias)
|
||||
}
|
||||
|
||||
fun wrapRows(rows: SizedIterable<ResultRow>, alias: QueryAlias) = rows mapLazy {
|
||||
wrapRow(it, alias)
|
||||
}
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
fun wrapRow(row: ResultRow) : T {
|
||||
val entity = wrap(row[table.id], row)
|
||||
if (entity._readValues == null)
|
||||
entity._readValues = row
|
||||
|
||||
return entity
|
||||
}
|
||||
|
||||
fun wrapRow(row: ResultRow, alias: Alias<IdTable<*>>) : T {
|
||||
require(alias.delegate == table) { "Alias for a wrong table ${alias.delegate.tableName} while ${table.tableName} expected"}
|
||||
val newFieldsMapping = row.fieldIndex.mapNotNull { (exp, _) ->
|
||||
val column = exp as? Column<*>
|
||||
val value = row[exp]
|
||||
val originalColumn = column?.let { alias.originalColumn(it) }
|
||||
when {
|
||||
originalColumn != null -> originalColumn to value
|
||||
column?.table == alias.delegate -> null
|
||||
else -> exp to value
|
||||
}
|
||||
}.toMap()
|
||||
return wrapRow(ResultRow.createAndFillValues(newFieldsMapping))
|
||||
}
|
||||
|
||||
fun wrapRow(row: ResultRow, alias: QueryAlias) : T {
|
||||
require(alias.columns.any { (it.table as Alias<*>).delegate == table }) { "QueryAlias doesn't have any column from ${table.tableName} table"}
|
||||
val originalColumns = alias.query.set.source.columns
|
||||
val newFieldsMapping = row.fieldIndex.mapNotNull { (exp, _) ->
|
||||
val value = row[exp]
|
||||
when {
|
||||
exp is Column && exp.table is Alias<*> -> {
|
||||
val delegate = (exp.table as Alias<*>).delegate
|
||||
val column = originalColumns.single {
|
||||
delegate == it.table && exp.name == it.name }
|
||||
column to value
|
||||
}
|
||||
exp is Column && exp.table == table -> null
|
||||
else -> exp to value
|
||||
}
|
||||
}.toMap()
|
||||
return wrapRow(ResultRow.createAndFillValues(newFieldsMapping))
|
||||
}
|
||||
|
||||
open fun all(): SizedIterable<T> = wrapRows(table.selectAll().notForUpdate())
|
||||
|
||||
/**
|
||||
* Get all the entities that conform to the [op] statement.
|
||||
*
|
||||
* @param op The statement to select the entities for. The statement must be of boolean type.
|
||||
*
|
||||
* @return All the entities that conform to the [op] statement.
|
||||
*/
|
||||
fun find(op: Op<Boolean>): SizedIterable<T> {
|
||||
warmCache()
|
||||
return wrapRows(searchQuery(op))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the entities that conform to the [op] statement.
|
||||
*
|
||||
* @param op The statement to select the entities for. The statement must be of boolean type.
|
||||
*
|
||||
* @return All the entities that conform to the [op] statement.
|
||||
*/
|
||||
fun find(op: SqlExpressionBuilder.()-> Op<Boolean>): SizedIterable<T> = find(SqlExpressionBuilder.op())
|
||||
|
||||
fun findWithCacheCondition(cacheCheckCondition: T.()->Boolean, op: SqlExpressionBuilder.()-> Op<Boolean>): Sequence<T> {
|
||||
val cached = testCache(cacheCheckCondition)
|
||||
return if (cached.any()) cached else find(op).asSequence()
|
||||
}
|
||||
|
||||
open val dependsOnTables: ColumnSet get() = table
|
||||
open val dependsOnColumns: List<Column<out Any?>> get() = dependsOnTables.columns
|
||||
|
||||
open fun searchQuery(op: Op<Boolean>): Query =
|
||||
dependsOnTables.slice(dependsOnColumns).select { op }.setForUpdateStatus()
|
||||
|
||||
/**
|
||||
* Count the amount of entities that conform to the [op] statement.
|
||||
*
|
||||
* @param op The statement to count the entities for. The statement must be of boolean type.
|
||||
*
|
||||
* @return The amount of entities that conform to the [op] statement.
|
||||
*/
|
||||
fun count(op: Op<Boolean>? = null): Int = with(TransactionManager.current()) {
|
||||
val query = table.slice(table.id.count())
|
||||
(if (op == null) query.selectAll() else query.select{op}).notForUpdate().first()[
|
||||
table.id.count()
|
||||
]
|
||||
}
|
||||
|
||||
protected open fun createInstance(entityId: EntityID<ID>, row: ResultRow?) : T = ctor.call(entityId) as T
|
||||
|
||||
fun wrap(id: EntityID<ID>, row: ResultRow?): T {
|
||||
val transaction = TransactionManager.current()
|
||||
return transaction.entityCache.find(this, id) ?: createInstance(id, row).also { new ->
|
||||
new.klass = this
|
||||
new.db = transaction.db
|
||||
warmCache().store(this, new)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new entity with the fields that are set in the [init] block. The id will be automatically set.
|
||||
*
|
||||
* @param init The block where the entities' fields can be set.
|
||||
*
|
||||
* @return The entity that has been created.
|
||||
*/
|
||||
open fun new(init: T.() -> Unit) = new(null, init)
|
||||
|
||||
/**
|
||||
* Create a new entity with the fields that are set in the [init] block and with a set [id].
|
||||
*
|
||||
* @param id The id of the entity. Set this to null if it should be automatically generated.
|
||||
* @param init The block where the entities' fields can be set.
|
||||
*
|
||||
* @return The entity that has been created.
|
||||
*/
|
||||
open fun new(id: ID?, init: T.() -> Unit): T {
|
||||
val entityId = if (id == null && table.id.defaultValueFun != null)
|
||||
table.id.defaultValueFun!!()
|
||||
else
|
||||
DaoEntityID(id, table)
|
||||
val prototype: T = createInstance(entityId, null)
|
||||
prototype.klass = this
|
||||
prototype.db = TransactionManager.current().db
|
||||
prototype._readValues = ResultRow.createAndFillDefaults(dependsOnColumns)
|
||||
if (entityId._value != null) {
|
||||
prototype.writeValues[table.id as Column<Any?>] = entityId
|
||||
warmCache().scheduleInsert(this, prototype)
|
||||
}
|
||||
prototype.init()
|
||||
if (entityId._value == null) {
|
||||
val readValues = prototype._readValues!!
|
||||
val writeValues = prototype.writeValues
|
||||
table.columns.filter { col ->
|
||||
col.defaultValueFun != null && col !in writeValues && readValues.hasValue(col)
|
||||
}.forEach { col ->
|
||||
writeValues[col as Column<Any?>] = readValues[col]
|
||||
}
|
||||
warmCache().scheduleInsert(this, prototype)
|
||||
}
|
||||
return prototype
|
||||
}
|
||||
|
||||
inline fun view (op: SqlExpressionBuilder.() -> Op<Boolean>) = View(SqlExpressionBuilder.op(), this)
|
||||
|
||||
private val refDefinitions = HashMap<Pair<Column<*>, KClass<*>>, Any>()
|
||||
|
||||
private inline fun <reified R: Any> registerRefRule(column: Column<*>, ref:()-> R): R =
|
||||
refDefinitions.getOrPut(column to R::class, ref) as R
|
||||
|
||||
infix fun <REF:Comparable<REF>> referencedOn(column: Column<REF>) = registerRefRule(column) { Reference(column, this) }
|
||||
|
||||
infix fun <REF:Comparable<REF>> optionalReferencedOn(column: Column<REF?>) = registerRefRule(column) { OptionalReference(column, this) }
|
||||
|
||||
infix fun <TargetID: Comparable<TargetID>, Target: Entity<TargetID>, REF:Comparable<REF>> EntityClass<TargetID, Target>.backReferencedOn(column: Column<REF>)
|
||||
: ReadOnlyProperty<Entity<ID>, Target> = registerRefRule(column) { BackReference(column, this) }
|
||||
|
||||
@JvmName("backReferencedOnOpt")
|
||||
infix fun <TargetID: Comparable<TargetID>, Target: Entity<TargetID>, REF:Comparable<REF>> EntityClass<TargetID, Target>.backReferencedOn(column: Column<REF?>)
|
||||
: ReadOnlyProperty<Entity<ID>, Target> = registerRefRule(column) { BackReference(column, this) }
|
||||
|
||||
infix fun <TargetID: Comparable<TargetID>, Target: Entity<TargetID>, REF:Comparable<REF>> EntityClass<TargetID, Target>.optionalBackReferencedOn(column: Column<REF>)
|
||||
= registerRefRule(column) { OptionalBackReference<TargetID, Target, ID, Entity<ID>, REF>(column as Column<REF?>, this) }
|
||||
|
||||
@JvmName("optionalBackReferencedOnOpt")
|
||||
infix fun <TargetID: Comparable<TargetID>, Target: Entity<TargetID>, REF:Comparable<REF>> EntityClass<TargetID, Target>.optionalBackReferencedOn(column: Column<REF?>)
|
||||
= registerRefRule(column) { OptionalBackReference<TargetID, Target, ID, Entity<ID>, REF>(column, this) }
|
||||
|
||||
infix fun <TargetID: Comparable<TargetID>, Target: Entity<TargetID>, REF: Comparable<REF>> EntityClass<TargetID, Target>.referrersOn(column: Column<REF>)
|
||||
= registerRefRule(column) { Referrers<ID, Entity<ID>, TargetID, Target, REF>(column, this, false) }
|
||||
|
||||
fun <TargetID: Comparable<TargetID>, Target: Entity<TargetID>, REF: Comparable<REF>> EntityClass<TargetID, Target>.referrersOn(column: Column<REF>, cache: Boolean)
|
||||
= registerRefRule(column) { Referrers<ID, Entity<ID>, TargetID, Target, REF>(column, this, cache) }
|
||||
|
||||
infix fun <TargetID: Comparable<TargetID>, Target: Entity<TargetID>, REF: Comparable<REF>> EntityClass<TargetID, Target>.optionalReferrersOn(column : Column<REF?>)
|
||||
= registerRefRule(column) { OptionalReferrers<ID, Entity<ID>, TargetID, Target, REF>(column, this, false) }
|
||||
|
||||
fun <TargetID: Comparable<TargetID>, Target: Entity<TargetID>, REF: Comparable<REF>> EntityClass<TargetID, Target>.optionalReferrersOn(column: Column<REF?>, cache: Boolean = false) =
|
||||
registerRefRule(column) { OptionalReferrers<ID, Entity<ID>, TargetID, Target, REF>(column, this, cache) }
|
||||
|
||||
fun<TColumn: Any?,TReal: Any?> Column<TColumn>.transform(toColumn: (TReal) -> TColumn, toReal: (TColumn) -> TReal): ColumnWithTransform<TColumn, TReal> = ColumnWithTransform(this, toColumn, toReal)
|
||||
|
||||
private fun Query.setForUpdateStatus(): Query = if (this@EntityClass is ImmutableEntityClass<*, *>) this.notForUpdate() else this
|
||||
|
||||
@Suppress("CAST_NEVER_SUCCEEDS")
|
||||
fun <SID> warmUpOptReferences(references: List<SID>, refColumn: Column<SID?>, forUpdate: Boolean? = null): List<T>
|
||||
= warmUpReferences(references, refColumn as Column<SID>, forUpdate)
|
||||
|
||||
fun <SID> warmUpReferences(references: List<SID>, refColumn: Column<SID>, forUpdate: Boolean? = null): List<T> {
|
||||
val parentTable = refColumn.referee?.table as? IdTable<*>
|
||||
requireNotNull(parentTable) { "RefColumn should have reference to IdTable" }
|
||||
if (references.isEmpty()) return emptyList()
|
||||
val distinctRefIds = references.distinct()
|
||||
val cache = TransactionManager.current().entityCache
|
||||
if (refColumn.columnType is EntityIDColumnType<*>) {
|
||||
refColumn as Column<EntityID<*>>
|
||||
distinctRefIds as List<EntityID<ID>>
|
||||
val toLoad = distinctRefIds.filter {
|
||||
cache.referrers[it]?.containsKey(refColumn)?.not() ?: true
|
||||
}
|
||||
if (toLoad.isNotEmpty()) {
|
||||
val findQuery = find { refColumn inList toLoad }
|
||||
val entities = when(forUpdate) {
|
||||
true -> findQuery.forUpdate()
|
||||
false -> findQuery.notForUpdate()
|
||||
else -> findQuery
|
||||
}.toList()
|
||||
|
||||
val result = entities.groupBy { it.readValues[refColumn] }
|
||||
|
||||
distinctRefIds.forEach { id ->
|
||||
cache.getOrPutReferrers(id, refColumn) { result[id]?.let { SizedCollection(it) } ?: emptySized<T>() }
|
||||
}
|
||||
}
|
||||
|
||||
return distinctRefIds.flatMap { cache.referrers[it]?.get(refColumn)?.toList().orEmpty() } as List<T>
|
||||
} else {
|
||||
val baseQuery = searchQuery(Op.build{ refColumn inList distinctRefIds })
|
||||
val finalQuery = if (parentTable.id in baseQuery.set.fields)
|
||||
baseQuery
|
||||
else {
|
||||
baseQuery.adjustSlice{ slice(this.fields + parentTable.id) }.
|
||||
adjustColumnSet { innerJoin(parentTable, { refColumn }, { refColumn.referee!! }) }
|
||||
}
|
||||
|
||||
val findQuery = wrapRows(finalQuery)
|
||||
val entities = when(forUpdate) {
|
||||
true -> findQuery.forUpdate()
|
||||
false -> findQuery.notForUpdate()
|
||||
else -> findQuery
|
||||
}.toList().distinct()
|
||||
|
||||
entities.groupBy { it.readValues[parentTable.id] }.forEach { (id, values) ->
|
||||
cache.getOrPutReferrers(id, refColumn) { SizedCollection(values) }
|
||||
}
|
||||
return entities
|
||||
}
|
||||
}
|
||||
|
||||
fun warmUpLinkedReferences(references: List<EntityID<*>>, linkTable: Table, forUpdate: Boolean? = null): List<T> {
|
||||
if (references.isEmpty()) return emptyList()
|
||||
val distinctRefIds = references.distinct()
|
||||
val sourceRefColumn = linkTable.columns.singleOrNull { it.referee == references.first().table.id } as? Column<EntityID<*>>
|
||||
?: error("Can't detect source reference column")
|
||||
val targetRefColumn = linkTable.columns.singleOrNull {it.referee == table.id} as? Column<EntityID<*>> ?: error("Can't detect target reference column")
|
||||
|
||||
val transaction = TransactionManager.current()
|
||||
|
||||
val inCache = transaction.entityCache.referrers.filter { it.key in distinctRefIds && sourceRefColumn in it.value }.mapValues { it.value[sourceRefColumn]!! }
|
||||
val loaded = (distinctRefIds - inCache.keys).takeIf { it.isNotEmpty() }?.let { idsToLoad ->
|
||||
val alreadyInJoin = (dependsOnTables as? Join)?.alreadyInJoin(linkTable) ?: false
|
||||
val entityTables = if (alreadyInJoin) dependsOnTables else dependsOnTables.join(linkTable, JoinType.INNER, targetRefColumn, table.id)
|
||||
|
||||
val columns = (dependsOnColumns + (if (!alreadyInJoin) linkTable.columns else emptyList())
|
||||
- sourceRefColumn).distinct() + sourceRefColumn
|
||||
|
||||
val query = entityTables.slice(columns).select { sourceRefColumn inList idsToLoad }
|
||||
val entitiesWithRefs = when(forUpdate) {
|
||||
true -> query.forUpdate()
|
||||
false -> query.notForUpdate()
|
||||
else -> query
|
||||
}.map { it[sourceRefColumn] to wrapRow(it) }
|
||||
|
||||
val groupedBySourceId = entitiesWithRefs.groupBy { it.first }.mapValues { it.value.map { it.second } }
|
||||
|
||||
idsToLoad.forEach {
|
||||
transaction.entityCache.getOrPutReferrers(it, sourceRefColumn) { SizedCollection(groupedBySourceId[it] ?: emptyList()) }
|
||||
}
|
||||
entitiesWithRefs.map { it.second }
|
||||
}
|
||||
return inCache.values.flatMap { it.toList() as List<T> } + loaded.orEmpty()
|
||||
}
|
||||
|
||||
fun <ID : Comparable<ID>, T: Entity<ID>> isAssignableTo(entityClass: EntityClass<ID, T>) = entityClass.klass.isAssignableFrom(klass)
|
||||
}
|
||||
|
||||
abstract class ImmutableEntityClass<ID:Comparable<ID>, out T: Entity<ID>>(table: IdTable<ID>, entityType: Class<T>? = null) : EntityClass<ID, T>(table, entityType) {
|
||||
open fun <T> forceUpdateEntity(entity: Entity<ID>, column: Column<T>, value: T) {
|
||||
table.update({ table.id eq entity.id }) {
|
||||
it[column] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ImmutableCachedEntityClass<ID:Comparable<ID>, out T: Entity<ID>>(table: IdTable<ID>, entityType: Class<T>? = null) : ImmutableEntityClass<ID, T>(table, entityType) {
|
||||
|
||||
private val cacheLoadingState = Key<Any>()
|
||||
private var _cachedValues: MutableMap<Database, MutableMap<Any, Entity<*>>> = ConcurrentHashMap()
|
||||
|
||||
override fun invalidateEntityInCache(o: Entity<ID>) {
|
||||
warmCache()
|
||||
}
|
||||
|
||||
final override fun warmCache(): EntityCache {
|
||||
val tr = TransactionManager.current()
|
||||
val db = tr.db
|
||||
val transactionCache = super.warmCache()
|
||||
if (_cachedValues[db] == null) synchronized(this) {
|
||||
val cachedValues = _cachedValues[db]
|
||||
when {
|
||||
cachedValues != null -> {} // already loaded in another transaction
|
||||
tr.getUserData(cacheLoadingState) != null -> {
|
||||
return transactionCache // prevent recursive call to warmCache() in .all()
|
||||
}
|
||||
else -> {
|
||||
tr.putUserData(cacheLoadingState, this)
|
||||
super.all().toList() /* force iteration to initialize lazy collection */
|
||||
_cachedValues[db] = transactionCache.data[table] ?: mutableMapOf()
|
||||
tr.removeUserData(cacheLoadingState)
|
||||
}
|
||||
}
|
||||
}
|
||||
transactionCache.data[table] = _cachedValues[db]!!
|
||||
return transactionCache
|
||||
}
|
||||
|
||||
override fun all(): SizedIterable<T> = SizedCollection(warmCache().findAll(this))
|
||||
|
||||
@Synchronized fun expireCache() {
|
||||
if (TransactionManager.isInitialized() && TransactionManager.currentOrNull() != null) {
|
||||
_cachedValues.remove(TransactionManager.current().db)
|
||||
} else {
|
||||
_cachedValues.clear()
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T> forceUpdateEntity(entity: Entity<ID>, column: Column<T>, value: T) {
|
||||
super.forceUpdateEntity(entity, column, value)
|
||||
entity._readValues?.set(column, value)
|
||||
expireCache()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.jetbrains.exposed.dao
|
||||
|
||||
import org.jetbrains.exposed.sql.Query
|
||||
import org.jetbrains.exposed.sql.Transaction
|
||||
import org.jetbrains.exposed.sql.statements.*
|
||||
import org.jetbrains.exposed.sql.targetTables
|
||||
|
||||
object EntityLifecycleInterceptor : StatementInterceptor, EntityIDFactory {
|
||||
init {
|
||||
Transaction.registerGlobalIntercepter(this)
|
||||
EntityIDFunctionProvider.factory = this
|
||||
}
|
||||
|
||||
override fun <T : Comparable<T>> createEntityID(value: T, table: IdTable<T>): EntityID<T> {
|
||||
return DaoEntityID(value, table)
|
||||
}
|
||||
override fun beforeExecution(transaction: Transaction, context: StatementContext) {
|
||||
when (val statement = context.statement) {
|
||||
is Query -> transaction.flushEntities(statement)
|
||||
|
||||
is DeleteStatement -> {
|
||||
transaction.flushCache()
|
||||
transaction.entityCache.removeTablesReferrers(listOf(statement.table))
|
||||
}
|
||||
|
||||
is InsertStatement<*> -> {
|
||||
if (statement.flushCache)
|
||||
transaction.flushCache()
|
||||
transaction.entityCache.removeTablesReferrers(listOf(statement.table))
|
||||
}
|
||||
|
||||
is BatchUpdateStatement -> {}
|
||||
|
||||
is UpdateStatement -> {
|
||||
transaction.flushCache()
|
||||
transaction.entityCache.removeTablesReferrers(statement.targetsSet.targetTables())
|
||||
}
|
||||
|
||||
else -> {
|
||||
if(statement.type.group == StatementGroup.DDL)
|
||||
transaction.flushCache()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun beforeCommit(transaction: Transaction) {
|
||||
val created = transaction.flushCache()
|
||||
EntityHook.alertSubscribers(transaction)
|
||||
val createdByHooks = transaction.flushCache()
|
||||
EntityCache.invalidateGlobalCaches(created + createdByHooks)
|
||||
}
|
||||
|
||||
override fun beforeRollback(transaction: Transaction) {
|
||||
val entityCache = transaction.entityCache
|
||||
entityCache.clearReferrersCache()
|
||||
entityCache.data.clear()
|
||||
entityCache.inserts.clear()
|
||||
}
|
||||
|
||||
private fun Transaction.flushEntities(query: Query) {
|
||||
// Flush data before executing query or results may be unpredictable
|
||||
val tables = query.set.source.columns.map { it.table }.filterIsInstance(IdTable::class.java).toSet()
|
||||
entityCache.flush(tables)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.jetbrains.exposed.dao
|
||||
|
||||
|
||||
abstract class IntEntity(id: EntityID<Int>) : Entity<Int>(id)
|
||||
|
||||
abstract class IntEntityClass<out E: IntEntity>(table: IdTable<Int>, entityType: Class<E>? = null) : EntityClass<Int, E>(table, entityType)
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.jetbrains.exposed.dao
|
||||
|
||||
abstract class LongEntity(id: EntityID<Long>) : Entity<Long>(id)
|
||||
|
||||
abstract class LongEntityClass<out E: LongEntity>(table: IdTable<Long>, entityType: Class<E>? = null) : EntityClass<Long, E>(table, entityType)
|
||||
|
||||
@@ -30,21 +30,21 @@ class OptionalReference<REF:Comparable<REF>, ID:Comparable<ID>, out Target : Ent
|
||||
}
|
||||
}
|
||||
|
||||
internal class BackReference<ParentID:Comparable<ParentID>, out Parent:Entity<ParentID>, ChildID:Comparable<ChildID>, in Child:Entity<ChildID>, REF>
|
||||
internal class BackReference<ParentID:Comparable<ParentID>, out Parent: Entity<ParentID>, ChildID:Comparable<ChildID>, in Child: Entity<ChildID>, REF>
|
||||
(reference: Column<REF>, factory: EntityClass<ParentID, Parent>) : ReadOnlyProperty<Child, Parent> {
|
||||
internal val delegate = Referrers<ChildID, Child, ParentID, Parent, REF>(reference, factory, true)
|
||||
|
||||
override operator fun getValue(thisRef: Child, property: KProperty<*>) = delegate.getValue(thisRef.apply { thisRef.id.value }, property).single() // flush entity before to don't miss newly created entities
|
||||
}
|
||||
|
||||
class OptionalBackReference<ParentID:Comparable<ParentID>, out Parent:Entity<ParentID>, ChildID:Comparable<ChildID>, in Child:Entity<ChildID>, REF>
|
||||
class OptionalBackReference<ParentID:Comparable<ParentID>, out Parent: Entity<ParentID>, ChildID:Comparable<ChildID>, in Child: Entity<ChildID>, REF>
|
||||
(reference: Column<REF?>, factory: EntityClass<ParentID, Parent>) : ReadOnlyProperty<Child, Parent?> {
|
||||
internal val delegate = OptionalReferrers<ChildID, Child, ParentID, Parent, REF>(reference, factory, true)
|
||||
|
||||
override operator fun getValue(thisRef: Child, property: KProperty<*>) = delegate.getValue(thisRef.apply { thisRef.id.value }, property).singleOrNull() // flush entity before to don't miss newly created entities
|
||||
}
|
||||
|
||||
class Referrers<ParentID:Comparable<ParentID>, in Parent:Entity<ParentID>, ChildID:Comparable<ChildID>, out Child:Entity<ChildID>, REF>
|
||||
class Referrers<ParentID:Comparable<ParentID>, in Parent: Entity<ParentID>, ChildID:Comparable<ChildID>, out Child: Entity<ChildID>, REF>
|
||||
(val reference: Column<REF>, val factory: EntityClass<ChildID, Child>, val cache: Boolean) : ReadOnlyProperty<Parent, SizedIterable<Child>> {
|
||||
init {
|
||||
reference.referee ?: error("Column $reference is not a reference")
|
||||
@@ -63,7 +63,7 @@ class Referrers<ParentID:Comparable<ParentID>, in Parent:Entity<ParentID>, Child
|
||||
}
|
||||
}
|
||||
|
||||
class OptionalReferrers<ParentID:Comparable<ParentID>, in Parent:Entity<ParentID>, ChildID:Comparable<ChildID>, out Child:Entity<ChildID>, REF>
|
||||
class OptionalReferrers<ParentID:Comparable<ParentID>, in Parent: Entity<ParentID>, ChildID:Comparable<ChildID>, out Child: Entity<ChildID>, REF>
|
||||
(val reference: Column<REF?>, val factory: EntityClass<ChildID, Child>, val cache: Boolean) : ReadOnlyProperty<Parent, SizedIterable<Child>> {
|
||||
init {
|
||||
reference.referee ?: error("Column $reference is not a reference")
|
||||
@@ -125,13 +125,13 @@ private fun <ID: Comparable<ID>> List<Entity<ID>>.preloadRelations(vararg relati
|
||||
refObject.factory.warmUpReferences(refIds, refColumn)
|
||||
}
|
||||
}
|
||||
is OptionalReferrers<*,*,*,*,*> -> {
|
||||
is OptionalReferrers<*, *, *, *, *> -> {
|
||||
(refObject as OptionalReferrers<ID, Entity<ID>, *, Entity<*>, Any>).reference.let { refColumn ->
|
||||
val refIds = this.mapNotNull { it.run { refColumn.referee<Any?>()!!.lookup() } }
|
||||
refObject.factory.warmUpOptReferences(refIds, refColumn)
|
||||
}
|
||||
}
|
||||
is InnerTableLink<*,*,*,*> -> {
|
||||
is InnerTableLink<*, *, *, *> -> {
|
||||
refObject.target.warmUpLinkedReferences(this.map{ it.id }, refObject.table)
|
||||
}
|
||||
is BackReference<*, *, *, *, *> -> {
|
||||
@@ -156,7 +156,7 @@ private fun <ID: Comparable<ID>> List<Entity<ID>>.preloadRelations(vararg relati
|
||||
val relationsToLoad = this.flatMap {
|
||||
when(val relation = (relationProperty as KProperty1<Entity<*>, *>).get(it)) {
|
||||
is SizedIterable<*> -> relation.toList()
|
||||
is Entity<*> -> listOf(relation)
|
||||
is Entity<*> -> listOf(relation)
|
||||
null -> listOf()
|
||||
else -> error("Unrecognised loaded relation")
|
||||
} as List<Entity<Int>>
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.jetbrains.exposed.dao
|
||||
|
||||
import java.util.*
|
||||
|
||||
abstract class UUIDEntity(id: EntityID<UUID>) : Entity<UUID>(id)
|
||||
|
||||
abstract class UUIDEntityClass<out E: UUIDEntity>(table: IdTable<UUID>, entityType: Class<E>? = null) : EntityClass<UUID, E>(table, entityType)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.jetbrains.exposed.dao.exceptions
|
||||
|
||||
import org.jetbrains.exposed.dao.EntityClass
|
||||
import org.jetbrains.exposed.dao.EntityID
|
||||
|
||||
class EntityNotFoundException(val id: EntityID<*>, val entity: EntityClass<*, *>)
|
||||
: Exception("Entity ${entity.klass.simpleName}, id=$id not found in the database")
|
||||
@@ -14,7 +14,9 @@ val dialect: String by project
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlinx", "kotlinx-coroutines-core", "1.3.0-M1")
|
||||
implementation(project(":exposed-core"))
|
||||
implementation(project(":exposed-jdbc"))
|
||||
implementation(project(":exposed-dao"))
|
||||
implementation(kotlin("test-junit"))
|
||||
implementation("org.slf4j", "slf4j-log4j12", "1.7.26")
|
||||
implementation("log4j", "log4j", "1.2.17")
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.mysql.management.driverlaunched.ServerLauncherSocketFactory
|
||||
import com.mysql.management.util.Files
|
||||
import com.opentable.db.postgres.embedded.EmbeddedPostgres
|
||||
import org.h2.engine.Mode
|
||||
import org.jetbrains.exposed.dao.EntityLifecycleInterceptor
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
@@ -96,6 +97,7 @@ private val postgresSQLProcess by lazy {
|
||||
abstract class DatabaseTestsBase {
|
||||
init {
|
||||
TimeZone.setDefault(TimeZone.getTimeZone("UTC"))
|
||||
EntityLifecycleInterceptor // register it
|
||||
}
|
||||
fun withDb(dbSettings: TestDB, statement: Transaction.() -> Unit) {
|
||||
if (dbSettings !in TestDB.enabledInTests()) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package org.jetbrains.exposed.sql.tests.h2
|
||||
|
||||
import org.jetbrains.exposed.dao.EntityID
|
||||
import org.jetbrains.exposed.dao.SimpleEntityID
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.jetbrains.exposed.sql.SchemaUtils
|
||||
import org.jetbrains.exposed.sql.tests.shared.EntityTestsData
|
||||
@@ -170,7 +170,7 @@ class MultiDatabaseEntityTest {
|
||||
commit()
|
||||
|
||||
transaction(db2) {
|
||||
assertNull(EntityTestsData.BEntity.testCache(EntityID(2, EntityTestsData.BEntity.table)))
|
||||
assertNull(EntityTestsData.BEntity.testCache(SimpleEntityID(2, EntityTestsData.BEntity.table)))
|
||||
val b2Reread = EntityTestsData.BEntity.all().single()
|
||||
assertEquals(db2b1.id, b2Reread.id)
|
||||
assertEquals(db2y1.id, b2Reread.y?.id)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.jetbrains.exposed.sql.tests.shared
|
||||
|
||||
import org.jetbrains.exposed.dao.UUIDTable
|
||||
import org.jetbrains.exposed.dao.entityCache
|
||||
import org.jetbrains.exposed.dao.flushCache
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.tests.DatabaseTestsBase
|
||||
import org.junit.Test
|
||||
|
||||
@@ -869,7 +869,7 @@ class DMLTests : DatabaseTestsBase() {
|
||||
listOf(TestDB.SQLITE, TestDB.MYSQL, TestDB.H2_MYSQL, TestDB.POSTGRESQL)
|
||||
withTables(insertIgnoreSupportedDB, idTable) {
|
||||
val id = idTable.insertIgnore {
|
||||
it[idTable.id] = EntityID(1, idTable)
|
||||
it[idTable.id] = SimpleEntityID(1, idTable)
|
||||
it[idTable.name] = "1"
|
||||
} get idTable.id
|
||||
assertEquals(1, id?.value)
|
||||
@@ -1220,14 +1220,14 @@ class DMLTests : DatabaseTestsBase() {
|
||||
val name = varchar("name", 10)
|
||||
}
|
||||
withTables(stringTable) {
|
||||
val entityID = EntityID("id1", stringTable)
|
||||
val entityID = SimpleEntityID("id1", stringTable)
|
||||
val id1 = stringTable.insertAndGetId {
|
||||
it[id] = entityID
|
||||
it[name] = "foo"
|
||||
}
|
||||
|
||||
stringTable.insertAndGetId {
|
||||
it[id] = EntityID("testId", stringTable)
|
||||
it[id] = SimpleEntityID("testId", stringTable)
|
||||
it[name] = "bar"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.jetbrains.exposed.sql.tests.shared
|
||||
|
||||
import org.jetbrains.exposed.dao.*
|
||||
import org.jetbrains.exposed.exceptions.EntityNotFoundException
|
||||
import org.jetbrains.exposed.dao.exceptions.EntityNotFoundException
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.statements.api.ExposedBlob
|
||||
import org.jetbrains.exposed.sql.tests.DatabaseTestsBase
|
||||
@@ -19,7 +19,7 @@ object EntityTestsData {
|
||||
|
||||
object YTable: IdTable<String>("YTable") {
|
||||
override val id: Column<EntityID<String>> = varchar("uuid", 36).primaryKey().entityId().clientDefault {
|
||||
EntityID(UUID.randomUUID().toString(), YTable)
|
||||
SimpleEntityID(UUID.randomUUID().toString(), YTable)
|
||||
}
|
||||
|
||||
val x = bool("x").default(true)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
rootProject.name = "exposed"
|
||||
include("exposed-core")
|
||||
include("exposed-dao")
|
||||
include("exposed-jodatime")
|
||||
include("exposed-java-time")
|
||||
include("spring-transaction")
|
||||
include("exposed-spring-boot-starter")
|
||||
include("exposed-jdbc")
|
||||
include("exposed-tests")
|
||||
include("exposed-tests")
|
||||
|
||||
Reference in New Issue
Block a user