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.CompositeID
import org.jetbrains.exposed.dao.id.CompositeIdTable
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.IdTable
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 EntityFieldWithTransform(
/** The original column that will be transformed */
val column: Column,
/** Instance of [ColumnTransformer] with the transformation logic */
private val transformer: ColumnTransformer,
/**
* The function used to convert a transformed value to a value that can be stored in the original column type.
* Whether the original and transformed values should be cached to avoid multiple conversion calls.
*/
protected val cacheResult: Boolean = false
) : ColumnTransformer {
private var cache: Pair? = null
override fun unwrap(value: Wrapped): Unwrapped {
return transformer.unwrap(value)
}
/** The function used to transform a value stored in the original column type. */
override fun wrap(value: Unwrapped): Wrapped {
return if (cacheResult) {
val localCache = cache
if (localCache != null && localCache.first == value) {
localCache.second
} else {
transformer.wrap(value).also { cache = value to it }
}
} else {
transformer.wrap(value)
}
}
}
/**
* 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
}
}
@Suppress("UNCHECKED_CAST")
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 isSingleIdReference = hasSingleReferenceWithReferee(allReferences)
val refValue = if (isSingleIdReference) {
reference.getValue(o, desc)
} else {
getCompositeID {
allReferences.map { (child, parent) -> parent to child.getValue(o, desc) }
}
}
when {
refValue is EntityID<*> && reference.referee() == factory.table.id -> {
factory.findById(refValue.value as RID).also {
storeReferenceInCache(reference, it)
}
}
refValue is CompositeID && allReferencesMatch(allReferences, factory.table) -> {
factory.findById(refValue as RID).also {
storeReferenceInCache(reference, it)
}
}
else -> {
val castReferee = reference.referee()!!
val baseReferee = (castReferee.columnType as? EntityIDColumnType)?.idColumn ?: castReferee
factory.findWithCacheCondition({
if (isSingleIdReference) {
reference.referee!!.getValue(this, desc) == refValue
} else {
allReferences.all {
it.value.getValue(this, desc) == (refValue as CompositeID)[it.key as Column>>]
}
}
}) {
baseReferee eq (refValue as REF)
}.singleOrNull()?.also {
storeReferenceInCache(reference, it)
}
}
} ?: 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
allReferences.forEach { (childColumn, parentColumn) ->
val refValue = value.run { parentColumn.getValue(this, desc) }
if (childColumn == reference) storeReferenceInCache(reference, value)
(childColumn as Column).setValue(o, desc, refValue)
}
}
@Suppress("UNCHECKED_CAST")
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 isSingleIdReference = hasSingleReferenceWithReferee(allReferences)
val refValue = if (isSingleIdReference) {
reference.getValue(o, desc)
} else {
val childValues = allReferences.map { (child, parent) ->
parent to child.getValue(o, desc)
}
if (childValues.any { it.second == null }) null else getCompositeID { childValues }
}
when {
refValue == null -> null
refValue is EntityID<*> && reference.referee() == factory.table.id -> {
factory.findById(refValue.value as RID).also {
storeReferenceInCache(reference, it)
}
}
refValue is CompositeID && allReferencesMatch(allReferences, factory.table) -> {
factory.findById(refValue as RID).also {
storeReferenceInCache(reference, it)
}
}
else -> {
factory.findWithCacheCondition({
if (isSingleIdReference) {
reference.referee!!.getValue(this, desc) == refValue
} else {
allReferences.all {
it.value.getValue(this, desc) == (refValue as CompositeID)[it.key as Column>>]
}
}
}) {
reference.referee()!! eq (refValue as REF)
}.singleOrNull().also {
storeReferenceInCache(reference, it)
}
}
}
}
}
@Suppress("UNCHECKED_CAST")
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
allReferences.forEach { (childColumn, parentColumn) ->
val refValue = value?.run { parentColumn.getValue(this, desc) }
if (childColumn == reference) storeReferenceInCache(reference, value)
(childColumn as Column).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)
}
}
val valueTypeMismatch = value is EntityID<*> && value.table is CompositeIdTable && this.columnType !is EntityIDColumnType<*>
writeValues[this as Column] = if (valueTypeMismatch) (value as EntityID<*>)._value else 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 EntityFieldWithTransform.getValue(o: Entity, desc: KProperty<*>): Wrapped =
wrap(column.getValue(o, desc))
operator fun EntityFieldWithTransform.setValue(o: Entity, desc: KProperty<*>, value: Wrapped) {
column.setValue(o, desc, unwrap(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) {
val unwrappedValue = if (c.columnType is ColumnWithTransform<*, *>) {
(c.columnType as ColumnWithTransform).unwrapRecursive(v)
} else {
v
}
_readValues!![c] = unwrappedValue
}
if (klass.dependsOnColumns.any { it.table == klass.table && !_readValues!!.hasValue(it) }) {
_readValues = null
}
}
// clear write values
writeValues.clear()
}
/**
* Stores a [value] for a [table] `id` column in this Entity's [writeValues] map.
* If the `id` column wraps a composite value, each non-null component value is stored for its component column.
*/
@Suppress("UNCHECKED_CAST")
internal fun writeIdColumnValue(table: IdTable<*>, value: EntityID<*>) {
(value._value as? CompositeID)?.let { id ->
table.idColumns.forEach { column ->
id[column as Column>>]?.let {
writeValues[column as Column] = it
}
}
} ?: run {
writeValues[table.id as Column] = value
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy