org.jetbrains.exposed.dao.Entity.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of exposed-dao Show documentation
Show all versions of exposed-dao Show documentation
Exposed, an ORM framework for Kotlin
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