Skip to content

Commit

Permalink
EXPOSED-368 Ordering on References
Browse files Browse the repository at this point in the history
  • Loading branch information
obabichevjb committed May 14, 2024
1 parent fd519d3 commit 54c482f
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 43 deletions.
26 changes: 26 additions & 0 deletions documentation-website/Writerside/topics/Deep-Dive-into-DAO.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,32 @@ class User(id: EntityID<Int>): IntEntity(id) {
}
```

### Ordered reference

You can also define the order in which referenced entities appear:

```kotlin
class User(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<User>(Users)

...
val ratings by UserRating referrersOn UserRatings.user orderBy UserRatings.value
...
}
```

In a more complex scenario, you can specify multiple columns along with the corresponding sort order for each:

```kotlin
class User(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<User>(Users)

...
val ratings by UserRating referrersOn UserRatings.user orderBy listOf(UserRatings.value to DESC, UserRatings.id to ASC)
...
}
```

### many-to-many reference
In some cases, a many-to-many reference may be required.
Let's assume you want to add a reference to the following Actors table to the StarWarsFilm class:
Expand Down
26 changes: 14 additions & 12 deletions exposed-dao/api/exposed-dao.api
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
public abstract class org/jetbrains/exposed/dao/BaseReferrers : kotlin/properties/ReadOnlyProperty {
public fun <init> (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/dao/EntityClass;Z)V
public final fun getCache ()Z
public final fun getFactory ()Lorg/jetbrains/exposed/dao/EntityClass;
public final fun getReference ()Lorg/jetbrains/exposed/sql/Column;
public synthetic fun getValue (Ljava/lang/Object;Lkotlin/reflect/KProperty;)Ljava/lang/Object;
public fun getValue (Lorg/jetbrains/exposed/dao/Entity;Lkotlin/reflect/KProperty;)Lorg/jetbrains/exposed/sql/SizedIterable;
public final fun orderBy (Ljava/util/List;)Lorg/jetbrains/exposed/dao/BaseReferrers;
public final fun orderBy (Lkotlin/Pair;)Lorg/jetbrains/exposed/dao/BaseReferrers;
public final fun orderBy (Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/dao/BaseReferrers;
}

public class org/jetbrains/exposed/dao/ColumnWithTransform {
public fun <init> (Lorg/jetbrains/exposed/sql/Column;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Z)V
public synthetic fun <init> (Lorg/jetbrains/exposed/sql/Column;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
Expand Down Expand Up @@ -256,13 +268,8 @@ public final class org/jetbrains/exposed/dao/OptionalReference {
public final fun getReference ()Lorg/jetbrains/exposed/sql/Column;
}

public final class org/jetbrains/exposed/dao/OptionalReferrers : kotlin/properties/ReadOnlyProperty {
public final class org/jetbrains/exposed/dao/OptionalReferrers : org/jetbrains/exposed/dao/BaseReferrers {
public fun <init> (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/dao/EntityClass;Z)V
public final fun getCache ()Z
public final fun getFactory ()Lorg/jetbrains/exposed/dao/EntityClass;
public final fun getReference ()Lorg/jetbrains/exposed/sql/Column;
public synthetic fun getValue (Ljava/lang/Object;Lkotlin/reflect/KProperty;)Ljava/lang/Object;
public fun getValue (Lorg/jetbrains/exposed/dao/Entity;Lkotlin/reflect/KProperty;)Lorg/jetbrains/exposed/sql/SizedIterable;
}

public final class org/jetbrains/exposed/dao/Reference {
Expand All @@ -276,13 +283,8 @@ public final class org/jetbrains/exposed/dao/ReferencesKt {
public static final fun with (Ljava/lang/Iterable;[Lkotlin/reflect/KProperty1;)Ljava/lang/Iterable;
}

public final class org/jetbrains/exposed/dao/Referrers : kotlin/properties/ReadOnlyProperty {
public final class org/jetbrains/exposed/dao/Referrers : org/jetbrains/exposed/dao/BaseReferrers {
public fun <init> (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/dao/EntityClass;Z)V
public final fun getCache ()Z
public final fun getFactory ()Lorg/jetbrains/exposed/dao/EntityClass;
public final fun getReference ()Lorg/jetbrains/exposed/sql/Column;
public synthetic fun getValue (Ljava/lang/Object;Lkotlin/reflect/KProperty;)Ljava/lang/Object;
public fun getValue (Lorg/jetbrains/exposed/dao/Entity;Lkotlin/reflect/KProperty;)Lorg/jetbrains/exposed/sql/SizedIterable;
}

public abstract class org/jetbrains/exposed/dao/UIntEntity : org/jetbrains/exposed/dao/Entity {
Expand Down
68 changes: 37 additions & 31 deletions exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/References.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package org.jetbrains.exposed.dao

import org.jetbrains.exposed.dao.id.IdTable
import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.Expression
import org.jetbrains.exposed.sql.LazySizedIterable
import org.jetbrains.exposed.sql.SizedIterable
import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.emptySized
import org.jetbrains.exposed.sql.transactions.TransactionManager
import kotlin.properties.ReadOnlyProperty
Expand Down Expand Up @@ -95,35 +97,10 @@ class OptionalBackReference<ParentID : Comparable<ParentID>, out Parent : Entity
* @param cache Whether loaded reference entities should be stored in the [EntityCache].
*/
class Referrers<ParentID : Comparable<ParentID>, in Parent : Entity<ParentID>, ChildID : Comparable<ChildID>, out Child : Entity<ChildID>, REF>(
val reference: Column<REF>,
val factory: EntityClass<ChildID, Child>,
val cache: Boolean
) : ReadOnlyProperty<Parent, SizedIterable<Child>> {
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<Child> {
val value = thisRef.run { reference.referee<REF>()!!.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()
}
}
}
reference: Column<REF>,
factory: EntityClass<ChildID, Child>,
cache: Boolean
) : BaseReferrers<ParentID, Parent, ChildID, Child, REF>(reference, factory, cache)

/**
* Class responsible for implementing property delegates of the read-only properties involved in an optional one-to-many
Expand All @@ -134,10 +111,19 @@ class Referrers<ParentID : Comparable<ParentID>, in Parent : Entity<ParentID>, C
* @param cache Whether loaded reference entities should be stored in the [EntityCache].
*/
class OptionalReferrers<ParentID : Comparable<ParentID>, in Parent : Entity<ParentID>, ChildID : Comparable<ChildID>, out Child : Entity<ChildID>, REF>(
val reference: Column<REF?>,
reference: Column<REF?>,
factory: EntityClass<ChildID, Child>,
cache: Boolean
) : BaseReferrers<ParentID, Parent, ChildID, Child, REF?>(reference, factory, cache)

abstract class BaseReferrers<ParentID : Comparable<ParentID>, in Parent : Entity<ParentID>, ChildID : Comparable<ChildID>, out Child : Entity<ChildID>, REF>(
val reference: Column<REF>,
val factory: EntityClass<ChildID, Child>,
val cache: Boolean
) : ReadOnlyProperty<Parent, SizedIterable<Child>> {
/** The list of columns and their [SortOrder] for ordering referred entities in on-to-many relationship. */
private var orderByExpressions: MutableList<Pair<Expression<*>, SortOrder>> = mutableListOf()

init {
reference.referee ?: error("Column $reference is not a reference")

Expand All @@ -150,7 +136,12 @@ class OptionalReferrers<ParentID : Comparable<ParentID>, in Parent : Entity<Pare
val value = thisRef.run { reference.referee<REF>()!!.lookup() }
if (thisRef.id._value == null || value == null) return emptySized()

val query = { factory.find { reference eq value } }
val query = {
@Suppress("SpreadOperator")
factory
.find { reference eq value }
.orderBy(*orderByExpressions.toTypedArray())
}
val transaction = TransactionManager.currentOrNull()
return when {
transaction == null -> thisRef.getReferenceFromCache(reference)
Expand All @@ -162,6 +153,21 @@ class OptionalReferrers<ParentID : Comparable<ParentID>, in Parent : Entity<Pare
else -> query()
}
}

/** Modifies this reference to sort entities according to the specified [order]. **/
infix fun orderBy(order: Pair<Expression<*>, SortOrder>): BaseReferrers<ParentID, Parent, ChildID, Child, REF> = apply {
this.orderByExpressions.add(order)
}

/** Modifies this reference to sort entities based on multiple columns as specified in [order]. **/
infix fun orderBy(order: List<Pair<Expression<*>, SortOrder>>): BaseReferrers<ParentID, Parent, ChildID, Child, REF> = apply {
this.orderByExpressions.addAll(order)
}

/** Modifies this reference to sort entities by a column specified in [expression] using ascending order. **/
infix fun orderBy(expression: Expression<*>): BaseReferrers<ParentID, Parent, ChildID, Child, REF> = apply {
this.orderByExpressions.add(expression to SortOrder.ASC)
}
}

private fun <SRC : Entity<*>> getReferenceObjectFromDelegatedProperty(entity: SRC, property: KProperty1<SRC, Any?>): Any? {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package org.jetbrains.exposed.sql.tests.shared.entities

import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.SortOrder.DESC
import org.jetbrains.exposed.sql.Transaction
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.tests.DatabaseTestsBase
import org.jetbrains.exposed.sql.tests.TestDB
import org.jetbrains.exposed.sql.tests.shared.assertEquals
import org.jetbrains.exposed.sql.tests.shared.assertTrue
import org.junit.Test

class OrderedReferenceTest : DatabaseTestsBase() {
object Users : IntIdTable()

object UserRatings : IntIdTable() {
val value = integer("value")
val user = reference("user", Users)
}

object UserNullableRatings : IntIdTable() {
val value = integer("value")
val user = reference("user", Users).nullable()
}

class UserRatingDefaultOrder(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<UserRatingDefaultOrder>(UserRatings)

var value by UserRatings.value
var user by UserDefaultOrder referencedOn UserRatings.user
}

class UserNullableRatingDefaultOrder(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<UserNullableRatingDefaultOrder>(UserNullableRatings)

var value by UserNullableRatings.value
var user by UserDefaultOrder optionalReferencedOn UserNullableRatings.user
}

class UserDefaultOrder(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<UserDefaultOrder>(Users)

val ratings by UserRatingDefaultOrder referrersOn UserRatings.user orderBy UserRatings.value
val nullableRatings by UserNullableRatingDefaultOrder optionalReferrersOn UserNullableRatings.user orderBy UserNullableRatings.value
}

@Test
fun testDefaultOrder() {
withOrderedReferenceTestTables {
val user = UserDefaultOrder.all().first()

unsortedRatingValues.sorted().zip(user.ratings).forEach { (value, rating) ->
assertEquals(value, rating.value)
}
unsortedRatingValues.sorted().zip(user.nullableRatings).forEach { (value, rating) ->
assertEquals(value, rating.value)
}
}
}

class UserRatingMultiColumn(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<UserRatingMultiColumn>(UserRatings)

var value by UserRatings.value
var user by UserMultiColumn referencedOn UserRatings.user
}

class UserNullableRatingMultiColumn(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<UserNullableRatingMultiColumn>(UserNullableRatings)

var value by UserNullableRatings.value
var user by UserMultiColumn optionalReferencedOn UserNullableRatings.user
}

class UserMultiColumn(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<UserMultiColumn>(Users)

val ratings by UserRatingMultiColumn referrersOn UserRatings.user orderBy listOf(UserRatings.value to DESC, UserRatings.id to DESC)
val nullableRatings by UserNullableRatingMultiColumn optionalReferrersOn UserNullableRatings.user orderBy listOf(
UserNullableRatings.value to DESC,
UserNullableRatings.id to DESC
)
}

@Test
fun testMultiColumnOrder() {
withOrderedReferenceTestTables {
val ratings = UserMultiColumn.all().first().ratings.map { it }
val nullableRatings = UserMultiColumn.all().first().nullableRatings.map { it }

// Ensure each value is less than the one before it.
// IDs should be sorted within groups of identical values.
fun assertRatingsOrdered(current: UserRatingMultiColumn, prev: UserRatingMultiColumn) {
assertTrue(current.value <= prev.value)
if (current.value == prev.value) {
assertTrue(current.id <= prev.id)
}
}

fun assertNullableRatingsOrdered(current: UserNullableRatingMultiColumn, prev: UserNullableRatingMultiColumn) {
assertTrue(current.value <= prev.value)
if (current.value == prev.value) {
assertTrue(current.id <= prev.id)
}
}

for (i in 1..<ratings.size) {
assertRatingsOrdered(ratings[i], ratings[i - 1])
assertNullableRatingsOrdered(nullableRatings[i], nullableRatings[i - 1])
}
}
}

private val unsortedRatingValues = listOf(0, 3, 1, 2, 4, 4, 5, 4, 5, 6, 9, 8)

private fun withOrderedReferenceTestTables(statement: Transaction.(TestDB) -> Unit) {
withTables(Users, UserRatings, UserNullableRatings) { db ->
val userId = Users.insert { }.resultedValues?.firstOrNull()?.get(Users.id) ?: error("User was not created")
unsortedRatingValues.forEach { value ->
UserRatings.insert {
it[user] = userId
it[UserRatings.value] = value
}
UserNullableRatings.insert {
it[user] = userId
it[UserRatings.value] = value
}
UserNullableRatings.insert {
it[user] = null
it[UserRatings.value] = value
}
}
statement(db)
}
}
}

0 comments on commit 54c482f

Please sign in to comment.