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.56.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

/**
 * Class responsible for enabling [Entity] field transformations, which may be useful when advanced database
 * type conversions are necessary for entity mappings.
 */
open class ColumnWithTransform(
    /** The original column type used in the transformation. */
    val column: Column,
    /** The function used to convert a transformed value to a value that can be stored in the original column type. */
    val toColumn: (TReal) -> TColumn,
    toReal: (TColumn) -> TReal,
    /** Whether the original and transformed values should be cached to avoid multiple conversion calls. */
    protected val cacheResult: Boolean = false
) {
    private var cache: Pair? = null

    /** The function used to transform a value stored in the original column type. */
    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)
        }
    }
}

/**
 * Class representing a mapping to values stored in a table record in a database.
 *
 * @param id The unique stored identity value for the mapped record.
 */
open class Entity>(val id: EntityID) {
    /** The associated [EntityClass] that manages this [Entity] instance. */
    var klass: EntityClass> by Delegates.notNull()
        internal set

    /** The [Database] associated with the record mapped to this [Entity] instance. */
    var db: Database by Delegates.notNull()
        internal set

    /** The initial column-value mapping for this [Entity] instance before being flushed and inserted into the database. */
    val writeValues = LinkedHashMap, Any?>()

    @Suppress("VariableNaming")
    var _readValues: ResultRow? = null

    /** The final column-value mapping for this [Entity] instance after being flushed and retrieved from the database. */
    val readValues: ResultRow
        get() = _readValues ?: run {
            val table = klass.table
            _readValues = klass.searchQuery(Op.build { table.id eq id }).firstOrNull()
                ?: table.selectAll().where { 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 the fields of this [Entity] instance with values retrieved from the database.
     * Override this function to refresh some additional state, if any.
     *
     * @param flush Whether pending entity changes should be flushed prior to updating.
     * @throws EntityNotFoundException If the entity no longer exists in the database.
     * @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.testNewWithIdAndRefresh
     */
    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)
    }

    /**
     * Checks if this column has been assigned a value retrieved from the database, then calls the [found] block
     * with this value as its argument, and returns its result.
     *
     * If a column-value mapping has not been retrieved, the result of calling the [notFound] block is returned instead.
     */
    fun  Column.lookupInReadValues(found: (T?) -> R?, notFound: () -> R?): R? =
        if (_readValues?.hasValue(this) == true) {
            found(readValues[this])
        } else {
            notFound()
        }

    /**
     * Returns the value assigned to this column mapping.
     *
     * Depending on the state of this [Entity] instance, the value returned may be the initial property assignment,
     * this column's default value, or the value retrieved from the database.
     */
    @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))
    }

    /**
     * Registers a reference as a field of the child entity class, which returns a parent object of this [EntityClass],
     * for use in many-to-many relations.
     *
     * The reference should have been defined by the creation of a column using `reference()` on an intermediate table.
     *
     * @param table The intermediate table containing reference columns to both child and parent objects.
     * @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityHookTestData.User
     * @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityHookTestData.City
     * @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityHookTestData.UsersToCities
     */
    infix fun , Target : Entity> EntityClass.via(
        table: Table
    ): InnerTableLink, TID, Target> =
        InnerTableLink(table, [email protected], this@via)

    /**
     * Registers a reference as a field of the child entity class, which returns a parent object of this [EntityClass],
     * for use in many-to-many relations.
     *
     * The reference should have been defined by the creation of a column using `reference()` on an intermediate table.
     *
     * @param sourceColumn The intermediate table's reference column for the child entity class.
     * @param targetColumn The intermediate table's reference column for the parent entity class.
     * @sample org.jetbrains.exposed.sql.tests.shared.entities.ViaTests.NodesTable
     * @sample org.jetbrains.exposed.sql.tests.shared.entities.ViaTests.Node
     * @sample org.jetbrains.exposed.sql.tests.shared.entities.ViaTests.NodeToNodes
     */
    fun , Target : Entity> EntityClass.via(
        sourceColumn: Column>,
        targetColumn: Column>
    ) = InnerTableLink(sourceColumn.table, [email protected], this@via, sourceColumn, targetColumn)

    /**
     * Deletes this [Entity] instance, both from the cache and from the database.
     *
     * @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.testErrorOnSetToDeletedEntity
     */
    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)
    }

    /**
     * Sends all cached inserts and updates for this [Entity] instance to the database.
     *
     * @param batch The [EntityBatchUpdate] instance that should be used to perform a batch update operation
     * for multiple entities. If left `null`, a single update operation will be executed for this entity only.
     * @return `false` if no cached inserts or updates were sent to the database; `true`, otherwise.
     * @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityHookTest.testCallingFlushNotifiesEntityHookSubscribers
     */
    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

                @Suppress("VariableNaming")
                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(this)
                for ((c, v) in writeValues) {
                    batch[c] = v
                }
                storeWrittenValues()
            }

            return true
        }
        return false
    }

    /** Transfers initial column-value mappings from [writeValues] to [readValues] and clears the former once complete. */
    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 - 2024 Weber Informatics LLC | Privacy Policy