org.jetbrains.exposed.dao.EntityClass.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.dao.id.IdTable
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.TransactionManager
import java.util.concurrent.ConcurrentHashMap
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.full.primaryConstructor
import kotlin.sequences.Sequence
import kotlin.sequences.any
import kotlin.sequences.filter
/**
* Base class responsible for the management of [Entity] instances and the maintenance of their relation
* to the provided [table].
*
* @param [table] The [IdTable] object that stores rows mapped to entities managed by this class.
* @param [entityType] The expected [Entity] class type. This can be left `null` if it is the class of type
* argument [T] provided to this [EntityClass] instance.
* @param [entityCtor] The function invoked to instantiate an [Entity] using a provided [EntityID] value. If a
* reference to a specific entity constructor or a custom function is not passed as an argument, reflection will
* be used to determine the primary constructor of the associated entity class on first access (which can be slower).
*/
@Suppress("UNCHECKED_CAST", "UnnecessaryAbstractClass", "TooManyFunctions")
abstract class EntityClass, out T : Entity>(
val table: IdTable,
entityType: Class? = null,
entityCtor: ((EntityID) -> T)? = null,
) {
internal val klass: Class<*> = entityType ?: javaClass.enclosingClass as Class
private val entityPrimaryCtor: KFunction by lazy { klass.kotlin.primaryConstructor as KFunction }
private val entityCtor: (EntityID) -> T = entityCtor ?: { entityID -> entityPrimaryCtor.call(entityID) }
operator fun get(id: EntityID): T = findById(id) ?: throw EntityNotFoundException(id, this)
operator fun get(id: ID): T = get(DaoEntityID(id, table))
/**
* Instantiates an [EntityCache] with the current [Transaction] if one does not already exist in the
* current transaction scope.
*/
protected open fun warmCache(): EntityCache = TransactionManager.current().entityCache
/**
* Gets an [Entity] by its [id] value.
*
* @param id The id value of the entity.
* @return The entity that has this id value, or `null` if no entity was found.
*/
fun findById(id: ID): T? = findById(DaoEntityID(id, table))
/**
* Gets an [Entity] by its [id] value and updates the retrieved entity.
*
* @param id The id value of the entity.
* @param block Lambda that contains entity field updates.
* @return The updated entity that has this id value, or `null` if no entity was found.
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.testDaoFindByIdAndUpdate
*/
fun findByIdAndUpdate(id: ID, block: (it: T) -> Unit): T? {
val result = find(table.id eq id).forUpdate().singleOrNull() ?: return null
block(result)
return result
}
/**
* Gets a single [Entity] that conforms to the [op] conditional expression and updates the retrieved entity.
*
* @param op The conditional expression to use when selecting the entity.
* @param block Lambda that contains entity field updates.
* @return The updated entity that conforms to this condition, or `null` if either no entity was found
* or if more than one entity conforms to the condition.
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.testDaoFindSingleByAndUpdate
*/
fun findSingleByAndUpdate(op: Op, block: (it: T) -> Unit): T? {
val result = find(op).forUpdate().singleOrNull() ?: return null
block(result)
return result
}
/**
* Gets an [Entity] by its [EntityID] value.
*
* @param id The [EntityID] value of the entity.
* @return The entity that has this wrapped id value, or `null` if no entity was found.
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityCacheNotUpdatedOnCommitIssue1380.testRegression
*/
open fun findById(id: EntityID): T? = testCache(id) ?: find { table.id eq id }.firstOrNull()
/**
* Reloads the fields of an [entity] from the database and returns the [entity] as a new object.
*
* The original [entity] will also be removed from the current cache.
* @see removeFromCache
*
* @param flush Whether pending entity changes should be flushed prior to reloading.
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.testThatUpdateOfInsertedEntitiesGoesBeforeAnInsert
*/
fun reload(entity: Entity, flush: Boolean = false): T? {
if (flush) {
if (entity.isNewEntity()) {
TransactionManager.current().entityCache.flushInserts(table)
} else {
entity.flush()
}
}
removeFromCache(entity)
return if (entity.id._value != null) findById(entity.id) else null
}
internal open fun invalidateEntityInCache(o: Entity) {
val entityAlreadyFlushed = o.id._value != null
val sameDatabase = TransactionManager.current().db == o.db
if (!entityAlreadyFlushed || !sameDatabase) return
val currentEntityInCache = testCache(o.id)
if (currentEntityInCache == null) {
get(o.id) // Check that entity is still exists in database
warmCache().store(o)
} else if (currentEntityInCache !== o) {
exposedLogger.error(
"Entity instance in cache differs from the provided: ${o::class.simpleName} with ID ${o.id.value}. " +
"Changes on entity could be missed."
)
}
}
/**
* Searches the current [EntityCache] for an [Entity] by its [EntityID] value.
*
* @return The entity that has this wrapped id value, or `null` if no entity was found.
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.testCacheInvalidatedOnDSLDelete
*/
fun testCache(id: EntityID): T? = warmCache().find(this, id)
/**
* Searches the current [EntityCache] for all [Entity] instances that match the provided [cacheCheckCondition].
*
* @return A sequence of matching entities found.
*/
fun testCache(cacheCheckCondition: T.() -> Boolean): Sequence = warmCache().findAll(this).asSequence().filter { it.cacheCheckCondition() }
/**
* Removes the specified [entity] from the current [EntityCache], as well as any stored references to
* or from the removed entity.
*/
fun removeFromCache(entity: Entity) {
val cache = warmCache()
cache.remove(table, entity)
cache.referrers.forEach { (col, referrers) ->
// Remove references from entity to other entities
referrers.remove(entity.id)
// Remove references from other entities to this entity
if (col.table == table) {
with(entity) { col.lookup() }?.let { referrers.remove(it as EntityID<*>) }
}
}
}
/** Returns a [SizedIterable] containing all entities with [EntityID] values from the provided [ids] list. */
open fun forEntityIds(ids: List>): SizedIterable {
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 }))
}
/** Returns a [SizedIterable] containing all entities with id values from the provided [ids] list. */
fun forIds(ids: List): SizedIterable = forEntityIds(ids.map { DaoEntityID(it, table) })
/**
* Returns a [SizedIterable] containing entities generated using data retrieved from a database result set in [rows].
*/
fun wrapRows(rows: SizedIterable): SizedIterable = rows mapLazy {
wrapRow(it)
}
/**
* Returns a [SizedIterable] containing entities generated using data retrieved from a database result set in [rows].
*
* An [alias] should be provided to adjust each [ResultRow] mapping, if necessary, before generating entities.
*/
fun wrapRows(rows: SizedIterable, alias: Alias>) = rows mapLazy {
wrapRow(it, alias)
}
/**
* Returns a [SizedIterable] containing entities generated using data retrieved from a database result set in [rows].
*
* An [alias] should be provided to adjust each [ResultRow] mapping, if necessary, before generating entities.
*/
fun wrapRows(rows: SizedIterable, alias: QueryAlias) = rows mapLazy {
wrapRow(it, alias)
}
/** Wraps the specified [ResultRow] data into an [Entity] instance. */
@Suppress("MemberVisibilityCanBePrivate")
fun wrapRow(row: ResultRow): T {
val entity = wrap(row[table.id], row)
if (entity._readValues == null) {
entity._readValues = row
}
return entity
}
/**
* Wraps the specified [ResultRow] data into an [Entity] instance.
*
* The provided [alias] will be used to adjust the [ResultRow] mapping before returning the entity.
*
* @sample org.jetbrains.exposed.sql.tests.shared.AliasesTests.testWrapRowWithAliasedTable
*/
fun wrapRow(row: ResultRow, alias: Alias>): 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))
}
/**
* Wraps the specified [ResultRow] data into an [Entity] instance.
*
* The provided [alias] will be used to adjust the [ResultRow] mapping before returning the entity.
*
* @sample org.jetbrains.exposed.sql.tests.shared.AliasesTests.testWrapRowWithAliasedQuery
*/
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))
}
/**
* Gets all the [Entity] instances associated with this [EntityClass].
*
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.testNonEntityIdReference
*/
open fun all(): SizedIterable = wrapRows(table.selectAll().notForUpdate())
/**
* Gets all the [Entity] instances that conform to the [op] conditional expression.
*
* @param op The conditional expression to use when selecting the entity.
* @return A [SizedIterable] of all the entities that conform to this condition.
*/
fun find(op: Op): SizedIterable {
warmCache()
return wrapRows(searchQuery(op))
}
/**
* Gets all the [Entity] instances that conform to the [op] conditional expression.
*
* @param op The conditional expression to use when selecting the entity.
* @return A [SizedIterable] of all the entities that conform to this condition.
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.preloadOptionalReferencesOnAnEntity
*/
fun find(op: SqlExpressionBuilder.() -> Op): SizedIterable = find(SqlExpressionBuilder.op())
/**
* Searches the current [EntityCache] for all [Entity] instances that match the provided [cacheCheckCondition].
* If the cache returns no matches, entities that conform to the provided [op] conditional expression
* will be retrieved from the database.
*
* @return A sequence of matching entities found.
*/
fun findWithCacheCondition(cacheCheckCondition: T.() -> Boolean, op: SqlExpressionBuilder.() -> Op): Sequence {
val cached = testCache(cacheCheckCondition)
return if (cached.any()) cached else find(op).asSequence()
}
/** The [IdTable] that this [EntityClass] depends on when maintaining relations with managed [Entity] instances. */
open val dependsOnTables: ColumnSet get() = table
/** The columns that this [EntityClass] depends on when maintaining relations with managed [Entity] instances. */
open val dependsOnColumns: List> get() = dependsOnTables.columns
/**
* Returns a [Query] to select all columns in [dependsOnTables] with a WHERE clause that includes
* the provided [op] conditional expression.
*/
open fun searchQuery(op: Op): Query =
dependsOnTables.select(dependsOnColumns).where { op }.setForUpdateStatus()
/**
* Counts the amount of [Entity] instances that conform to the [op] conditional expression.
*
* @param op The conditional expression to use when selecting the entity.
* @return The amount of entities that conform to this condition.
* @sample org.jetbrains.exposed.sql.tests.h2.MultiDatabaseEntityTest.crossReferencesProhibitedForEntitiesFromDifferentDB
*/
fun count(op: Op? = null): Long {
val countExpression = table.id.count()
val query = table.select(countExpression).notForUpdate()
op?.let { query.adjustWhere { op } }
return query.first()[countExpression]
}
/** Creates a new [Entity] instance with the provided [entityId] value. */
protected open fun createInstance(entityId: EntityID, row: ResultRow?): T = entityCtor(entityId)
/**
* Returns an [Entity] with the provided [EntityID] value, or, if an entity was not found in the current
* [EntityCache], creates a new instance using the data in [row].
*/
fun wrap(id: EntityID, 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)
}
}
/**
* Creates a new [Entity] instance with the fields that are set in the [init] block. The id will be automatically set.
*
* @param init The block where the entity's fields can be set.
* @return The entity that has been created.
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.testNonEntityIdReference
*/
open fun new(init: T.() -> Unit) = new(null, init)
/**
* Creates a new [Entity] instance with the fields that are set in the [init] block and with the provided [id].
*
* @param id The id of the entity. Set this to `null` if it should be automatically generated.
* @param init The block where the entity's fields can be set.
* @return The entity that has been created.
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.testNewIdWithGet
*/
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 entityCache = warmCache()
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] = entityId
}
try {
entityCache.addNotInitializedEntityToQueue(prototype)
prototype.init()
} finally {
entityCache.finishEntityInitialization(prototype)
}
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] = readValues[col]
}
}
entityCache.scheduleInsert(this, prototype)
return prototype
}
/**
* Creates a [View] or subset of [Entity] instances, which are managed by this [EntityClass] and
* conform to the specified [op] conditional expression.
*/
inline fun view(op: SqlExpressionBuilder.() -> Op) = View(SqlExpressionBuilder.op(), this)
private val refDefinitions = HashMap, KClass<*>>, Any>()
private inline fun registerRefRule(column: Column<*>, ref: () -> R): R =
refDefinitions.getOrPut(column to R::class, ref) as R
/**
* Registers a reference as a field of the child entity class, which returns a parent object of this `EntityClass`.
*
* The reference should have been defined by the creation of a [column] using `reference()` on the child table.
*
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.Parent
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.Children
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.Child
*/
infix fun > referencedOn(column: Column) = registerRefRule(column) { Reference(column, this) }
/**
* Registers an optional reference as a field of the child entity class, which returns a parent object of
* this `EntityClass`.
*
* The reference should have been defined by the creation of a [column] using either `optReference()` or
* `reference().nullable()` on the child table.
*
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.Board
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.Posts
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.Post
*/
infix fun > optionalReferencedOn(column: Column) = registerRefRule(column) { OptionalReference(column, this) }
/**
* Registers a reference as an immutable field of the parent entity class, which returns a child object of
* this `EntityClass`.
*
* The reference should have been defined by the creation of a [column] using `reference()` on the child table.
*
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTestsData.YEntity
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTestsData.XTable
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTestsData.BEntity
*/
infix fun , Target : Entity, REF : Comparable> EntityClass.backReferencedOn(
column: Column
): ReadOnlyProperty, Target> = registerRefRule(column) { BackReference(column, this) }
/**
* Registers a reference as an immutable field of the parent entity class, which returns a child object of
* this `EntityClass`.
*
* The reference should have been defined by the creation of a [column] using `reference()` on the child table.
*
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTestsData.YEntity
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTestsData.XTable
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTestsData.BEntity
*/
@JvmName("backReferencedOnOpt")
infix fun , Target : Entity, REF : Comparable> EntityClass.backReferencedOn(
column: Column
): ReadOnlyProperty, Target> = registerRefRule(column) { BackReference(column, this) }
/**
* Registers an optional reference as an immutable field of the parent entity class, which returns a child object of
* this `EntityClass`.
*
* The reference should have been defined by the creation of a [column] using either `optReference()` or
* `reference().nullable()` on the child table.
*
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.Student
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.StudentBios
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.StudentBio
*/
infix fun , Target : Entity, REF : Comparable> EntityClass.optionalBackReferencedOn(
column: Column
) =
registerRefRule(column) { OptionalBackReference, REF>(column as Column, this) }
/**
* Registers an optional reference as an immutable field of the parent entity class, which returns a child object of
* this `EntityClass`.
*
* The reference should have been defined by the creation of a [column] using either `optReference()` or
* `reference().nullable()` on the child table.
*
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.Student
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.StudentBios
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.StudentBio
*/
@JvmName("optionalBackReferencedOnOpt")
infix fun , Target : Entity, REF : Comparable> EntityClass.optionalBackReferencedOn(
column: Column
) =
registerRefRule(column) { OptionalBackReference, REF>(column, this) }
/**
* Registers a reference as an immutable field of the parent entity class, which returns a collection of
* child objects of this `EntityClass` that all reference the parent.
*
* The reference should have been defined by the creation of a [column] using `reference()` on the child table.
*
* By default, this also stores the loaded entities to a cache.
*
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityHookTestData.Country
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityHookTestData.Cities
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityHookTestData.City
*/
infix fun , Target : Entity, REF : Comparable> EntityClass.referrersOn(column: Column) =
registerRefRule(column) { Referrers, TargetID, Target, REF>(column, this, true) }
/**
* Registers a reference as an immutable field of the parent entity class, which returns a collection of
* child objects of this `EntityClass` that all reference the parent.
*
* The reference should have been defined by the creation of a [column] using `reference()` on the child table.
*
* Set [cache] to `true` to also store the loaded entities to a cache.
*
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.School
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.Students
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.Student
*/
fun , Target : Entity, REF : Comparable> EntityClass.referrersOn(
column: Column,
cache: Boolean
) =
registerRefRule(column) { Referrers, TargetID, Target, REF>(column, this, cache) }
/**
* Registers an optional reference as an immutable field of the parent entity class, which returns a collection of
* child objects of this `EntityClass` that all reference the parent.
*
* The reference should have been defined by the creation of a [column] using either `optReference()` or
* reference().nullable()` on the child table.
*
* By default, this also stores the loaded entities to a cache.
*
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.Category
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.Posts
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.Post
*/
infix fun , Target : Entity, REF : Comparable> EntityClass.optionalReferrersOn(
column: Column
) =
registerRefRule(column) { OptionalReferrers, TargetID, Target, REF>(column, this, true) }
/**
* Registers an optional reference as an immutable field of the parent entity class, which returns a collection of
* child objects of this `EntityClass` that all reference the parent.
*
* The reference should have been defined by the creation of a [column] using either `optReference()` or
* `reference().nullable()` on the child table.
*
* Set [cache] to `true` to also store the loaded entities to a cache.
*
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.Student
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.Detentions
* @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.Detention
*/
fun , Target : Entity, REF : Comparable> EntityClass.optionalReferrersOn(
column: Column,
cache: Boolean = false
) =
registerRefRule(column) { OptionalReferrers, TargetID, Target, REF>(column, this, cache) }
/**
* Returns a [ColumnWithTransform] delegate that transforms this stored [TColumn] value on every read.
*
* @param toColumn A pure function that converts a transformed value to a value that can be stored
* in this original column type.
* @param toReal A pure function that transforms a value stored in this original column type.
* @sample org.jetbrains.exposed.sql.tests.shared.entities.TransformationsTable
* @sample org.jetbrains.exposed.sql.tests.shared.entities.TransformationEntity
*/
fun Column.transform(
toColumn: (TReal) -> TColumn,
toReal: (TColumn) -> TReal
): ColumnWithTransform = ColumnWithTransform(this, toColumn, toReal, false)
/**
* Returns a [ColumnWithTransform] delegate that will cache the transformed value on first read of
* this same stored [TColumn] value.
*
* @param toColumn A pure function that converts a transformed value to a value that can be stored
* in this original column type.
* @param toReal A pure function that transforms a value stored in this original column type.
* @sample org.jetbrains.exposed.sql.tests.shared.entities.TransformationsTable
* @sample org.jetbrains.exposed.sql.tests.shared.entities.TransformationEntity
*/
fun Column.memoizedTransform(
toColumn: (TReal) -> TColumn,
toReal: (TColumn) -> TReal
): ColumnWithTransform = ColumnWithTransform(this, toColumn, toReal, true)
private fun Query.setForUpdateStatus(): Query = if (this@EntityClass is ImmutableEntityClass<*, *>) this.notForUpdate() else this
/**
* Returns a list of retrieved [Entity] instances whose [refColumn] optionally matches any of the id values in [references].
*
* The [EntityCache] in the current transaction scope will be searched for matching entities, if appropriate
* for [refColumn]'s column type; otherwise, matching results will be queried from the database.
*
* Set [forUpdate] to `true` or `false` depending on whether a locking read should be placed or removed from the
* search query used. Leave the argument as `null` to use the query without any locking option.
*/
fun warmUpOptReferences(references: List, refColumn: Column, forUpdate: Boolean? = null): List =
warmUpReferences(references, refColumn as Column, forUpdate)
/**
* Returns a list of retrieved [Entity] instances whose [refColumn] matches any of the id values in [references].
*
* The [EntityCache] in the current transaction scope will be searched for matching entities, if appropriate
* for [refColumn]'s column type; otherwise, matching results will be queried from the database.
*
* Set [forUpdate] to `true` or `false` depending on whether a locking read should be placed or removed from the
* search query used. Leave the argument as `null` to use the query without any locking option.
*/
fun warmUpReferences(references: List, refColumn: Column, forUpdate: Boolean? = null): List {
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 transaction = TransactionManager.current()
val cache = transaction.entityCache
val keepLoadedReferenceOutOfTransaction = transaction.db.config.keepLoadedReferencesOutOfTransaction
if (refColumn.columnType is EntityIDColumnType<*>) {
refColumn as Column>
distinctRefIds as List>
val toLoad = distinctRefIds.filter {
cache.referrers[refColumn]?.containsKey(it)?.not() ?: true
}
if (toLoad.isNotEmpty()) {
val findQuery = find { refColumn inList toLoad }
val entities = getEntities(forUpdate, findQuery)
val result = entities.groupBy { it.readValues[refColumn] }
distinctRefIds.forEach { id ->
cache.getOrPutReferrers(id, refColumn) { result[id]?.let { SizedCollection(it) } ?: emptySized() }.also {
if (keepLoadedReferenceOutOfTransaction) {
val childEntity = find { refColumn eq id }.firstOrNull()
childEntity?.storeReferenceInCache(refColumn, it)
}
}
}
}
return distinctRefIds.flatMap { cache.getReferrers(it, refColumn)?.toList().orEmpty() }
} else {
val baseQuery = searchQuery(Op.build { refColumn inList distinctRefIds })
val finalQuery = if (parentTable.id in baseQuery.set.fields) {
baseQuery
} else {
baseQuery.adjustSelect { select(fields + parentTable.id) }
.adjustColumnSet { innerJoin(parentTable, { refColumn }, { refColumn.referee!! }) }
}
val findQuery = wrapRows(finalQuery)
val entities = getEntities(forUpdate, findQuery).distinct()
entities.groupBy { it.readValues[refColumn] }.forEach { (id, values) ->
val parentEntityId: EntityID<*> = parentTable.selectAll().where { refColumn.referee as Column eq id }
.single()[parentTable.id]
cache.getOrPutReferrers(parentEntityId, refColumn) { SizedCollection(values) }.also {
if (keepLoadedReferenceOutOfTransaction) {
val childEntity = find { refColumn eq id }.firstOrNull()
childEntity?.storeReferenceInCache(refColumn, it)
}
}
}
return entities
}
}
private fun getEntities(forUpdate: Boolean?, findQuery: SizedIterable): List = when (forUpdate) {
true -> findQuery.forUpdate()
false -> findQuery.notForUpdate()
else -> findQuery
}.toList()
@Suppress("ComplexMethod")
internal fun > warmUpLinkedReferences(
references: List>,
sourceRefColumn: Column>,
targetRefColumn: Column>,
linkTable: Table,
forUpdate: Boolean? = null,
optimizedLoad: Boolean = false
): List {
if (references.isEmpty()) return emptyList()
val distinctRefIds = references.distinct()
val transaction = TransactionManager.current()
val inCache = transaction.entityCache.referrers[sourceRefColumn] ?: emptyMap()
val loaded = ((distinctRefIds - inCache.keys).takeIf { it.isNotEmpty() } as List>?)?.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 = when {
optimizedLoad -> listOf(sourceRefColumn, targetRefColumn)
alreadyInJoin -> (dependsOnColumns + sourceRefColumn).distinct()
else -> (dependsOnColumns + linkTable.columns + sourceRefColumn).distinct()
}
val query = entityTables.select(columns).where { sourceRefColumn inList idsToLoad }
val targetEntities = mutableMapOf, T>()
val entitiesWithRefs = when (forUpdate) {
true -> query.forUpdate()
false -> query.notForUpdate()
else -> query
}.map {
val targetId = it[targetRefColumn]
if (!optimizedLoad) {
targetEntities.getOrPut(targetId) { wrapRow(it) }
}
it[sourceRefColumn] to targetId
}
if (optimizedLoad) {
forEntityIds(entitiesWithRefs.map { it.second }).forEach {
targetEntities[it.id] = it
}
}
val groupedBySourceId = entitiesWithRefs.groupBy({ it.first }) { targetEntities.getValue(it.second) }
idsToLoad.forEach {
transaction.entityCache.getOrPutReferrers(it, sourceRefColumn) {
SizedCollection(groupedBySourceId[it] ?: emptyList())
}
}
targetEntities.values
}
return inCache.values.flatMap { it.toList() as List } + loaded.orEmpty()
}
/**
* Returns a list of retrieved [Entity] instances whose reference column matches any of the [EntityID] values
* in [references]. Both the entity's source and target reference columns should have been defined in [linkTable].
*
* The [EntityCache] in the current transaction scope will be searched for matching entities.
*
* Set [forUpdate] to `true` or `false` depending on whether a locking read should be placed or removed from the
* search query used. Leave the argument as `null` to use the query without any locking option.
*
* Set [optimizedLoad] to `true` to force two queries separately, one for loading ids and another for loading
* referenced entities. This could be useful when references target the same entities. This will prevent them from
* loading multiple times (per each reference row) and will require less memory/bandwidth for "heavy" entities
* (with a lot of columns and/or columns that store large data sizes).
*/
fun > warmUpLinkedReferences(
references: List>,
linkTable: Table,
forUpdate: Boolean? = null,
optimizedLoad: Boolean = false
): List {
if (references.isEmpty()) return emptyList()
val sourceRefColumn = linkTable.columns.singleOrNull { it.referee == references.first().table.id } as? Column>
?: error("Can't detect source reference column")
val targetRefColumn =
linkTable.columns.singleOrNull { it.referee == table.id } as? Column> ?: error("Can't detect target reference column")
return warmUpLinkedReferences(references, sourceRefColumn, targetRefColumn, linkTable, forUpdate, optimizedLoad)
}
/**
* Returns whether the [entityClass] type is equivalent to or a superclass of this [EntityClass] instance's [klass].
*/
fun , T : Entity> isAssignableTo(entityClass: EntityClass) = entityClass.klass.isAssignableFrom(klass)
}
/**
* Base class responsible for the management of immutable [Entity] instances and the maintenance of their relation
* to the provided [table].
*
* @param [table] The [IdTable] object that stores rows mapped to entities managed by this class.
* @param [entityType] The expected [Entity] class type. This can be left `null` if it is the class of type
* argument [T] provided to this [EntityClass] instance.
* @param [ctor] The function invoked to instantiate an [Entity] using a provided [EntityID] value. If a
* reference to a specific entity constructor or a custom function is not passed as an argument, reflection will
* be used to determine the primary constructor of the associated entity class on first access.
* @sample org.jetbrains.exposed.sql.tests.shared.entities.ImmutableEntityTest.Schema.Organization
* @sample org.jetbrains.exposed.sql.tests.shared.entities.ImmutableEntityTest.EOrganization
*/
abstract class ImmutableEntityClass, out T : Entity>(
table: IdTable,
entityType: Class? = null,
ctor: ((EntityID) -> T)? = null
) :
EntityClass(table, entityType, ctor) {
/**
* Updates an [entity] field directly in the database, then removes this entity from the [EntityCache] in
* the current transaction scope. This is useful when needing to ensure that an entity is only updated with
* data retrieved directly from a database query.
*
* @sample org.jetbrains.exposed.sql.tests.shared.entities.ImmutableEntityTest.immutableEntityReadAfterUpdate
*/
open fun forceUpdateEntity(entity: Entity, column: Column, value: T) {
table.update({ table.id eq entity.id }) {
it[column] = value
}
/* Evict the entity from the current transaction entity cache,
so that the next read of this entity using DAO API would return
actual data from the DB */
TransactionManager.currentOrNull()?.entityCache?.remove(table, entity)
}
}
/**
* Base class responsible for the management of immutable [Entity] instances and the maintenance of their relation
* to the provided [table].
* An internal cache is used to store entity loading states by the associated database, in order to guarantee that
* that entity updates are synchronized with this class as the lock object.
*
* @param [table] The [IdTable] object that stores rows mapped to entities managed by this class.
* @param [entityType] The expected [Entity] class type. This can be left `null` if it is the class of type
* argument [T] provided to this [EntityClass] instance.
* @param [ctor] The function invoked to instantiate an [Entity] using a provided [EntityID] value. If a
* reference to a specific entity constructor or a custom function is not passed as an argument, reflection will
* be used to determine the primary constructor of the associated entity class on first access.
* @sample org.jetbrains.exposed.sql.tests.shared.entities.ImmutableEntityTest.Schema.Organization
* @sample org.jetbrains.exposed.sql.tests.shared.entities.ImmutableEntityTest.ECachedOrganization
*/
abstract class ImmutableCachedEntityClass, out T : Entity>(
table: IdTable,
entityType: Class? = null,
ctor: ((EntityID) -> T)? = null
) :
ImmutableEntityClass(table, entityType, ctor) {
private val cacheLoadingState = Key()
private var _cachedValues: MutableMap>> = ConcurrentHashMap()
override fun invalidateEntityInCache(o: Entity) {
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 = SizedCollection(warmCache().findAll(this))
/**
* Clears either only values for the database associated with the current [Transaction] or
* the entire cache if a database transaction cannot be found.
*/
@Synchronized
fun expireCache() {
if (TransactionManager.isInitialized() && TransactionManager.currentOrNull() != null) {
_cachedValues.remove(TransactionManager.current().db)
} else {
_cachedValues.clear()
}
}
override fun forceUpdateEntity(entity: Entity, column: Column, value: T) {
super.forceUpdateEntity(entity, column, value)
entity._readValues?.set(column, value)
expireCache()
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy