diff --git a/README.md b/README.md index d4da6ab..e0bb35c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,123 @@ # Exposed GADT mapping -A library based on [Exposed](https://github.com/JetBrains/Exposed) [DSL](https://github.com/JetBrains/Exposed/wiki/DSL) providing mappings between data entities and tables with support for generic algebraic data types (nested classes, type parameters, and sealed classes) +[![Maven Central](https://img.shields.io/maven-central/v/com.huanshankeji/exposed-adt-mapping)](https://search.maven.org/artifact/com.huanshankeji/exposed-adt-mapping) -This project is an attemp to provide an alternative to [Exposed DAO](https://github.com/JetBrains/Exposed/wiki/DAO) while supporting some more advanced functional programming features. Also see JetBrains/Exposed#24 for more details. +A library based on [Exposed](https://github.com/JetBrains/Exposed) [DSL](https://github.com/JetBrains/Exposed/wiki/DSL) providing mappings between data entities and tables with support for GADT (generalized algebraic data type), aka features including nested properties of composite class types, type parameters and their type inference, and sealed classes + +This project is an attempt to provide an alternative to [Exposed DAO](https://github.com/JetBrains/Exposed/wiki/DAO) while supporting some more advanced functional programming features. See JetBrains/Exposed#24 for more details. + +## Basic usage guide + +Please note that these APIs are far from stable. There are going to be refactors in future releases. + +### Table and data definitions + +#### Tables and joins + +```kotlin +typealias DirectorId = Int + +class Director(val directorId: DirectorId, val name: String) + +class FilmDetails( + val sequelId: Int, + val name: String, + val director: DirectorT +) +typealias FilmDetailsWithDirectorId = FilmDetails + +typealias FilmId = Int + +class Film(val filmId: FilmId, val filmDetails: FilmDetails) +typealias FilmWithDirectorId = Film +typealias FullFilm = Film +``` + +#### Data entities and attributes + +```kotlin +typealias DirectorId = Int + +class Director(val directorId: DirectorId, val name: String) + +class FilmDetails( + val sequelId: Int, + val name: String, + val director: DirectorT +) +typealias FilmDetailsWithDirectorId = FilmDetails + +typealias FilmId = Int + +class Film(val filmId: FilmId, val filmDetails: FilmDetails) +typealias FilmWithDirectorId = Film +typealias FullFilm = Film +``` + +A nested composite class property can either map to flattened fields or a table referenced by a foreign key: `FilmDetails` is a nested class in `Film`, but the corresponding table `Films` has the `FilmDetails` members/fields flattened directly instead of referencing a corresponding table for `FilmDetails` with a foreign key; on the contrary, a `director : Director` member of `FilmDetails` maps to the `Directors` table referenced. + +As laid out above in the code, a recommended approach to define data types is to make necessary use of type parameters to improve code reuse. + +### Create mappers + +You can create mappers with the overloaded `reflectionBasedClassPropertyDataMapper` functions. Pass the `propertyColumnMappingConfigMapOverride` parameter to override the default options. + +```kotlin +object Mappers { + val director = reflectionBasedClassPropertyDataMapper(Directors) + val filmDetailsWithDirectorId = reflectionBasedClassPropertyDataMapper( + Films, + propertyColumnMappingConfigMapOverride = mapOf( + // The default name is the property name "director", but there is no column property with such a name, therefore we need to pass a custom name. + FilmDetailsWithDirectorId::director to PropertyColumnMappingConfig.create(columnPropertyName = Films::directorId.name) + ) + ) + val filmWithDirectorId = reflectionBasedClassPropertyDataMapper( + Films, + propertyColumnMappingConfigMapOverride = mapOf( + FilmWithDirectorId::filmDetails to PropertyColumnMappingConfig.create( + // You can pass a nested custom mapper. + customMapper = filmDetailsWithDirectorId + ) + ) + ) + val fullFilm = reflectionBasedClassPropertyDataMapper( + filmsLeftJoinDirectors, + propertyColumnMappingConfigMapOverride = mapOf( + FullFilm::filmDetails to PropertyColumnMappingConfig.create( + adt = PropertyColumnMappingConfig.Adt.Product( + mapOf( + // Because `name` is a duplicate name column so a custom mapper has to be passed here, otherwise the `CHOOSE_FIRST` option maps the data property `Director::name` to the wrong column `Films::name`. + FilmDetails::director to PropertyColumnMappingConfig.create(customMapper = director) + ) + ) + ) + ) + ) +} +``` + +### CRUD operations + +Call `updateBuilderSetter` to get a setter lambda to pass to `insert` or `update`. Call `selectWithMapper` to execute a query with a mapper (not available yet, available soon in 0.2.0). + +```kotlin +val directorId = 1 +val director = Director(directorId, "George Lucas") +Directors.insert(Mappers.director.updateBuilderSetter(director)) + +val episodeIFilmDetails = FilmDetails(1, "Star Wars: Episode I – The Phantom Menace", directorId) +Films.insert(Mappers.filmDetailsWithDirectorId.updateBuilderSetter(episodeIFilmDetails)) // insert without the ID since it's `AUTO_INCREMENT` + +val filmId = 2 +val episodeIIFilmDetails = FilmDetails(2, "Star Wars: Episode II – Attack of the Clones", directorId) +val filmWithDirectorId = FilmWithDirectorId(filmId, episodeIIFilmDetails) +Films.insert(Mappers.filmWithDirectorId.updateBuilderSetter(filmWithDirectorId)) // insert with the ID + +val fullFilm = with(Mappers.fullFilm) { + resultRowToData(filmsLeftJoinDirectors.select(neededColumns).where(Films.filmId eq filmId).single()) +} +// not available yet, available soon in 0.2.0 +val fullFilms = + filmsLeftJoinDirectors.selectWithMapper(Mappers.fullFilm, Films.filmId inList listOf(1, 2)).toList() +``` diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index c7076f6..bf8774c 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -15,7 +15,8 @@ repositories { } dependencies { - implementation(kotlin("gradle-plugin", "1.9.23")) - implementation("com.huanshankeji:common-gradle-dependencies:0.7.1-20240314") - implementation("com.huanshankeji.team:gradle-plugins:0.5.1") + // With Kotlin 2.0.20, a "Could not parse POM" build error occurs in the JVM projects of some dependent projects. + implementation(kotlin("gradle-plugin", "2.0.10")) + implementation("com.huanshankeji:common-gradle-dependencies:0.8.0-20241016") // don't use a snapshot version in a main branch + implementation("com.huanshankeji.team:gradle-plugins:0.6.0") // don't use a snapshot version in a main branch } diff --git a/buildSrc/src/main/kotlin/VersionsAndDependencies.kt b/buildSrc/src/main/kotlin/VersionsAndDependencies.kt index d3c3958..ab8d702 100644 --- a/buildSrc/src/main/kotlin/VersionsAndDependencies.kt +++ b/buildSrc/src/main/kotlin/VersionsAndDependencies.kt @@ -1,7 +1,7 @@ import com.huanshankeji.CommonDependencies import com.huanshankeji.CommonVersions -val projectVersion = "0.1.0-SNAPSHOT" +val projectVersion = "0.2.0-SNAPSHOT" -val commonVersions = CommonVersions() +val commonVersions = CommonVersions(kotlinCommon = "0.5.1") val commonDependencies = CommonDependencies(commonVersions) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd49..a4b76b9 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2ea3535..79eb9d0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a4..f5feea6 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 25da30d..9d21a21 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## diff --git a/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/DataMapperInterfaces.kt b/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/DataMapperInterfaces.kt index 5610b4d..307c66a 100644 --- a/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/DataMapperInterfaces.kt +++ b/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/DataMapperInterfaces.kt @@ -26,7 +26,7 @@ interface SimpleDataMapper : SimpleDataQueryMapper, DataUpdate interface NullableDataQueryMapper : SimpleNullableDataQueryMapper { - val neededColumns: List> + val neededColumns: List> // TODO consider refactoring to `ExpressionWithColumnType` } interface DataQueryMapper : NullableDataQueryMapper diff --git a/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/Table.kt b/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/Table.kt new file mode 100644 index 0000000..9ef6b02 --- /dev/null +++ b/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/Table.kt @@ -0,0 +1,9 @@ +package com.huanshankeji.exposed.datamapping + +import org.jetbrains.exposed.sql.ColumnSet +import org.jetbrains.exposed.sql.Op + +fun ColumnSet.selectWithMapper(mapper: NullableDataQueryMapper, where: Op? = null) = + select(mapper.neededColumns) + .run { where?.let { where(it) } ?: this } + .asSequence().map { mapper.resultRowToData(it) } diff --git a/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/classproperty/ClassPropertyMapping.kt b/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/classproperty/ClassPropertyMapping.kt index 822e9f9..a800f3b 100644 --- a/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/classproperty/ClassPropertyMapping.kt +++ b/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/classproperty/ClassPropertyMapping.kt @@ -40,6 +40,7 @@ sealed class PropertyColumnMapping(val fctProperty: Fu class SqlPrimitive( fctProperty: FullConcreteTypeProperty1, val column: Column + //val isEntityId : Boolean // TODO implement or remove, or add another separate sealed subclass ) : PropertyColumnMapping(fctProperty) class NestedClass( @@ -114,7 +115,8 @@ fun KClass<*>.isExposedSqlPrimitiveType(): Boolean = fun KType.isExposedSqlPrimitiveType() = (classifier as KClass<*>).isExposedSqlPrimitiveType() -class ColumnWithPropertyName(val propertyName: String, val column: Column<*>) +// made a data class so it can be printed while debugging +data class ColumnWithPropertyName(val propertyName: String, val column: Column<*>) fun getColumnsWithPropertyNamesWithoutTypeParameter( table: Table, clazz: KClass = table::class @@ -125,7 +127,11 @@ fun getColumnsWithPropertyNamesWithoutTypeParameter( } enum class OnDuplicateColumnPropertyNames { - CHOOSE_FIRST, THROW + /** + * Use left joins only for this to work properly. Don't use right joins. + */ + CHOOSE_FIRST, + THROW } fun getColumnByPropertyNameMap( @@ -189,7 +195,9 @@ class PropertyColumnMappingConfig

( // Non-nullable properties can be skipped when updating but not when querying. if (usedForQuery) require(!skip) - require(whetherNullDependentColumn === null) + require(whetherNullDependentColumn === null) { + "`whetherNullDependentColumn` should be null for a not-null type $type" + } } @@ -224,6 +232,7 @@ class PropertyColumnMappingConfig

( // ADT: algebraic data type sealed class Adt { + // TODO use a custom type instead of `Pair` for the map entries used to construct the map so more type-safety can be ensured class Product(val nestedConfigMap: PropertyColumnMappingConfigMap) : Adt() @@ -278,7 +287,7 @@ private fun doGetDefaultClassPropertyColumnMappings( customMappings: PropertyColumnMappings = emptyList() /* TODO Constructing `FullConcreteTypeProperty1` seems complicated after the code is refactored. Consider refactoring `PropertyColumnMapping` with one extra `Property` type parameter and apply simple `KProperty` for `customMappings`, - or merging it into config. */ + or merging it into `propertyColumnMappingConfigMapOverride` for better usability. */ ): ClassPropertyColumnMappings { val customMappingPropertySet = customMappings.asSequence().map { it.fctProperty }.toSet() @@ -347,7 +356,7 @@ private fun doGetDefaultClassPropertyColumnMappings( 1. find the first non-nullable column with the suffix "id". They all have their drawbacks. - The first approach is too unpredictable, adding or removing properties can affect which column to choose. + Both the first approach and the third approach are too unpredictable, adding or removing properties can affect which column to choose. Both the second and the third approach can't deal with the case where the column is not within the mapped columns, which happens when selecting a small portion of the fields as data. */ @@ -427,7 +436,8 @@ private fun doGetDefaultClassPropertyColumnMappings( fun getDefaultClassPropertyColumnMappings( fullConcreteTypeClass: FullConcreteTypeClass, - tables: List, onDuplicateColumnPropertyNames: OnDuplicateColumnPropertyNames = CHOOSE_FIRST, // TODO consider removing this default argument as there is one for joins now + tables: List
, + onDuplicateColumnPropertyNames: OnDuplicateColumnPropertyNames = CHOOSE_FIRST, // TODO consider removing this default argument as there is one for joins now propertyColumnMappingConfigMapOverride: PropertyColumnMappingConfigMap = emptyMap(), customMappings: PropertyColumnMappings = emptyList() ): ClassPropertyColumnMappings = @@ -439,6 +449,7 @@ fun getDefaultClassPropertyColumnMappings( ) // TODO: decouple query mapper and update mapper. +// TODO add the `ColumnSet` as a type parameter since all kinds of `ColumnSet`s will be supported in `reflectionBasedClassPropertyDataMapper` /** Supports classes with nested composite class properties and multiple tables */ class ReflectionBasedClassPropertyDataMapper( val fullConcreteTypeClass: FullConcreteTypeClass, @@ -459,7 +470,7 @@ private fun constructDataWithResultRow( classPropertyColumnMappings: ClassPropertyColumnMappings, resultRow: ResultRow ): Data = - fctClass.kClass.primaryConstructor!!.call(*classPropertyColumnMappings.map { + fctClass.kClass.primaryConstructor!!.callWithCatch(*classPropertyColumnMappings.map { fun typeParameterHelper( propertyColumnMapping: PropertyColumnMapping, nestedFctClass: FullConcreteTypeClass @@ -511,8 +522,10 @@ fun setUpdateBuilder( fun typeParameterHelper(propertyColumnMapping: PropertyColumnMapping) { val propertyData = propertyColumnMapping.fctProperty.kProperty(data) when (propertyColumnMapping) { - is SqlPrimitive -> - updateBuilder[propertyColumnMapping.column] = propertyData + is SqlPrimitive -> { + // TODO also consider judging whether it's an entity ID when constructing the `PropertyColumnMapping` + updateBuilder.setWithColumnPossiblyBeingEntityId(propertyColumnMapping.column, propertyData) + } is NestedClass -> { // `propertyColumnMapping.nullability` is not needed here @@ -619,6 +632,9 @@ fun ClassPropertyColumnMappings<*>.getColumnSet(): Set> = // TODO add a version of `reflectionBasedClassPropertyDataMapper` that takes column properties and make the following 2 functions depend on it +/** + * @param tables be sure that the tables are passed in the right order. + */ inline fun reflectionBasedClassPropertyDataMapper( tables: List
, onDuplicateColumnPropertyNames: OnDuplicateColumnPropertyNames = CHOOSE_FIRST, // TODO consider removing this default argument as there is one for joins now @@ -645,8 +661,32 @@ inline fun reflectionBasedClassProperty * A shortcut for [Join]s. */ inline fun reflectionBasedClassPropertyDataMapper( - join : Join, + join: Join, propertyColumnMappingConfigMapOverride: PropertyColumnMappingConfigMap = emptyMap(), customMappings: PropertyColumnMappings = emptyList() ) = - reflectionBasedClassPropertyDataMapper(join.targetTables(), CHOOSE_FIRST, propertyColumnMappingConfigMapOverride, customMappings) + reflectionBasedClassPropertyDataMapper( + join.targetTables(), CHOOSE_FIRST, propertyColumnMappingConfigMapOverride, customMappings + ) + +// not completely implemented yet +private inline fun reflectionBasedClassPropertyDataMapper( + queryAlias: QueryAlias, + propertyColumnMappingConfigMapOverride: PropertyColumnMappingConfigMap = emptyMap(), + customMappings: PropertyColumnMappings = emptyList() +): ReflectionBasedClassPropertyDataMapper = + reflectionBasedClassPropertyDataMapper( + queryAlias.query.targets, CHOOSE_FIRST, propertyColumnMappingConfigMapOverride, customMappings + ).run { + TODO("map the columns to alias columns") + } + +/** + * @see targetTables + */ +private inline fun reflectionBasedClassPropertyDataMapper( + columnSet: ColumnSet, + propertyColumnMappingConfigMapOverride: PropertyColumnMappingConfigMap = emptyMap(), + customMappings: PropertyColumnMappings = emptyList() +): ReflectionBasedClassPropertyDataMapper = + TODO() diff --git a/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/classproperty/ReflectionFunctionInvocation.kt b/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/classproperty/ReflectionFunctionInvocation.kt new file mode 100644 index 0000000..66987f1 --- /dev/null +++ b/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/classproperty/ReflectionFunctionInvocation.kt @@ -0,0 +1,14 @@ +package com.huanshankeji.exposed.datamapping.classproperty + +import kotlin.reflect.KFunction + +class ReflectionFunctionInvocationException(constructor: KFunction<*>, vararg args: Any?, cause: Throwable) : + Exception("calling the function $constructor with params ${args.toList()}", cause) + +// also consider catching only in debug/test mode +fun KFunction.callWithCatch(vararg args: Any?) = + try { + call(args = args) + } catch (e: Exception) { + throw ReflectionFunctionInvocationException(this, args = args, cause = e) + } diff --git a/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/classproperty/SimpleClassPropertyMapping.kt b/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/classproperty/SimpleClassPropertyMapping.kt index 8dcf713..7c55ec2 100644 --- a/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/classproperty/SimpleClassPropertyMapping.kt +++ b/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/classproperty/SimpleClassPropertyMapping.kt @@ -2,14 +2,16 @@ package com.huanshankeji.exposed.datamapping.classproperty import com.huanshankeji.exposed.datamapping.SimpleDataMapper import org.jetbrains.exposed.dao.id.EntityID -import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.Alias +import org.jetbrains.exposed.sql.Column +import org.jetbrains.exposed.sql.ResultRow +import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.statements.UpdateBuilder import kotlin.reflect.KClass import kotlin.reflect.KFunction import kotlin.reflect.KProperty1 import kotlin.reflect.full.memberProperties import kotlin.reflect.full.primaryConstructor -import kotlin.sequences.Sequence fun ResultRow.getValue(column: Column<*>): Any? = this[column].let { @@ -23,13 +25,13 @@ interface ReflectionBasedSimpleClassPropertyDataMapper : SimpleDataM override fun resultRowToData(resultRow: ResultRow): Data { val params = propertyAndColumnPairs.map { (_, column) -> resultRow.getValue(column) } - return dataPrimaryConstructor.call(*params.toTypedArray()) + return dataPrimaryConstructor.callWithCatch(*params.toTypedArray()) } override fun setUpdateBuilder(data: Data, updateBuilder: UpdateBuilder<*>) { for ((property, column) in propertyAndColumnPairs) @Suppress("UNCHECKED_CAST") - updateBuilder[column as Column] = property(data) + updateBuilder.setWithColumnPossiblyBeingEntityId(column as Column, property(data)) } } diff --git a/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/classproperty/UpdateBuilder.kt b/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/classproperty/UpdateBuilder.kt new file mode 100644 index 0000000..1e50519 --- /dev/null +++ b/lib/src/main/kotlin/com/huanshankeji/exposed/datamapping/classproperty/UpdateBuilder.kt @@ -0,0 +1,22 @@ +package com.huanshankeji.exposed.datamapping.classproperty + +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.sql.Column +import org.jetbrains.exposed.sql.EntityIDColumnType +import org.jetbrains.exposed.sql.statements.UpdateBuilder + +/** + * This is a workaround for a column that is possibly an [EntityID]. + * It seems needed since Exposed 0.53.0. + */ +fun UpdateBuilder<*>.setWithColumnPossiblyBeingEntityId(column: Column, value: S) { + if (column.columnType is EntityIDColumnType<*>) { + fun > typeParameterHelper(column: Column>, value: S) { + this[column] = value + } + typeParameterHelper( + column as Column>>, value as Comparable + ) + } else + this[column] = value +} diff --git a/lib/src/test/kotlin/com/huanshankeji/exposed/datamapping/classproperty/Examples.kt b/lib/src/test/kotlin/com/huanshankeji/exposed/datamapping/classproperty/Examples.kt new file mode 100644 index 0000000..60f2707 --- /dev/null +++ b/lib/src/test/kotlin/com/huanshankeji/exposed/datamapping/classproperty/Examples.kt @@ -0,0 +1,101 @@ +package com.huanshankeji.exposed.datamapping.classproperty + +import com.huanshankeji.exposed.datamapping.selectWithMapper +import com.huanshankeji.exposed.datamapping.updateBuilderSetter +import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList +import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.transactions.transaction + +// copied and adapted from https://jetbrains.github.io/Exposed/deep-dive-into-dao.html + +object Directors : IntIdTable("directors") { + val directorId = id + val name = varchar("name", 50) +} + +object Films : IntIdTable() { + val filmId = id + val sequelId = integer("sequel_id").uniqueIndex() + val name = varchar("name", 50) + val directorId = integer("director_id").references(Directors.directorId) +} + +val filmsLeftJoinDirectors = Films leftJoin Directors + + +typealias DirectorId = Int + +class Director(val directorId: DirectorId, val name: String) + +class FilmDetails( + val sequelId: Int, + val name: String, + val director: DirectorT +) +typealias FilmDetailsWithDirectorId = FilmDetails + +typealias FilmId = Int + +class Film(val filmId: FilmId, val filmDetails: FilmDetails) +typealias FilmWithDirectorId = Film +typealias FullFilm = Film + + +object Mappers { + val director = reflectionBasedClassPropertyDataMapper(Directors) + val filmDetailsWithDirectorId = reflectionBasedClassPropertyDataMapper( + Films, + propertyColumnMappingConfigMapOverride = mapOf( + // The default name is the property name "director", but there is no column property with such a name, therefore we need to pass a custom name. + FilmDetailsWithDirectorId::director to PropertyColumnMappingConfig.create(columnPropertyName = Films::directorId.name) + ) + ) + val filmWithDirectorId = reflectionBasedClassPropertyDataMapper( + Films, + propertyColumnMappingConfigMapOverride = mapOf( + FilmWithDirectorId::filmDetails to PropertyColumnMappingConfig.create( + // You can pass a nested custom mapper. + customMapper = filmDetailsWithDirectorId + ) + ) + ) + val fullFilm = reflectionBasedClassPropertyDataMapper( + filmsLeftJoinDirectors, + propertyColumnMappingConfigMapOverride = mapOf( + FullFilm::filmDetails to PropertyColumnMappingConfig.create( + adt = PropertyColumnMappingConfig.Adt.Product( + mapOf( + // Because `name` is a duplicate name column so a custom mapper has to be passed here, otherwise the `CHOOSE_FIRST` option maps the data property `Director::name` to the wrong column `Films::name`. + FilmDetails::director to PropertyColumnMappingConfig.create(customMapper = director) + ) + ) + ) + ) + ) +} + +fun main() { + // TODO create a database with Testcontainers and connect + transaction { + val directorId = 1 + val director = Director(directorId, "George Lucas") + Directors.insert(Mappers.director.updateBuilderSetter(director)) + + val episodeIFilmDetails = FilmDetails(1, "Star Wars: Episode I – The Phantom Menace", directorId) + Films.insert(Mappers.filmDetailsWithDirectorId.updateBuilderSetter(episodeIFilmDetails)) // insert without the ID since it's `AUTO_INCREMENT` + + val filmId = 2 + val episodeIIFilmDetails = FilmDetails(2, "Star Wars: Episode II – Attack of the Clones", directorId) + val filmWithDirectorId = FilmWithDirectorId(filmId, episodeIIFilmDetails) + Films.insert(Mappers.filmWithDirectorId.updateBuilderSetter(filmWithDirectorId)) // insert with the ID + + val fullFilm = with(Mappers.fullFilm) { + resultRowToData(filmsLeftJoinDirectors.select(neededColumns).where(Films.filmId eq filmId).single()) + } + // not available yet, available soon in 0.2.0 + val fullFilms = + filmsLeftJoinDirectors.selectWithMapper(Mappers.fullFilm, Films.filmId inList listOf(1, 2)).toList() + } +}