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

org.jetbrains.exposed.dao.References.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.id.IdTable
import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.LazySizedIterable
import org.jetbrains.exposed.sql.SizedIterable
import org.jetbrains.exposed.sql.emptySized
import org.jetbrains.exposed.sql.transactions.TransactionManager
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
import kotlin.reflect.KProperty1
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible

private fun checkReference(reference: Column<*>, factoryTable: IdTable<*>) {
    val refColumn = reference.referee ?: error("Column $reference is not a reference")
    val targetTable = refColumn.table
    if (factoryTable != targetTable) {
        error("Column and factory point to different tables")
    }
}

/**
 * Class representing a table relation between two [Entity] classes, which is responsible for
 * retrieving the parent entity referenced by the child entity.
 *
 * @param reference The reference column defined on the child entity's associated table.
 * @param factory The [EntityClass] associated with the parent entity referenced by the child entity.
 */
class Reference, ID : Comparable, out Target : Entity>(
    val reference: Column,
    val factory: EntityClass
) {
    init {
        checkReference(reference, factory.table)
    }
}

/**
 * Class representing an optional table relation between two [Entity] classes, which is responsible for
 * retrieving the parent entity optionally referenced by the child entity.
 *
 * @param reference The nullable reference column defined on the child entity's associated table.
 * @param factory The [EntityClass] associated with the parent entity optionally referenced by the child entity.
 */
class OptionalReference, ID : Comparable, out Target : Entity>(
    val reference: Column,
    val factory: EntityClass
) {
    init {
        checkReference(reference, factory.table)
    }
}

/**
 * Class responsible for implementing property delegates of the read-only properties involved in a table
 * relation between two [Entity] classes, which retrieves the child entity that references the parent entity.
 *
 * @param reference The reference column defined on the child entity's associated table.
 * @param factory The [EntityClass] associated with the child entity that references the parent entity.
 */
internal class BackReference, out Parent : Entity, ChildID : Comparable, in Child : Entity, REF>(
    reference: Column,
    factory: EntityClass
) : ReadOnlyProperty {
    internal val delegate = Referrers(reference, factory, true)

    override operator fun getValue(thisRef: Child, property: KProperty<*>) =
        delegate.getValue(thisRef.apply { thisRef.id.value }, property).single() // flush entity before to don't miss newly created entities
}

/**
 * Class responsible for implementing property delegates of the read-only properties involved in an optional table
 * relation between two [Entity] classes, which retrieves the child entity that optionally references the parent entity.
 *
 * @param reference The nullable reference column defined on the child entity's associated table.
 * @param factory The [EntityClass] associated with the child entity that optionally references the parent entity.
 */
class OptionalBackReference, out Parent : Entity, ChildID : Comparable, in Child : Entity, REF>(
    reference: Column,
    factory: EntityClass
) : ReadOnlyProperty {
    internal val delegate = OptionalReferrers(reference, factory, true)

    override operator fun getValue(thisRef: Child, property: KProperty<*>) =
        delegate.getValue(thisRef.apply { thisRef.id.value }, property).singleOrNull() // flush entity before to don't miss newly created entities
}

/**
 * Class responsible for implementing property delegates of the read-only properties involved in a one-to-many
 * relation, which retrieves all child entities that reference the parent entity.
 *
 * @param reference The reference column defined on the child entity's associated table.
 * @param factory The [EntityClass] associated with the child entity that references the parent entity.
 * @param cache Whether loaded reference entities should be stored in the [EntityCache].
 */
class Referrers, in Parent : Entity, ChildID : Comparable, out Child : Entity, REF>(
    val reference: Column,
    val factory: EntityClass,
    val cache: Boolean
) : ReadOnlyProperty> {
    init {
        reference.referee ?: error("Column $reference is not a reference")

        if (factory.table != reference.table) {
            error("Column and factory point to different tables")
        }
    }

    override operator fun getValue(thisRef: Parent, property: KProperty<*>): SizedIterable {
        val value = thisRef.run { reference.referee()!!.lookup() }
        if (thisRef.id._value == null || value == null) return emptySized()

        val query = { factory.find { reference eq value } }
        val transaction = TransactionManager.currentOrNull()
        return when {
            transaction == null -> thisRef.getReferenceFromCache(reference)
            cache -> {
                transaction.entityCache.getOrPutReferrers(thisRef.id, reference, query).also {
                    thisRef.storeReferenceInCache(reference, it)
                }
            }
            else -> query()
        }
    }
}

/**
 * Class responsible for implementing property delegates of the read-only properties involved in an optional one-to-many
 * relation, which retrieves all child entities that optionally reference the parent entity.
 *
 * @param reference The nullable reference column defined on the child entity's associated table.
 * @param factory The [EntityClass] associated with the child entity that optionally references the parent entity.
 * @param cache Whether loaded reference entities should be stored in the [EntityCache].
 */
class OptionalReferrers, in Parent : Entity, ChildID : Comparable, out Child : Entity, REF>(
    val reference: Column,
    val factory: EntityClass,
    val cache: Boolean
) : ReadOnlyProperty> {
    init {
        reference.referee ?: error("Column $reference is not a reference")

        if (factory.table != reference.table) {
            error("Column and factory point to different tables")
        }
    }

    override operator fun getValue(thisRef: Parent, property: KProperty<*>): SizedIterable {
        val value = thisRef.run { reference.referee()!!.lookup() }
        if (thisRef.id._value == null || value == null) return emptySized()

        val query = { factory.find { reference eq value } }
        val transaction = TransactionManager.currentOrNull()
        return when {
            transaction == null -> thisRef.getReferenceFromCache(reference)
            cache -> {
                transaction.entityCache.getOrPutReferrers(thisRef.id, reference, query).also {
                    thisRef.storeReferenceInCache(reference, it)
                }
            }
            else -> query()
        }
    }
}

private fun > getReferenceObjectFromDelegatedProperty(entity: SRC, property: KProperty1): Any? {
    property.isAccessible = true
    return property.getDelegate(entity)
}

private fun > filterRelationsForEntity(
    entity: SRC,
    relations: Array, Any?>>
): Collection> {
    val validMembers = entity::class.memberProperties
    return validMembers.filter { it in relations } as Collection>
}

@Suppress("UNCHECKED_CAST", "NestedBlockDepth", "ComplexMethod")
private fun > List>.preloadRelations(
    vararg relations: KProperty1, Any?>,
    nodesVisited: MutableSet> = mutableSetOf()
) {
    val entity = this.firstOrNull() ?: return
    if (nodesVisited.contains(entity.klass)) {
        return
    } else {
        nodesVisited.add(entity.klass)
    }

    val isReferenceCacheEnabled = TransactionManager.currentOrNull()?.db?.config?.keepLoadedReferencesOutOfTransaction ?: false

    fun storeReferenceCache(reference: Column<*>, prop: KProperty1, Any?>) {
        if (isReferenceCacheEnabled) {
            this.forEach { entity ->
                entity.storeReferenceInCache(reference, prop.get(entity))
            }
        }
    }

    val directRelations = filterRelationsForEntity(entity, relations)
    directRelations.forEach { prop ->
        when (val refObject = getReferenceObjectFromDelegatedProperty(entity, prop)) {
            is Reference<*, *, *> -> {
                (refObject as Reference>, *, Entity<*>>).reference.let { refColumn ->
                    this.map { it.run { refColumn.lookup() } }.takeIf { it.isNotEmpty() }?.let { refIds ->
                        refObject.factory.find { refColumn.referee>>()!! inList refIds.distinct() }.toList()
                    }.orEmpty()
                    storeReferenceCache(refColumn, prop)
                }
            }
            is OptionalReference<*, *, *> -> {
                (refObject as OptionalReference>, *, Entity<*>>).reference.let { refColumn ->
                    this.mapNotNull { it.run { refColumn.lookup() } }.takeIf { it.isNotEmpty() }?.let { refIds ->
                        refObject.factory.find { refColumn.referee>>()!! inList refIds.distinct() }.toList()
                    }.orEmpty()
                    storeReferenceCache(refColumn, prop)
                }
            }
            is Referrers<*, *, *, *, *> -> {
                (refObject as Referrers, *, Entity<*>, Any>).reference.let { refColumn ->
                    val refIds = this.map { it.run { refColumn.referee()!!.lookup() } }
                    refObject.factory.warmUpReferences(refIds, refColumn)
                    storeReferenceCache(refColumn, prop)
                }
            }
            is OptionalReferrers<*, *, *, *, *> -> {
                (refObject as OptionalReferrers, *, Entity<*>, Any>).reference.let { refColumn ->
                    val refIds = this.mapNotNull { it.run { refColumn.referee()!!.lookup() } }
                    refObject.factory.warmUpOptReferences(refIds, refColumn)
                    storeReferenceCache(refColumn, prop)
                }
            }
            is InnerTableLink<*, *, *, *> -> {
                (refObject as InnerTableLink, Comparable>, Entity>>>).let { innerTableLink ->
                    innerTableLink.target.warmUpLinkedReferences(
                        references = this.map { it.id },
                        sourceRefColumn = innerTableLink.sourceColumn,
                        targetRefColumn = innerTableLink.targetColumn,
                        linkTable = innerTableLink.table
                    )
                    storeReferenceCache(innerTableLink.sourceColumn, prop)
                }
            }
            is BackReference<*, *, *, *, *> -> {
                (refObject.delegate as Referrers, *, Entity<*>, Any>).reference.let { refColumn ->
                    val refIds = this.map { it.run { refColumn.referee()!!.lookup() } }
                    refObject.delegate.factory.warmUpReferences(refIds, refColumn)
                    storeReferenceCache(refColumn, prop)
                }
            }
            is OptionalBackReference<*, *, *, *, *> -> {
                (refObject.delegate as OptionalReferrers, *, Entity<*>, Any>).reference.let { refColumn ->
                    val refIds = this.map { it.run { refColumn.referee()!!.lookup() } }
                    refObject.delegate.factory.warmUpOptReferences(refIds, refColumn)
                    storeReferenceCache(refColumn, prop)
                }
            }
            else -> error("Relation delegate has an unknown type")
        }
    }

    if (directRelations.isNotEmpty() && relations.size != directRelations.size) {
        val remainingRelations = relations.toList() - directRelations
        directRelations.map { relationProperty ->
            val relationsToLoad = this.flatMap {
                when (val relation = (relationProperty as KProperty1, *>).get(it)) {
                    is SizedIterable<*> -> relation.toList()
                    is Entity<*> -> listOf(relation)
                    null -> listOf()
                    else -> error("Unrecognised loaded relation")
                } as List>
            }.groupBy { it::class }

            relationsToLoad.forEach { (_, entities) ->
                entities.preloadRelations(
                    relations = remainingRelations.toTypedArray() as Array, Any?>>,
                    nodesVisited = nodesVisited
                )
            }
        }
    }
}

/**
 * Eager loads references for all [Entity] instances in this collection and returns this collection.
 *
 * **See also:** [Eager Loading](https://github.com/JetBrains/Exposed/wiki/DAO#eager-loading)
 *
 * @param relations The reference fields of the entities, as [KProperty]s, which should be loaded.
 * @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.preloadRelationAtDepth
 */
fun , SRC : Entity, REF : Entity<*>, L : Iterable> L.with(vararg relations: KProperty1): L {
    toList().apply {
        (this@with as? LazySizedIterable)?.loadedResult = this
        if (any { it.isNewEntity() }) {
            TransactionManager.current().flushCache()
        }
        preloadRelations(*relations)
    }
    return this
}

/**
 * Eager loads references for this [Entity] instance and returns this entity instance.
 *
 * **See also:** [Eager Loading](https://github.com/JetBrains/Exposed/wiki/DAO#eager-loading)
 *
 * @param relations The reference fields of this entity, as [KProperty]s, which should be loaded.
 * @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.preloadOptionalReferencesOnAnEntity
 */
fun , SRC : Entity> SRC.load(vararg relations: KProperty1, Any?>): SRC = apply {
    listOf(this).with(*relations)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy