-
Notifications
You must be signed in to change notification settings - Fork 695
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-388 Support for column type converters #2143
feat!: EXPOSED-388 Support for column type converters #2143
Conversation
cdb1d3f
to
88b2caa
Compare
exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/ColumnTransformTest.kt
Outdated
Show resolved
Hide resolved
exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/ColumnTransformTest.kt
Outdated
Show resolved
Hide resolved
exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt
Outdated
Show resolved
Hide resolved
interface ColumnTransformer<TReal : Any, TColumn : Any> { | ||
/** | ||
* Transforms a column value from the underlying `ColumnType` into the real type. | ||
*/ | ||
fun toReal(value: TColumn): TReal | ||
|
||
/** | ||
* Transforms a real type value into a column value. | ||
*/ | ||
fun toColumn(value: TReal): TColumn | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would be the plan for this feature in relation to the existing DAO ColumnWithTransform? Could that class make use of this transformer instead?
And given how transform already works for Entity field columns, if the user does not choose to use memoizedTransform()
for example, could defining the transformation on the table object actually replace defining the transformation on the entity class?
Also, would using the 2 in parallel work ok? For example, defining a column with core/DSL transform()
in table object. Then defining entity class with a field for that column, which then uses a DAO transform()
on every read. Would it create similar behavior as in the nested transformations test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, DAO transform and DSL transform are independent.
The main difference is that on the DSL level it's necessary to make column replacement with a new column type. It's needed because during reading/writing values go through the underlying ColumnType
, so table object must contain the column with the proper column type.
I was trying to make the transform without replacing column but haven't found a way to do that, if you probably have any ideas how it could be done, I'd be happy to know about it).
From the DAO's perspective transformed DSL column is still just a column, so DAO works as before. I added a test testDaoTransformWithDslTransform()
to show that DAO and DSL transforms could be mixed.
/** | ||
* An interface defining the transformation between a database column type and a real type. | ||
* | ||
* @param TReal The real type to which the column value is transformed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to define the Real
type. It's not clear what it is in the air.
I would also check what names are used in applications for that. There are already some known, like Domain
, Model
, ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I took these names from the existing ColumnWithTransform
class. But we may think about complete renaming of these types. What do you think about TOuter/TInner
(and methods toOuter()/toInner()
respectively), or TTransformed/TOriginal
, or TWrapper/TColumn
or something different?
fun toColumn(value: TReal): TColumn | ||
} | ||
|
||
class ColumnTransformerImpl<TReal, TColumn>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please consider making it private
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's used in both: DSL transform, and DAO transform which are in different packages. I could copy it and make private in both packages.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or it can be internal
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's the first thing I tried to do, but it is not reachable from the exposed-dao
module..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another variant is in ktorm:
fun <C : Any, R : Any> Column<C>.transform(
fromUnderlyingValue: (C) -> R,
toUnderlyingValue: (R) -> C
): Column<R>
Hibernate also has similar concept, but it applies modification directly to the sql, and looks like the following annotation:
@ColumnTransformer(
read="decrypt(credit_card_num)"
write="encrypt(?)"
)
String creditCardNumber;
In SqlDelight there is a similar to our transform thing called ColumnAdapter
:
object : ColumnAdapter<List<String>, String> {
override fun decode(databaseValue: String) =
if (databaseValue.isEmpty()) listOf() else databaseValue.split(",")
override fun encode(value: List<String>) = value.joinToString(separator = ",")
}
exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnType.kt
Outdated
Show resolved
Hide resolved
Updated PR. I tried to check how similar APIs look in other libraries and tried to make it more consistent inside Exposed. At the current moment I stopped at the variant of type names for transformation as But for more specific cases there are changes:
Also in all the generic interfaces/methods the source/column type is the first. It was not aligned before. One more change is the renaming of base transformation class for DSL into |
/** | ||
* An interface defining the transformation between a source column type and a target type. | ||
* | ||
* @param Target The type of the column values after transformation | ||
* @param Source The type of the column values without transformation | ||
*/ | ||
interface ColumnTransformer<Source, Target> { | ||
/** | ||
* Applies transformation to the underlying column value ([Source] -> [Target]) | ||
*/ | ||
fun toTarget(value: Source): Target | ||
|
||
/** | ||
* Applies back transformation to the value of the transformed type [Target] to the column type [Source] ([Target] -> [Source]) | ||
*/ | ||
fun toSource(value: Target): Source | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally I think this may be quite confusing. Reading the function names alone, it's not immediately clear which value is meant for the database. With the KDocs it does help imply (but I had to really think) that Source
means what is actually stored in the db and Target
means the value used client-side?
I get that the original toReal()
was also vague, but maybe we could still consider other options?
Something like toColumnValue + toTransformedValue
,toColumnValue + fromColumnValue
or some variant of read + write
like readFromColumn + writeToColumn
?
fun <TColumn : Any, Target : Any> Column<TColumn>.transform( | ||
toTarget: (TColumn) -> Target, | ||
toColumn: (Target) -> TColumn | ||
): Column<Target> = transform(ColumnTransformerImpl(toColumn, toTarget)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand that transform()
doesn't actually have to be used with a ColumnTransformer
object, but it still seems misaligned to me that the parameter names and interface method names are only partially matched:
toTarget
-> toTarget()
toColumn
-> toSource()
Maybe if both were completely different it would be better, but since 1 of them is the same, shouldn't they both match?
fun <TColumn : Any?, Target : Any?> Column<TColumn>.transform( | ||
transformer: ColumnTransformer<TColumn, Target> | ||
): EntityFieldWithTransform<TColumn, Target> = EntityFieldWithTransform(this, transformer, false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the introduction of transform()
in DSL, I'm still not sure that I see the value of DAO's transform()
being kept. They serve the same purpose (except for DAO's additional caching to avoid multiple expensive transformations) and they have the same name. At the moment, I can't think of any other example in the API that has separate DSL and DAO components that are identically named and also so close in functionality.
For example, using the DAO test file ColumnWithTransformTest.kt
(of course without the new DSL+DAO unit tests):
If I cut & paste all the transform
blocks from each entity class and place them in the respective table object instead, all tests still pass. So our current tests at least get the same result using the new functionality on the DSL table instead of the old functionality on the entity class.
I guess I just still don't have an answer to my original question: Why would/should a user choose transform
on Entity
instead of transform
on Table
?
If the answer is, just for potential memoisation, we could still enable that through the new DSL implementation by leaving the actual transformation logic in the core module only, deprecating the DAO transform()
, & reducing the DAO EntityFieldWithTransform
to only having the caching logic (& renaming function to something like withTransformation()
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For me it looks the opposite way: I usually choose if I need Entity or Table and next stick with what I have. Let's discuss it anyway
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, except for two questions for the design:
- naming
- Entity and Table transform behavior
@obabichevjb, could you find a slot for us to discuss this?
object Meals : Table() { | ||
val mealTime: Column<Meal> = time("meal_time") | ||
.transform( | ||
toTarget = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's discuss the naming. Please define the Target
upfront
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you want to have a symmetric name for toColumn
, I would go with toUserType
.
There are also other options we can discuss:
- pack / unpack
- save / load
- encode / decode
- wrap / unwrap
- persist / retrieve
- store / fetch
fun <TColumn : Any?, Target : Any?> Column<TColumn>.transform( | ||
transformer: ColumnTransformer<TColumn, Target> | ||
): EntityFieldWithTransform<TColumn, Target> = EntityFieldWithTransform(this, transformer, false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For me it looks the opposite way: I usually choose if I need Entity or Table and next stick with what I have. Let's discuss it anyway
I would sum up the questions for the discussion:
NamingThe naming So I made a generic transformation interface and specific parameters for specific cases. That's why The method name From the comment above the variant DSL/DAOI also do not see something blocking from removing DAO's transform. Anyway we could mark it as deprecated and check if somebody complains about the case that could not be covered with DSL's only variant. Potential reasons to keep both variants I see:
|
c61be39
to
95640af
Compare
…es. More consistency between DSL and DAO
95640af
to
6fd8b5c
Compare
Updated PR. Methods |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, just some minor KDocs comments. Oh and potential breaking change point.
/** | ||
* Applies back transformation to the value of the transformed type [Wrapped] to the column type [Unwrapped] ([Wrapped] -> [Unwrapped]) | ||
*/ | ||
fun unwrap(value: Wrapped): Unwrapped |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The double "transform" and double "to'" is a bit confusing. I'd just suggest something like any of the following:
- "Returns the underlying column value without a transformation applied"
- "Reverts the value of the transformed type and returns the underlying column type"
- "Applies a reverse transformation on the value of the transformed type and returns the underlying column type"
- "Applies a reverse transformation on the value of the transformed type returning it to the column type"
* ``` | ||
* | ||
* @param Wrapped The type into which the value of the underlying column will be transformed. | ||
* @param Unwrapped The source type of the column. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would change this to be consistent with all the other @param
s in the functions below:
@param Unwrapped The type of the original column.
It's also more clear.
"DAOs transform() is deprecated and will be removed in future releases. " + | ||
"Please use the transform function from the DSL layer.", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the goal is to see what feedback we get from users, I'd also suggest adding a line like:
"Please log a request on YouTrack if a use-case cannot be sufficiently covered by the DSL transform()."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a remark, the ReplaceWith
code seems to be completely useless to convert code like
var finishedAt by AdvisorJobsTable.finishedAt.transform({ it?.toDatabasePrecision() }, { it })
which creates
var finishedAt by run {
{ it?.toDatabasePrecision() }
{ it }
object : Table() {
val c = column().transform(wrap, unwrap)
}
}
that does not compile.
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [org.jetbrains.exposed:exposed-jdbc](https://github.com/JetBrains/Exposed) | `0.52.0` -> `0.53.0` | [![age](https://developer.mend.io/api/mc/badges/age/maven/org.jetbrains.exposed:exposed-jdbc/0.53.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/org.jetbrains.exposed:exposed-jdbc/0.53.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/org.jetbrains.exposed:exposed-jdbc/0.52.0/0.53.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/org.jetbrains.exposed:exposed-jdbc/0.52.0/0.53.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [org.jetbrains.exposed:exposed-core](https://github.com/JetBrains/Exposed) | `0.52.0` -> `0.53.0` | [![age](https://developer.mend.io/api/mc/badges/age/maven/org.jetbrains.exposed:exposed-core/0.53.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/org.jetbrains.exposed:exposed-core/0.53.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/org.jetbrains.exposed:exposed-core/0.52.0/0.53.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/org.jetbrains.exposed:exposed-core/0.52.0/0.53.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Release Notes <details> <summary>JetBrains/Exposed (org.jetbrains.exposed:exposed-jdbc)</summary> ### [`v0.53.0`](https://github.com/JetBrains/Exposed/blob/HEAD/CHANGELOG.md#0530) [Compare Source](https://github.com/JetBrains/Exposed/compare/0.52.0...0.53.0) Infrastructure: - SQLite driver 3.46.0.1 - Spring Framework 6.1.11 - Spring Boot 3.3.2 - junit-bom 5.10.3 Features: - feat: Add time extension function for temporal expressions in Kotlin and Java by [@​joc-a](https://github.com/joc-a) in [https://github.com/JetBrains/Exposed/pull/2121](https://github.com/JetBrains/Exposed/pull/2121) - feat: EXPOSED-435 Allow insertReturning() to set isIgnore = true by [@​bog-walk](https://github.com/bog-walk) in [https://github.com/JetBrains/Exposed/pull/2148](https://github.com/JetBrains/Exposed/pull/2148) - feat: EXPOSED-77 Support entity class for table with composite primary key by [@​bog-walk](https://github.com/bog-walk) in [https://github.com/JetBrains/Exposed/pull/1987](https://github.com/JetBrains/Exposed/pull/1987) - feat: EXPOSED-446 Support N-column inList equality comparisons by [@​bog-walk](https://github.com/bog-walk) in [https://github.com/JetBrains/Exposed/pull/2157](https://github.com/JetBrains/Exposed/pull/2157) - feat: EXPOSED-450 Merge command: PostgreSQL improvements by [@​obabichevjb](https://github.com/obabichevjb) in [https://github.com/JetBrains/Exposed/pull/2161](https://github.com/JetBrains/Exposed/pull/2161) - feat: EXPOSED-388 Support for column type converters by [@​obabichevjb](https://github.com/obabichevjb) in [https://github.com/JetBrains/Exposed/pull/2143](https://github.com/JetBrains/Exposed/pull/2143) - Adding comment text for a query SQL by [@​xJoeWoo](https://github.com/xJoeWoo) in [https://github.com/JetBrains/Exposed/pull/2088](https://github.com/JetBrains/Exposed/pull/2088) - feat: EXPOSED-459 Open AbstractQuery.copyTo() to allow custom Query class extension by [@​bog-walk](https://github.com/bog-walk) in [https://github.com/JetBrains/Exposed/pull/2173](https://github.com/JetBrains/Exposed/pull/2173) - feat: EXPOSED-461 Add time column in Joda-Time module by [@​joc-a](https://github.com/joc-a) in [https://github.com/JetBrains/Exposed/pull/2175](https://github.com/JetBrains/Exposed/pull/2175) Bug fixes: - fix: EXPOSED-424 ClassCastException exception when using `fetchBatchedResults` with `alias` by [@​joc-a](https://github.com/joc-a) in [https://github.com/JetBrains/Exposed/pull/2140](https://github.com/JetBrains/Exposed/pull/2140) - fix: EXPOSED-407 compositeMoney() nullability definition issues by [@​bog-walk](https://github.com/bog-walk) in [https://github.com/JetBrains/Exposed/pull/2137](https://github.com/JetBrains/Exposed/pull/2137) - fix: EXPOSED-415 SchemaUtils incorrectly generates ALTER statements for existing nullable columns by [@​obabichevjb](https://github.com/obabichevjb) in [https://github.com/JetBrains/Exposed/pull/2136](https://github.com/JetBrains/Exposed/pull/2136) - fix: EXPOSED-363 LocalTime and literal(LocalTime) are not the same by [@​joc-a](https://github.com/joc-a) in [https://github.com/JetBrains/Exposed/pull/2152](https://github.com/JetBrains/Exposed/pull/2152) - fix: EXPOSED-432 CurrentDate default is generated as null in MariaDB by [@​joc-a](https://github.com/joc-a) in [https://github.com/JetBrains/Exposed/pull/2149](https://github.com/JetBrains/Exposed/pull/2149) - fix: Allow column reference in default expressions for MySQL and MariaDB by [@​joc-a](https://github.com/joc-a) in [https://github.com/JetBrains/Exposed/pull/2159](https://github.com/JetBrains/Exposed/pull/2159) - fix: EXPOSED-430 Insert and BatchInsert do not return default values by [@​obabichevjb](https://github.com/obabichevjb) in [https://github.com/JetBrains/Exposed/pull/2158](https://github.com/JetBrains/Exposed/pull/2158) - fix: EXPOSED-452 Flaky H2\_Oracle test `testTimestampWithTimeZoneDefaults` by [@​joc-a](https://github.com/joc-a) in [https://github.com/JetBrains/Exposed/pull/2169](https://github.com/JetBrains/Exposed/pull/2169) - EXPOSED-457 The column default value always compares unequal by [@​obabichevjb](https://github.com/obabichevjb) in [https://github.com/JetBrains/Exposed/pull/2170](https://github.com/JetBrains/Exposed/pull/2170) - EXPOSED-409 Custom primary key. Access to the primary key fails with ClassCastException by [@​obabichevjb](https://github.com/obabichevjb) in [https://github.com/JetBrains/Exposed/pull/2151](https://github.com/JetBrains/Exposed/pull/2151) - fix: EXPOSED-447 Eager loading does not work with composite PK entity by [@​bog-walk](https://github.com/bog-walk) in [https://github.com/JetBrains/Exposed/pull/2177](https://github.com/JetBrains/Exposed/pull/2177) Docs: - chore: Add migration sample by [@​joc-a](https://github.com/joc-a) in [https://github.com/JetBrains/Exposed/pull/2144](https://github.com/JetBrains/Exposed/pull/2144) - docs: Change repetitionAttempts to maxAttempts in website docs by [@​bog-walk](https://github.com/bog-walk) in [https://github.com/JetBrains/Exposed/pull/2164](https://github.com/JetBrains/Exposed/pull/2164) - docs: EXPOSED-445 Add documentation for DSL & DAO composite primary keys by [@​bog-walk](https://github.com/bog-walk) in [https://github.com/JetBrains/Exposed/pull/2165](https://github.com/JetBrains/Exposed/pull/2165) - docs: EXPOSED-419 Rework the getting started tutorial by [@​vnikolova](https://github.com/vnikolova) in [https://github.com/JetBrains/Exposed/pull/2160](https://github.com/JetBrains/Exposed/pull/2160) - Configure API documentation for Exposed by [@​e5l](https://github.com/e5l) in [https://github.com/JetBrains/Exposed/pull/2171](https://github.com/JetBrains/Exposed/pull/2171) </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View the [repository job log](https://developer.mend.io/github/DonRobo/home-former). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40NDAuNyIsInVwZGF0ZWRJblZlciI6IjM3LjQ0MC43IiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCIsImxhYmVscyI6W119--> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
This is not a proper replacement for the DAO transform! I have code that transforms one column's value using another column's value (or any DAO property for that matter). The point of having the DAO is moving the complex business domain logic there and I don't see why transformations shouldn't be able to take advantage of that. Please revert the depracation of the DAO transform |
@yassenb Thank you a lot for the comment, to be honest it was not expected that transform allows to make transformation based on the whole entity. I'll take it into account and rethink on what we'll do with the whole feature. |
Description
Here is the PR that allows to transform columns from their original types to another.
Transformations could be nested, and could be applied to nullable columns. Transformation of a non-nullable column produces a non-nullable column, and transformation of a nullable column produces a nullable one. Creating nullable columns requires not only types changing, but also set field
nullable
into true, so still the methodnullable()
must be used.We already have
transform()
for columns on the DAO layer, so I took naming from there (toColumn
,toReal
methods):But probably it's not the perfect variant. I was initially thinking about
Target
/Source
instead ofTReal
/TColumn
respectively. If you prefer the first variant (or have other suggestions) I'd be happy to know. It would be breaking change, but it could be changed for DAO as well if needed.Breaking changes
transform()
andmemoizedTransform()
now usewrap
andunwrap
instead oftoColumn
andtoReal
.ColumnWithTransform
is nowEntityFieldWithTransform
, consolidating properties into a singletransformer
.