All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jetbrains.exposed.dao.Entity.kt Maven / Gradle / Ivy

There is a newer version: 0.57.0
Show newest version
package org.jetbrains.exposed.dao

import org.jetbrains.exposed.dao.exceptions.EntityNotFoundException
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.TransactionManager
import kotlin.properties.Delegates
import kotlin.reflect.KProperty

open class ColumnWithTransform(
    val column: Column,
    val toColumn: (TReal) -> TColumn,
    toReal: (TColumn) -> TReal,
    protected val cacheResult: Boolean = false
) {
    private var cache: Pair? = null

    val toReal: (TColumn) -> TReal = { columnValue ->
        if (cacheResult) {
            val localCache = cache
            if (localCache != null && localCache.first == columnValue) {
                localCache.second
            } else {
                toReal(columnValue).also { cache = columnValue to it }
            }
        } else {
            toReal(columnValue)
        }
    }
}

open class Entity>(val id: EntityID) {
    var klass: EntityClass> by Delegates.notNull()
        internal set

    var db: Database by Delegates.notNull()
        internal set

    val writeValues = LinkedHashMap, 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!!
        }

    private val referenceCache by lazy { HashMap, Any?>() }

    internal fun isNewEntity(): Boolean {
        val cache = TransactionManager.current().entityCache
        return cache.inserts[klass.table]?.contains(this) ?: false
    }

    /**
     * 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) {
        val transaction = TransactionManager.current()
        val cache = transaction.entityCache
        val isNewEntity = isNewEntity()
        when {
            isNewEntity && flush -> cache.flushInserts(klass.table)
            flush -> flush()
            isNewEntity -> throw EntityNotFoundException(this.id, this.klass)
            else -> writeValues.clear()
        }

        klass.removeFromCache(this)
        val reloaded = klass[id]
        cache.store(this)
        _readValues = reloaded.readValues
        db = transaction.db
    }

    internal fun  getReferenceFromCache(ref: Column<*>): T {
        return referenceCache[ref] as T
    }

    internal fun storeReferenceInCache(ref: Column<*>, value: Any?) {
        if (db.config.keepLoadedReferencesOutOfTransaction) {
            referenceCache[ref] = value
        }
    }

    operator fun , RID : Comparable, T : Entity> Reference.getValue(
        o: Entity,
        desc: KProperty<*>
    ): T {
        val outOfTransaction = TransactionManager.currentOrNull() == null
        if (outOfTransaction && reference in referenceCache) return getReferenceFromCache(reference)
        return executeAsPartOfEntityLifecycle {
            val refValue = reference.getValue(o, desc)
            when {
                refValue is EntityID<*> && reference.referee() == factory.table.id -> {
                    factory.findById(refValue.value as RID).also {
                        storeReferenceInCache(reference, it)
                    }
                }
                else -> {
                    // @formatter:off
                    factory.findWithCacheCondition({
                       reference.referee!!.getValue(this, desc) == refValue
                    }) {
                        reference.referee()!! eq refValue
                    }.singleOrNull()?.also {
                        storeReferenceInCache(reference, it)
                    }
                    // @formatter:on
                }
            } ?: error("Cannot find ${factory.table.tableName} WHERE id=$refValue")
        }
    }

    operator fun , RID : Comparable, T : Entity> Reference.setValue(
        o: Entity,
        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()!!.getValue(this, desc) }
        storeReferenceInCache(reference, value)
        reference.setValue(o, desc, refValue)
    }

    operator fun , RID : Comparable, T : Entity> OptionalReference.getValue(
        o: Entity,
        desc: KProperty<*>
    ): T? {
        val outOfTransaction = TransactionManager.currentOrNull() == null
        if (outOfTransaction && reference in referenceCache) return getReferenceFromCache(reference)
        return executeAsPartOfEntityLifecycle {
            val refValue = reference.getValue(o, desc)
            when {
                refValue == null -> null
                refValue is EntityID<*> && reference.referee() == factory.table.id -> {
                    factory.findById(refValue.value as RID).also {
                        storeReferenceInCache(reference, it)
                    }
                }
                else -> {
                    // @formatter:off
                   factory.findWithCacheCondition({
                       reference.referee!!.getValue(this, desc) == refValue
                   }) {
                       reference.referee()!! eq refValue
                   }.singleOrNull().also {
                       storeReferenceInCache(reference, it)
                   }
                    // @formatter:on
                }
            }
        }
    }

    operator fun , RID : Comparable, T : Entity> OptionalReference.setValue(
        o: Entity,
        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()!!.getValue(this, desc) }
        storeReferenceInCache(reference, value)
        reference.setValue(o, desc, refValue)
    }

    operator fun  Column.getValue(o: Entity, desc: KProperty<*>): T = lookup()

    operator fun  CompositeColumn.getValue(o: Entity, desc: KProperty<*>): T {
        val values = this.getRealColumns().associateWith { it.lookup() }
        return this.restoreValueFromParts(values)
    }

    @Suppress("UNCHECKED_CAST")
    fun  Column.lookupInReadValues(found: (T?) -> R?, notFound: () -> R?): R? =
        if (_readValues?.hasValue(this) == true) {
            found(readValues[this])
        } else {
            notFound()
        }

    @Suppress("UNCHECKED_CAST", "USELESS_CAST")
    fun  Column.lookup(): T = when {
        writeValues.containsKey(this as Column) -> writeValues[this as Column] as T
        id._value == null && _readValues?.hasValue(this)?.not() ?: true -> defaultValueFun?.invoke() as T
        columnType.nullable -> readValues[this]
        else -> readValues[this]!!
    }

    operator fun  Column.setValue(o: Entity, desc: KProperty<*>, value: T) {
        klass.invalidateEntityInCache(o)
        val currentValue = _readValues?.getOrNull(this)
        if (writeValues.containsKey(this as Column) || currentValue != value) {
            val entityCache = TransactionManager.current().entityCache
            if (referee != null) {
                if (value is EntityID<*> && value.table == referee!!.table) value.value // flush

                listOfNotNull(value, currentValue).forEach {
                    entityCache.referrers[this]?.remove(it)
                }
            }
            writeValues[this as Column] = value
            if (entityCache.data[table].orEmpty().contains(o.id._value)) {
                entityCache.scheduleUpdate(klass, o)
            }
        }
    }

    operator fun  CompositeColumn.setValue(o: Entity, desc: KProperty<*>, value: T) {
        with(o) {
            [email protected](value).forEach {
                (it.key as Column).setValue(o, desc, it.value)
            }
        }
    }

    operator fun  ColumnWithTransform.getValue(o: Entity, desc: KProperty<*>): TReal =
        toReal(column.getValue(o, desc))

    operator fun  ColumnWithTransform.setValue(o: Entity, desc: KProperty<*>, value: TReal) {
        column.setValue(o, desc, toColumn(value))
    }

    infix fun , Target : Entity> EntityClass.via(table: Table): InnerTableLink, TID, Target> =
        InnerTableLink(table, [email protected], this@via)

    fun , Target : Entity> EntityClass.via(
        sourceColumn: Column>,
        targetColumn: Column>
    ) = InnerTableLink(sourceColumn.table, [email protected], this@via, sourceColumn, targetColumn)

    /**
     * Delete this entity.
     *
     * This will remove the entity from the database as well as the cache.
     */
    open fun delete() {
        val table = klass.table
        // Capture reference to the field
        val entityId = this.id
        TransactionManager.current().registerChange(klass, entityId, EntityChangeType.Removed)
        executeAsPartOfEntityLifecycle {
            table.deleteWhere { table.id eq entityId }
        }
        klass.removeFromCache(this)
    }

    open fun flush(batch: EntityBatchUpdate? = null): Boolean {
        if (isNewEntity()) {
            TransactionManager.current().entityCache.flushInserts(this.klass.table)
            return true
        }
        if (writeValues.isNotEmpty()) {
            if (batch == null) {
                val table = klass.table
                // Store values before update to prevent flush inside UpdateStatement
                val _writeValues = writeValues.toMap()
                storeWrittenValues()
                // In case of batch all changes will be registered after all entities flushed
                TransactionManager.current().registerChange(klass, id, EntityChangeType.Updated)
                executeAsPartOfEntityLifecycle {
                    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()
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy