Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: EXPOSED-483 Column.transform() not applied when creating DAO entity #2246

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions documentation-website/Writerside/topics/Deep-Dive-into-DAO.md
Original file line number Diff line number Diff line change
Expand Up @@ -578,3 +578,39 @@ class EntityWithUInt : IntEntity() {
After that in your code you'll be able to put only `UInt` instances into `uint` field.
It still possible to insert/update values with negative integers via DAO, but your business code becomes much cleaner.
Please keep in mind what such transformations will aqure on every access to a field what means that you should avoid heavy transformations here.

Here’s the rewritten documentation for the `prePersist` function:

### Field value mutation before assignment

Databases store basic types like integers and strings, but on the DAO layer,
you often want more control over how these values are handled before they are set in your entity fields.
In many cases, you may want to perform transformations, such as sanitizing input, formatting values, or applying business rules.

To address this, you can use field transformations with `prePersist()` entity field method that modifies values before they are assigned to the entity.
This allows you to standardize or format data while ensuring that the transformation happens consistently across your codebase.

For example, assume you want to automatically trim and capitalize strings before they are assigned to an entity field:

```kotlin
object TableWithNames : IntIdTable() {
val name = varchar("name", 255)
}

class EntityWithName(id: EntityID<Int>) : IntEntity(id) {
var name: String by TableWithNames.name
.prePersist { it.trim().toUpperCase() }

companion object : IntEntityClass<EntityWithName>()
}
```

The `prePersist` function takes a lambda that defines how the value should be transformed before it is assigned to the entity field. In this example, every time the `name` field is set, the value is automatically trimmed and converted to uppercase.

This ensures that all `name` values in the entity are formatted uniformly, reducing the risk of inconsistent data handling in your business logic.

Keep in mind that these transformations occur **every time** a field is assigned, so it’s recommended to avoid heavy or performance-intensive operations in these transformations.

---

This version explains how `prePersist` ensures transformations happen before a value is assigned to the field, contrasting it with two-way transformations, and encourages the use of efficient operations in the transformation logic.
9 changes: 9 additions & 0 deletions exposed-dao/api/exposed-dao.api
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public class org/jetbrains/exposed/dao/Entity {
public final fun getId ()Lorg/jetbrains/exposed/dao/id/EntityID;
public final fun getKlass ()Lorg/jetbrains/exposed/dao/EntityClass;
public final fun getReadValues ()Lorg/jetbrains/exposed/sql/ResultRow;
public final fun getValue (Lorg/jetbrains/exposed/dao/EntityFieldWithPrePersist;Lorg/jetbrains/exposed/dao/Entity;Lkotlin/reflect/KProperty;)Ljava/lang/Object;
public final fun getValue (Lorg/jetbrains/exposed/dao/EntityFieldWithTransform;Lorg/jetbrains/exposed/dao/Entity;Lkotlin/reflect/KProperty;)Ljava/lang/Object;
public final fun getValue (Lorg/jetbrains/exposed/dao/OptionalReference;Lorg/jetbrains/exposed/dao/Entity;Lkotlin/reflect/KProperty;)Lorg/jetbrains/exposed/dao/Entity;
public final fun getValue (Lorg/jetbrains/exposed/dao/Reference;Lorg/jetbrains/exposed/dao/Entity;Lkotlin/reflect/KProperty;)Lorg/jetbrains/exposed/dao/Entity;
Expand All @@ -36,6 +37,7 @@ public class org/jetbrains/exposed/dao/Entity {
public final fun lookupInReadValues (Lorg/jetbrains/exposed/sql/Column;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;
public fun refresh (Z)V
public static synthetic fun refresh$default (Lorg/jetbrains/exposed/dao/Entity;ZILjava/lang/Object;)V
public final fun setValue (Lorg/jetbrains/exposed/dao/EntityFieldWithPrePersist;Lorg/jetbrains/exposed/dao/Entity;Lkotlin/reflect/KProperty;Ljava/lang/Object;)V
public final fun setValue (Lorg/jetbrains/exposed/dao/EntityFieldWithTransform;Lorg/jetbrains/exposed/dao/Entity;Lkotlin/reflect/KProperty;Ljava/lang/Object;)V
public final fun setValue (Lorg/jetbrains/exposed/dao/OptionalReference;Lorg/jetbrains/exposed/dao/Entity;Lkotlin/reflect/KProperty;Lorg/jetbrains/exposed/dao/Entity;)V
public final fun setValue (Lorg/jetbrains/exposed/dao/Reference;Lorg/jetbrains/exposed/dao/Entity;Lkotlin/reflect/KProperty;Lorg/jetbrains/exposed/dao/Entity;)V
Expand Down Expand Up @@ -152,6 +154,7 @@ public abstract class org/jetbrains/exposed/dao/EntityClass {
public final fun optionalReferrersOn (Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/sql/Column;Z)Lorg/jetbrains/exposed/dao/OptionalReferrers;
public static synthetic fun optionalReferrersOn$default (Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/dao/id/IdTable;ZILjava/lang/Object;)Lorg/jetbrains/exposed/dao/OptionalReferrers;
public static synthetic fun optionalReferrersOn$default (Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/sql/Column;ZILjava/lang/Object;)Lorg/jetbrains/exposed/dao/OptionalReferrers;
public final fun prePersist (Lorg/jetbrains/exposed/sql/Column;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/dao/EntityFieldWithPrePersist;
public final fun referencedOn (Lorg/jetbrains/exposed/dao/id/IdTable;)Lorg/jetbrains/exposed/dao/Reference;
public final fun referencedOn (Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/dao/Reference;
public final fun referrersOn (Lorg/jetbrains/exposed/dao/EntityClass;Lorg/jetbrains/exposed/dao/id/IdTable;)Lorg/jetbrains/exposed/dao/Referrers;
Expand Down Expand Up @@ -184,6 +187,12 @@ public abstract class org/jetbrains/exposed/dao/EntityClass {
public final fun wrapRows (Lorg/jetbrains/exposed/sql/SizedIterable;Lorg/jetbrains/exposed/sql/QueryAlias;)Lorg/jetbrains/exposed/sql/SizedIterable;
}

public final class org/jetbrains/exposed/dao/EntityFieldWithPrePersist {
public fun <init> (Lorg/jetbrains/exposed/sql/Column;Lkotlin/jvm/functions/Function1;)V
public final fun getColumn ()Lorg/jetbrains/exposed/sql/Column;
public final fun getPrePersistFn ()Lkotlin/jvm/functions/Function1;
}

public class org/jetbrains/exposed/dao/EntityFieldWithTransform : org/jetbrains/exposed/sql/ColumnTransformer {
public fun <init> (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/ColumnTransformer;Z)V
public synthetic fun <init> (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/ColumnTransformer;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
Expand Down
15 changes: 15 additions & 0 deletions exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/Entity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ import org.jetbrains.exposed.sql.transactions.TransactionManager
import kotlin.properties.Delegates
import kotlin.reflect.KProperty

/**
* Class responsible for enabling [Entity] field pre persist transformations,
* which may be useful when the value should be modified before it is set to the entity field
*/
class EntityFieldWithPrePersist<T>(
val column: Column<T>,
val prePersistFn: (T) -> T
)

/**
* Class responsible for enabling [Entity] field transformations, which may be useful when advanced database
* type conversions are necessary for entity mappings.
Expand Down Expand Up @@ -313,6 +322,12 @@ open class Entity<ID : Comparable<ID>>(val id: EntityID<ID>) {
column.setValue(o, desc, unwrap(value))
}

operator fun <T> EntityFieldWithPrePersist<T>.getValue(o: Entity<ID>, desc: KProperty<*>): T = column.getValue(o, desc)

operator fun <T> EntityFieldWithPrePersist<T>.setValue(o: Entity<ID>, desc: KProperty<*>, value: T) {
column.setValue(o, desc, prePersistFn(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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,27 @@ abstract class EntityClass<ID : Comparable<ID>, out T : Entity<ID>>(
true
)

/**
* Defines a transformation to be applied to a column's value before it is assigned to the entity field.
*
* This function enables modifying or transforming the column's value when setting the field in the entity,
* ensuring that the transformation happens on every field assignment. It is useful for cases like sanitizing input,
* applying business rules, or formatting the data before it is stored in the entity.
*
* @param T The type of the column.
* @param body The transformation function to apply before the value is assigned to the entity field. It takes new value of the column and returns the modified value.
* @return A wrapped column that applies the transformation before the value is set in the entity.
*
* Usage example:
*
* ```
* var name by nameColumn.prePersist { value ->
* value.trim().toUpperCase() // Trims and capitalizes the string before assigning it to the entity field
* }
* ```
*/
fun <T> Column<T>.prePersist(body: (T) -> T) = EntityFieldWithPrePersist(this, body)

private fun Query.setForUpdateStatus(): Query = if (this@EntityClass is ImmutableEntityClass<*, *>) this.notForUpdate() else this

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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.selectAll
import org.jetbrains.exposed.sql.tests.DatabaseTestsBase
import org.jetbrains.exposed.sql.tests.shared.assertEquals
import org.junit.Test

class PrePersistsTests : DatabaseTestsBase() {
object TestTable : IntIdTable("test_table") {
val value = text("value")
val nullableValue = text("nullableValue").nullable()
}

class TestEntity(id: EntityID<Int>) : IntEntity(id) {
var value by TestTable.value
.prePersist { it.uppercase() }
var nullableValue by TestTable.nullableValue
.prePersist { it?.uppercase() }

companion object : IntEntityClass<TestEntity>(TestTable)
}

@Test
fun testPrePersis() {
withTables(TestTable) {
val entity = TestEntity.new {
value = "test-value"
nullableValue = "nullable-test-value"
}

assertEquals("TEST-VALUE", entity.value)
assertEquals("NULLABLE-TEST-VALUE", entity.nullableValue)

TestTable.selectAll().first().let { entry ->
assertEquals("TEST-VALUE", entry[TestTable.value])
assertEquals("NULLABLE-TEST-VALUE", entry[TestTable.nullableValue])
}
}
}
}
Loading