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

v0.2.0 release #8

Merged
merged 28 commits into from
Oct 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5850b95
Add the Maven Central badge
ShreckYe Mar 15, 2024
eff246f
Update README.md
ShreckYe Mar 24, 2024
2e56ea5
Bump Kotlin to 2.0.0 and Exposed to the latest via "common-gradle-dep…
ShreckYe May 24, 2024
0d4f50c
Bump Gradle to 8.10 and Kotlin to 2.0.20
ShreckYe Aug 28, 2024
65e18e1
Downgrade Kotlin to 2.0.10 instead
ShreckYe Aug 29, 2024
9063c3d
Add an uncompleted overload of `reflectionBasedClassPropertyDataMappe…
ShreckYe Sep 12, 2024
8c93142
Update the project version to "0.2.0-SNAPSHOT"
ShreckYe Sep 13, 2024
886fe6e
Add an exception message for the `whetherNullDependentColumn` `requir…
ShreckYe Sep 13, 2024
080af47
Fix the message in the previous commit
ShreckYe Sep 13, 2024
512e5b9
Add a `KFunction<R>.callWithCatch` for easier debugging
ShreckYe Sep 13, 2024
e70fe86
Add a KDoc for `OnDuplicateColumnPropertyNames.CHOOSE_FIRST`
ShreckYe Sep 13, 2024
65b97fa
Debug with an internal project, add some comments and KDocs, bump Exp…
ShreckYe Sep 16, 2024
394ac0c
Add a TODO
ShreckYe Sep 16, 2024
2e36916
Add a TODO
ShreckYe Sep 20, 2024
b3c5274
Review and recover a stashed TODO
ShreckYe Sep 20, 2024
c68ebe2
Merge branch 'a-stashed-todo'
ShreckYe Sep 20, 2024
023c415
Add some example code which does not run yet, and add a `selectWithMa…
ShreckYe Sep 21, 2024
5ff9a37
Improve the example code, update README.md with a basic usage guide w…
ShreckYe Sep 22, 2024
e34d24d
Add a comment about stability in README.md
ShreckYe Sep 22, 2024
5d4afff
Rename a file
ShreckYe Sep 22, 2024
5a019d3
Remove a redundant line break
ShreckYe Sep 22, 2024
aeb5fae
Update README.md
ShreckYe Sep 28, 2024
6646fee
Bump the Gradle wrapper to 8.10.2
ShreckYe Oct 16, 2024
5796c90
Bump dependency versions with "com.huanshankeji:common-gradle-depende…
ShreckYe Oct 17, 2024
843b2ca
Bump "common-gradle-dependencies" to "0.8.0-20241016-SNAPSHOT"
ShreckYe Oct 18, 2024
7c38d14
Bump our Gradle dependencies to the release versions
ShreckYe Oct 18, 2024
1f3b520
Merge pull request #6 from huanshankeji/dev-dependent-on-snapshots
ShreckYe Oct 19, 2024
7c8198b
Bump "kotlin-common" to 0.5.1
ShreckYe Oct 19, 2024
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
122 changes: 120 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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<DirectorT>(
val sequelId: Int,
val name: String,
val director: DirectorT
)
typealias FilmDetailsWithDirectorId = FilmDetails<DirectorId>

typealias FilmId = Int

class Film<DirectorT>(val filmId: FilmId, val filmDetails: FilmDetails<DirectorT>)
typealias FilmWithDirectorId = Film<DirectorId>
typealias FullFilm = Film<Director>
```

#### Data entities and attributes

```kotlin
typealias DirectorId = Int

class Director(val directorId: DirectorId, val name: String)

class FilmDetails<DirectorT>(
val sequelId: Int,
val name: String,
val director: DirectorT
)
typealias FilmDetailsWithDirectorId = FilmDetails<DirectorId>

typealias FilmId = Int

class Film<DirectorT>(val filmId: FilmId, val filmDetails: FilmDetails<DirectorT>)
typealias FilmWithDirectorId = Film<DirectorId>
typealias FullFilm = Film<Director>
```

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<Director>` 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<Director>(Directors)
val filmDetailsWithDirectorId = reflectionBasedClassPropertyDataMapper<FilmDetailsWithDirectorId>(
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<DirectorId>(columnPropertyName = Films::directorId.name)
)
)
val filmWithDirectorId = reflectionBasedClassPropertyDataMapper<FilmWithDirectorId>(
Films,
propertyColumnMappingConfigMapOverride = mapOf(
FilmWithDirectorId::filmDetails to PropertyColumnMappingConfig.create<FilmDetailsWithDirectorId>(
// You can pass a nested custom mapper.
customMapper = filmDetailsWithDirectorId
)
)
)
val fullFilm = reflectionBasedClassPropertyDataMapper<FullFilm>(
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>::director to PropertyColumnMappingConfig.create<Director>(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()
```
7 changes: 4 additions & 3 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
4 changes: 2 additions & 2 deletions buildSrc/src/main/kotlin/VersionsAndDependencies.kt
Original file line number Diff line number Diff line change
@@ -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)
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -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
Expand Down
7 changes: 5 additions & 2 deletions gradlew
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#

##############################################################################
#
Expand Down Expand Up @@ -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/.
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions gradlew.bat
Original file line number Diff line number Diff line change
Expand Up @@ -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 ##########################################################################
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ interface SimpleDataMapper<Data : Any> : SimpleDataQueryMapper<Data>, DataUpdate


interface NullableDataQueryMapper<Data> : SimpleNullableDataQueryMapper<Data> {
val neededColumns: List<Column<*>>
val neededColumns: List<Column<*>> // TODO consider refactoring to `ExpressionWithColumnType`
}

interface DataQueryMapper<Data : Any> : NullableDataQueryMapper<Data>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.huanshankeji.exposed.datamapping

import org.jetbrains.exposed.sql.ColumnSet
import org.jetbrains.exposed.sql.Op

fun <Data : Any> ColumnSet.selectWithMapper(mapper: NullableDataQueryMapper<Data>, where: Op<Boolean>? = null) =
select(mapper.neededColumns)
.run { where?.let { where(it) } ?: this }
.asSequence().map { mapper.resultRowToData(it) }
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ sealed class PropertyColumnMapping<Data : Any, PropertyData>(val fctProperty: Fu
class SqlPrimitive<Data : Any, PropertyData>(
fctProperty: FullConcreteTypeProperty1<Data, PropertyData>,
val column: Column<PropertyData>
//val isEntityId : Boolean // TODO implement or remove, or add another separate sealed subclass
) : PropertyColumnMapping<Data, PropertyData>(fctProperty)

class NestedClass<Data : Any, PropertyData>(
Expand Down Expand Up @@ -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<out Table> = table::class
Expand All @@ -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(
Expand Down Expand Up @@ -189,7 +195,9 @@ class PropertyColumnMappingConfig<P>(
// 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"
}
}


Expand Down Expand Up @@ -224,6 +232,7 @@ class PropertyColumnMappingConfig<P>(

// ADT: algebraic data type
sealed class Adt<Data : Any> {
// 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<Data : Any>(val nestedConfigMap: PropertyColumnMappingConfigMap<Data>) :
Adt<Data>()

Expand Down Expand Up @@ -278,7 +287,7 @@ private fun <Data : Any> doGetDefaultClassPropertyColumnMappings(
customMappings: PropertyColumnMappings<Data> = 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<Data> {
val customMappingPropertySet = customMappings.asSequence().map { it.fctProperty }.toSet()

Expand Down Expand Up @@ -347,7 +356,7 @@ private fun <Data : Any> 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.
*/
Expand Down Expand Up @@ -427,7 +436,8 @@ private fun <Data : Any> doGetDefaultClassPropertyColumnMappings(

fun <Data : Any> getDefaultClassPropertyColumnMappings(
fullConcreteTypeClass: FullConcreteTypeClass<Data>,
tables: List<Table>, onDuplicateColumnPropertyNames: OnDuplicateColumnPropertyNames = CHOOSE_FIRST, // TODO consider removing this default argument as there is one for joins now
tables: List<Table>,
onDuplicateColumnPropertyNames: OnDuplicateColumnPropertyNames = CHOOSE_FIRST, // TODO consider removing this default argument as there is one for joins now
propertyColumnMappingConfigMapOverride: PropertyColumnMappingConfigMap<Data> = emptyMap(),
customMappings: PropertyColumnMappings<Data> = emptyList()
): ClassPropertyColumnMappings<Data> =
Expand All @@ -439,6 +449,7 @@ fun <Data : Any> 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<Data : Any>(
val fullConcreteTypeClass: FullConcreteTypeClass<Data>,
Expand All @@ -459,7 +470,7 @@ private fun <Data : Any> constructDataWithResultRow(
classPropertyColumnMappings: ClassPropertyColumnMappings<Data>,
resultRow: ResultRow
): Data =
fctClass.kClass.primaryConstructor!!.call(*classPropertyColumnMappings.map {
fctClass.kClass.primaryConstructor!!.callWithCatch(*classPropertyColumnMappings.map {
fun <PropertyReturnT> typeParameterHelper(
propertyColumnMapping: PropertyColumnMapping<Data, PropertyReturnT>,
nestedFctClass: FullConcreteTypeClass<PropertyReturnT & Any>
Expand Down Expand Up @@ -511,8 +522,10 @@ fun <Data : Any> setUpdateBuilder(
fun <PropertyData> typeParameterHelper(propertyColumnMapping: PropertyColumnMapping<Data, PropertyData>) {
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
Expand Down Expand Up @@ -619,6 +632,9 @@ fun ClassPropertyColumnMappings<*>.getColumnSet(): Set<Column<*>> =

// 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 <reified Data : Any> reflectionBasedClassPropertyDataMapper(
tables: List<Table>,
onDuplicateColumnPropertyNames: OnDuplicateColumnPropertyNames = CHOOSE_FIRST, // TODO consider removing this default argument as there is one for joins now
Expand All @@ -645,8 +661,32 @@ inline fun <reified Data : Any/*, TableT : Table*/> reflectionBasedClassProperty
* A shortcut for [Join]s.
*/
inline fun <reified Data : Any> reflectionBasedClassPropertyDataMapper(
join : Join,
join: Join,
propertyColumnMappingConfigMapOverride: PropertyColumnMappingConfigMap<Data> = emptyMap(),
customMappings: PropertyColumnMappings<Data> = emptyList()
) =
reflectionBasedClassPropertyDataMapper(join.targetTables(), CHOOSE_FIRST, propertyColumnMappingConfigMapOverride, customMappings)
reflectionBasedClassPropertyDataMapper(
join.targetTables(), CHOOSE_FIRST, propertyColumnMappingConfigMapOverride, customMappings
)

// not completely implemented yet
private inline fun <reified Data : Any> reflectionBasedClassPropertyDataMapper(
queryAlias: QueryAlias,
propertyColumnMappingConfigMapOverride: PropertyColumnMappingConfigMap<Data> = emptyMap(),
customMappings: PropertyColumnMappings<Data> = emptyList()
): ReflectionBasedClassPropertyDataMapper<Data> =
reflectionBasedClassPropertyDataMapper(
queryAlias.query.targets, CHOOSE_FIRST, propertyColumnMappingConfigMapOverride, customMappings
).run {
TODO("map the columns to alias columns")
}

/**
* @see targetTables
*/
private inline fun <reified Data : Any> reflectionBasedClassPropertyDataMapper(
columnSet: ColumnSet,
propertyColumnMappingConfigMapOverride: PropertyColumnMappingConfigMap<Data> = emptyMap(),
customMappings: PropertyColumnMappings<Data> = emptyList()
): ReflectionBasedClassPropertyDataMapper<Data> =
TODO()
Original file line number Diff line number Diff line change
@@ -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 <R> KFunction<R>.callWithCatch(vararg args: Any?) =
try {
call(args = args)
} catch (e: Exception) {
throw ReflectionFunctionInvocationException(this, args = args, cause = e)
}
Loading