From f6077bb420a95fb5c2fc0fca419ca4368c4940ba Mon Sep 17 00:00:00 2001 From: Oleg Babichev Date: Thu, 19 Sep 2024 15:22:03 +0200 Subject: [PATCH] feat: EXPOSED-483 Column.transform() not applied when creating DAO entity --- .../Writerside/topics/Deep-Dive-into-DAO.md | 36 +++++++++++++++ exposed-dao/api/exposed-dao.api | 9 ++++ .../org/jetbrains/exposed/dao/Entity.kt | 15 +++++++ .../org/jetbrains/exposed/dao/EntityClass.kt | 21 +++++++++ .../tests/shared/entities/PrePersistsTests.kt | 44 +++++++++++++++++++ 5 files changed, 125 insertions(+) create mode 100644 exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/PrePersistsTests.kt diff --git a/documentation-website/Writerside/topics/Deep-Dive-into-DAO.md b/documentation-website/Writerside/topics/Deep-Dive-into-DAO.md index a46e843a9c..fc6d6682b5 100644 --- a/documentation-website/Writerside/topics/Deep-Dive-into-DAO.md +++ b/documentation-website/Writerside/topics/Deep-Dive-into-DAO.md @@ -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) : IntEntity(id) { + var name: String by TableWithNames.name + .prePersist { it.trim().toUpperCase() } + + companion object : IntEntityClass() +} +``` + +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. diff --git a/exposed-dao/api/exposed-dao.api b/exposed-dao/api/exposed-dao.api index 65bc0bd4a0..6175a3ae8d 100644 --- a/exposed-dao/api/exposed-dao.api +++ b/exposed-dao/api/exposed-dao.api @@ -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; @@ -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 @@ -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; @@ -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 (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 (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/ColumnTransformer;Z)V public synthetic fun (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/ColumnTransformer;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V diff --git a/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/Entity.kt b/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/Entity.kt index 023a03a017..8285abda9c 100644 --- a/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/Entity.kt +++ b/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/Entity.kt @@ -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( + val column: Column, + 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. @@ -313,6 +322,12 @@ open class Entity>(val id: EntityID) { column.setValue(o, desc, unwrap(value)) } + operator fun EntityFieldWithPrePersist.getValue(o: Entity, desc: KProperty<*>): T = column.getValue(o, desc) + + operator fun EntityFieldWithPrePersist.setValue(o: Entity, 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. diff --git a/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityClass.kt b/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityClass.kt index 4a59a8e7a8..7a29c3df38 100644 --- a/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityClass.kt +++ b/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityClass.kt @@ -876,6 +876,27 @@ abstract class EntityClass, out T : Entity>( 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 Column.prePersist(body: (T) -> T) = EntityFieldWithPrePersist(this, body) + private fun Query.setForUpdateStatus(): Query = if (this@EntityClass is ImmutableEntityClass<*, *>) this.notForUpdate() else this /** diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/PrePersistsTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/PrePersistsTests.kt new file mode 100644 index 0000000000..4f862c6a51 --- /dev/null +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/PrePersistsTests.kt @@ -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) : IntEntity(id) { + var value by TestTable.value + .prePersist { it.uppercase() } + var nullableValue by TestTable.nullableValue + .prePersist { it?.uppercase() } + + companion object : IntEntityClass(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]) + } + } + } +}