exposed-dao module introduced

This commit is contained in:
Tapac
2019-10-19 02:00:36 +03:00
parent 96e84e2cf2
commit a9e33bd8b2
42 changed files with 1229 additions and 1067 deletions

View File

@@ -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()
}
}

View File

@@ -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() {}
}

View File

@@ -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()
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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>)
}
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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
}
}
}
}

View File

@@ -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)

View File

@@ -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()

View File

@@ -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>> {

View File

@@ -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
}
}
}

View File

@@ -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)!!
}
}
}
}
}

View File

@@ -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()
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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 =

View File

@@ -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?) {

View 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)

View File

@@ -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)
}
}

View 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()
}
}

View File

@@ -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)!!
}
}
}
}

View File

@@ -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
}
}

View File

@@ -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()
}
}

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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>>

View File

@@ -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)

View File

@@ -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")

View File

@@ -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")

View File

@@ -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()) {

View File

@@ -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)

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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)

View File

@@ -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")